Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.moda.app/llms.txt

Use this file to discover all available pages before exploring further.

Brief-to-deck (PDF intake)

Problem: A user uploads a brief PDF to your app. You want to produce a branded pitch deck from it and return a PPTX download URL.

Primitives

  • POST /v1/uploads — upload the PDF (multipart)
  • POST /v1/tasks with attachments: [{file_id, role: "source"}] + brand_kit_id + number_of_slides
  • Webhook OR polling to detect completion
  • POST /v1/canvases/{id}/export?format=pptx — synchronous export

TypeScript (Node 20+)

// server/api/generate-deck.ts
import { FastifyInstance } from "fastify";
import fs from "node:fs";

const HEADERS = {
  Authorization: `Bearer ${process.env.MODA_API_KEY!}`,
  "Moda-Version": "2026-05-01",
};

export default async function (app: FastifyInstance) {
  app.post("/generate-deck", async (req, reply) => {
    const { pdfPath, userId } = (await req.body) as { pdfPath: string; userId: string };

    // 1. upload the brief
    const form = new FormData();
    form.set("file", new Blob([fs.readFileSync(pdfPath)]), "brief.pdf");
    const uploadRes = await fetch("https://api.moda.app/v1/uploads", {
      method: "POST",
      headers: { Authorization: HEADERS.Authorization, "Moda-Version": HEADERS["Moda-Version"] },
      body: form,
    });
    const brief = await uploadRes.json();                        // { id: "file_...", ... }

    // 2. find default brand kit
    const kits = await fetch("https://api.moda.app/v1/brand-kits", { headers: HEADERS }).then(r => r.json());
    const kit = kits.data.find((k: any) => k.is_default);

    // 3. start the design task
    const task = await fetch("https://api.moda.app/v1/tasks", {
      method: "POST",
      headers: { ...HEADERS, "Content-Type": "application/json" },
      body: JSON.stringify({
        prompt:
          "Build a pitch deck from the attached brief. Use our brand styling. " +
          "Prioritize real data and quotes from the brief; do not invent specifics.",
        format: { category: "slides", width: 1920, height: 1080 },
        number_of_slides: 10,
        brand_kit_id: kit?.id,
        attachments: [
          { file_id: brief.id, role: "source", label: "Brief" },
        ],
        callback_url: "https://myapp.com/webhooks/moda",
        idempotency_key: `deck:${userId}:${brief.id}`,
      }),
    }).then(r => r.json());

    reply.send({
      message: "Generating deck — takes 2–10 minutes. You'll get a notification when it's ready.",
      task_id: task.id,
      canvas_url: task.links?.canvas ?? null,                    // useful placeholder while it cooks
    });
  });
}
Webhook handler (abbreviated — full handler in webhook-receiver.md):
async function handleAsync(event: any) {
  if (event.type !== "task.succeeded") return;
  const canvas_id = event.data.result.canvas_id;

  // 4. export synchronously
  const exp = await fetch(
    `https://api.moda.app/v1/canvases/${canvas_id}/export?format=pptx`,
    { method: "POST", headers: HEADERS },
  ).then(r => r.json());
  // exp.url — signed URL valid 7 days

  await notifyUser(
    eventIdToUserId(event.id),
    `Your deck is ready: ${event.data.result.canvas_url}\nPPTX: ${exp.url}`,
  );
}

Python (FastAPI + httpx)

# server/api/generate_deck.py
import os, httpx
from fastapi import APIRouter, UploadFile, File, Form

router = APIRouter()
HEADERS = {
    "Authorization": f"Bearer {os.environ['MODA_API_KEY']}",
    "Moda-Version": "2026-05-01",
}

@router.post("/generate-deck")
async def generate_deck(
    file: UploadFile = File(...),
    user_id: str = Form(...),
):
    async with httpx.AsyncClient(
        base_url="https://api.moda.app/v1", headers=HEADERS, timeout=60,
    ) as c:
        # 1. upload
        content = await file.read()
        brief = (await c.post(
            "/uploads",
            files={"file": (file.filename, content, file.content_type)},
        )).json()

        # 2. default brand kit
        kits = (await c.get("/brand-kits")).json()
        kit_id = next((k["id"] for k in kits["data"] if k["is_default"]), None)

        # 3. start task
        task = (await c.post("/tasks", json={
            "prompt": (
                "Build a pitch deck from the attached brief. Use our brand styling. "
                "Prioritize real data and quotes from the brief; do not invent specifics."
            ),
            "format": {"category": "slides", "width": 1920, "height": 1080},
            "number_of_slides": 10,
            "brand_kit_id": kit_id,
            "attachments": [
                {"file_id": brief["id"], "role": "source", "label": "Brief"},
            ],
            "callback_url": "https://myapp.com/webhooks/moda",
            "idempotency_key": f"deck:{user_id}:{brief['id']}",
        })).json()

    return {
        "message": "Generating deck — takes 2–10 minutes.",
        "task_id": task["id"],
    }
Webhook handler does the export:
@app.post("/webhooks/moda")
async def moda_webhook(...):
    # ... verify signature (see webhook-receiver.md) ...
    event = json.loads(body)
    if event["type"] == "task.succeeded":
        canvas_id = event["data"]["result"]["canvas_id"]
        async with httpx.AsyncClient(base_url="https://api.moda.app/v1", headers=HEADERS) as c:
            exp = (await c.post(
                f"/canvases/{canvas_id}/export",
                params={"format": "pptx"},
            )).json()
        await notify_user(user_for_event(event["id"]),
                          canvas_url=event["data"]["result"]["canvas_url"],
                          pptx_url=exp["url"])
    return {"ok": True}

Gotchas

  • idempotency_key encoding. Using {user_id}:{file_id} means re-uploading the same PDF for the same user hits the same task (desirable — idempotent, no wasted compute). If you want a fresh task each time, include a timestamp.
  • Brief → role: "source". The agent extracts content. Passing it as reference would make the deck mimic the PDF’s formatting — not what you want.
  • number_of_slides is a hint, not a hard cap. If the brief is thin, the agent may produce fewer; if it’s rich, marginally more.
  • Export is synchronous. No polling the export. One POST, one URL.
  • Signed PPTX URL expires after 7 days. Either surface it directly to the user (they’ll click within minutes usually) or download + re-host yourself.
  • callback_url requires API-key auth. This recipe runs server-side so that’s fine. An OAuth / MCP caller can’t set callback_url — they’d have to poll.
  • If brand_kit_id is null (empty team), the design task still runs but without brand styling — or you can error out. Decide based on UX: for internal tools, off-brand is fine; for customer-facing, force the user to set up a brand kit first.

See also