Task envelope
Every task-shaped operation — design, export (when async), remix, brand-kit extraction — returns the canonical Task envelope. PinModa-Version: 2026-05-01 to get this shape.
Full shape
Fields
Status state machine
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:
Always check
kind before reading result fields.
On a succeeded design task, result.export carries the finished design already rendered to a file — {url, format, status, page_count}, exported in the canvas’s category-default format. Read it directly instead of issuing a separate POST /v1/canvases/{id}/export. It is absent when auto-export was disabled (export_on_complete: {enabled: false}) or did not finish within the budget.
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:1. Webhook (callback_url) — recommended for async backends
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
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.
- 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
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.
status — the wait header doesn’t change the return contract, only the latency.
Timing expectations
Set user expectations up front. Don’t block callers on multi-minute operations.
Common wrong guesses
- Using
Prefer: wait=30on 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_idon a canonical response. It’sresponse.result.canvas_id. The flat field is legacy (2026-04-12). - Expecting
exportkind fromPOST /v1/canvases/{id}/export. That endpoint is synchronous and does not return a Task envelope. - Treating
expiredas 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
docs.moda.app/api/tasks/startTaskdocs.moda.app/api/tasks/getTaskdocs.moda.app/api/versioning— for the legacy → canonical migration map