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.

Task envelope

Every task-shaped operation — design, export (when async), remix, brand-kit extraction — returns the canonical Task envelope. Pin Moda-Version: 2026-05-01 to get this shape.

Full shape

{
  "id": "task_01HT9WK8N3M2J4A5Z6P7Q8R9TV",
  "kind": "design",
  "status": "queued",
  "created_at": "2026-04-15T12:00:00+00:00",
  "started_at": null,
  "completed_at": null,
  "progress": null,
  "attempt": 1,
  "max_attempts": 3,
  "input": { "prompt": "Create a sales deck", "format": { "category": "slides", ... } },
  "result": null,
  "error": null,
  "credits": null,
  "links": {
    "self": "/v1/tasks/task_01HT9...",
    "events": null,
    "cancel": "/v1/tasks/task_01HT9.../cancel",
    "canvas": null
  },
  "retry_after_ms": 3000
}

Fields

FieldTypeNotes
idstringtask_-prefixed ID
kindstringdesign / export / remix / brand_kit_extract — discriminator for input / result shape
statusstringqueued / running / succeeded / failed / canceled / expired
created_atISO 8601When the task was created
started_atISO 8601 / nullWhen the worker picked it up
completed_atISO 8601 / nullTerminal timestamp
progressobject / null{percent, step, message} — populated during running
attemptintegerCurrent attempt number
max_attemptsintegerUsually 3 — auto-retry cap for transient failures
inputobjectVaries by kind (design: {prompt, format, ...}; export: {canvas_id, format, ...})
resultobject / nullPopulated on succeeded; varies by kind (design: {canvas_id, canvas_url, conversation_id})
errorobject / null{message, retryable} — populated on failed
creditsobject / null{credits_used, credits_remaining} on succeeded
linksobjectHATEOAS-style relations: self, events, cancel, canvas
retry_after_msinteger / nullNon-null while non-terminal; typically 3000 (3s)

Status state machine

queued   → running → succeeded
                   → failed
                   → canceled
                   → expired (rare; long-queued tasks time out)
Terminal: succeeded, failed, canceled, expired. Non-terminal: queued, running. Derive is_terminal = status in {"succeeded","failed","canceled","expired"}. Derive can_export = status == "succeeded" && result && result.canvas_id.

Discriminator — kind

Task shape is polymorphic on kind:
kindinput includesresult includes (on succeeded)
designprompt, format, attachments, brand_kit_id, number_of_slides, …canvas_id, canvas_url, conversation_id
remixcanvas_id, prompt, brand_kit_id, …canvas_id, canvas_url, source_canvas_id
exportcanvas_id, format, …export_url, format, expires_at
brand_kit_extracturlbrand_kit_id
Always check kind before reading result fields. Note: POST /v1/canvases/{id}/export today is synchronous — it returns a plain {url, format} payload, not a Task envelope. The export kind applies to webhook events (export.succeeded / export.failed) and to long-running export flows if/when they exist. See canvases-and-exports.md.

Delivery patterns

Pick one per task — don’t stack: Pass callback_url in the task body. Webhook fires on terminal state only. Requires moda_live_… API key auth (OAuth callers get 400). See webhooks.md.

2. Polling — simplest for short-lived callers

POST /v1/tasks → task envelope with retry_after_ms
loop:
  wait retry_after_ms
  GET /v1/tasks/{id}
  break when status is terminal
Respect retry_after_ms. Don’t poll faster.

3. Prefer: wait=<seconds> — for fast operations only

Add the header on any POST that returns a Task envelope. The server holds the response until terminal OR the wait budget expires.
POST /v1/tasks
Prefer: wait=30
Cap: 30 seconds (server-enforced, per RFC 7240 wait parameter). Valid use cases:
  • Brand-kit creation (POST /v1/brand-kits) — takes 10–30s
  • Remix without a prompt — synchronous on the backend
  • Short design tasks with model_tier: "lite" and a tight scope
Not valid for from-scratch design tasks. They take 2–10 minutes; wait=30 will time out and return the non-terminal envelope. Use webhooks or polling for design. Gateway / proxy warning: behind a 30s-timeout CDN or load balancer, don’t set wait=30 — you’ll hit the gateway timeout before the server times out, and the client sees an abrupt disconnect rather than the graceful envelope return. Use wait=20 or poll.

How Prefer: wait responds

  • If the task reaches terminal inside the budget: returns terminal envelope.
  • If the budget expires first: returns the current (non-terminal) envelope. Not an error.
You still need to branch on status — the wait header doesn’t change the return contract, only the latency.

Timing expectations

OperationTypical time
POST /v1/tasks (design from scratch)2–10 min
POST /v1/tasks with conversation_id (follow-up edit)1–5 min
POST /v1/remix with a prompt2–10 min (it’s a design task under the hood)
POST /v1/remix without a prompt (plain duplicate)< 1s (synchronous, no task)
POST /v1/brand-kits (from URL)10–30s
POST /v1/canvases/{id}/exportseconds (synchronous signed URL)
Set user expectations up front. Don’t block callers on multi-minute operations.

Common wrong guesses

  • Using Prefer: wait=30 on a design task from scratch. It almost always times out. Use webhooks or polling.
  • Polling faster than retry_after_ms. Burns your rate budget for no latency benefit.
  • Reading response.canvas_id on a canonical response. It’s response.result.canvas_id. The flat field is legacy (2026-04-12).
  • Expecting export kind from POST /v1/canvases/{id}/export. That endpoint is synchronous and does not return a Task envelope.
  • Treating expired as a failure. It is terminal (same as the others) but the cause is queue-depth or worker-starvation, not a user-actionable error — retry the task fresh.

Upstream