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.

The Moda REST API uses a calendar-dated version string carried on the Moda-Version request header. Pinning a version guarantees the wire shape you built against stays stable even when we ship a newer version.

Supported versions

VersionRoleStatusSunsets
2026-05-01Canonical (newest)Latest response shape
2026-04-12Default (legacy)Current default for unpinned traffic2026-05-12
  • Canonical means routes emit this shape natively. Pin this to get the newest response fields.
  • Default means unpinned requests (no Moda-Version header) resolve to this version. The default advances to the next-newest supported version once the current default sunsets.
  • Sunset means the version stops being supported on the listed date. After that date, pinning the sunset version returns 400 unsupported_version, and the default advances to the next-newest.

The Moda-Version header

Sending the header

Pin explicitly on every request:
curl https://api.moda.app/v1/tasks \
  -H "Authorization: Bearer moda_live_abc123..." \
  -H "Moda-Version: 2026-05-01"

Omitting the header

If you omit Moda-Version, the server resolves your request to the current default (2026-04-12). Unpinned integrations keep working across version bumps until a sunset advances the default. For production, pin explicitly so future default advancement doesn’t silently change your response shapes.

Unknown versions

Any value other than a currently-supported version returns 400 unsupported_version:
{
  "error": {
    "type": "invalid_request",
    "code": "unsupported_version",
    "message": "Moda-Version '2026-01-01' is not supported. Supported versions: 2026-04-12, 2026-05-01.",
    "details": {
      "requested": "2026-01-01",
      "supported": ["2026-04-12", "2026-05-01"]
    },
    "request_id": "019d8996-16b3-73ee-841a-5bc5038eb972"
  }
}
The response’s Moda-Version header still carries the current default, so clients can detect the drift and upgrade.

Response header

Every response — success or error — carries the resolved Moda-Version back:
HTTP/1.1 200 OK
Moda-Version: 2026-04-12
Content-Type: application/json
...
If you omit the header and want to record which version the server resolved you to, read it off the response.

What changes between versions

The 2026-05-01 canonical shape is a cleaner envelope for all task-shaped operations (POST /v1/tasks, GET /v1/tasks/{id}, GET /v1/tasks). The 2026-04-12 legacy shape is the original flat JobResponse form.

Task endpoints

2026-04-12 (legacy flat shape)
{
  "job_id": "task_01HT9WK8N3M2J4A5Z6P7Q8R9TV",
  "status": "completed",
  "canvas_id": "cvs_77DX73QNEE9QPTBRB654CN1SBV",
  "canvas_url": "https://app.moda.com/canvas/...",
  "conversation_id": null,
  "message": "Job completed successfully.",
  "task": "Create a sales deck",
  "progress_percent": 100,
  "current_step": null,
  "error": null,
  "created_at": "2026-04-15T12:00:00",
  "started_at": "2026-04-15T12:00:02",
  "completed_at": "2026-04-15T12:01:00",
  "is_terminal": true,
  "can_export": true,
  "retry_after_seconds": null,
  "credits": { "credits_used": 5, "credits_remaining": 12 }
}
2026-05-01 (canonical Task envelope)
{
  "id": "task_01HT9WK8N3M2J4A5Z6P7Q8R9TV",
  "kind": "design",
  "status": "succeeded",
  "created_at": "2026-04-15T12:00:00",
  "started_at": "2026-04-15T12:00:02",
  "completed_at": "2026-04-15T12:01:00",
  "progress": null,
  "attempt": 1,
  "max_attempts": 3,
  "input": { "prompt": "Create a sales deck" },
  "result": {
    "canvas_id": "cvs_77DX73QNEE9QPTBRB654CN1SBV",
    "canvas_url": "https://app.moda.com/canvas/..."
  },
  "error": null,
  "credits": { "credits_used": 5, "credits_remaining": 12 },
  "links": {
    "self": "/v1/tasks/task_01HT9WK8N3M2J4A5Z6P7Q8R9TV",
    "events": null,
    "cancel": null,
    "canvas": "https://app.moda.com/canvas/..."
  },
  "retry_after_ms": null
}

Quick migration map

Old (2026-04-12)New (2026-05-01)
response.job_idresponse.id
response.status == "completed"response.status == "succeeded"
response.canvas_idresponse.result.canvas_id
response.canvas_urlresponse.result.canvas_url
response.conversation_idresponse.result.conversation_id
response.taskresponse.input.prompt
response.progress_percentresponse.progress.percent
response.current_stepresponse.progress.step
response.error (string)response.error.message (object)
response.is_terminalderived: status in ("succeeded","failed","canceled","expired")
response.can_exportderived: status == "succeeded" && result.canvas_id
response.retry_after_secondsresponse.retry_after_ms / 1000
Per-operation examples are on each endpoint’s reference page.

List endpoints: offset → cursor pagination

Every list endpoint moved from offset pagination to opaque cursor pagination. Iterate until next_cursor is null: Before (2026-04-12):
{
  "canvases": [ ... ],
  "total": 123,
  "offset": 20,
  "limit": 20,
  "has_more": true
}
After (2026-05-01):
{
  "data": [ ... ],
  "next_cursor": "eyJ2IjoxLCJzIjoiMjAyNi0wNC0xNVQxMjow..."
}
Migration pattern:
cursor = None
while True:
    resp = client.get("/v1/canvases", headers={"Moda-Version": "2026-05-01"}, params={"cursor": cursor}).json()
    for canvas in resp["data"]:
        process(canvas)
    cursor = resp["next_cursor"]
    if cursor is None:
        break
Old (2026-04-12)New (2026-05-01)
response.canvases / brand_kits / organizations / jobsresponse.data
response.totalno longer computed — iterate until next_cursor null
response.offset + response.limitopaque cursor query param (server-managed position)
response.has_moreresponse.next_cursor !== null
?offset=20&limit=20?cursor=<opaque>&limit=20
Sort order changed from updated_at DESC (canvases) / name ASC (orgs) to (created_at DESC, id DESC) across every list endpoint — immutable columns only, which avoids row skip / duplicate bugs when rows mutate mid-scan. GET /v1/brand-kits also dropped the team_id field from the canonical response — it’s implicit in the API key context.

Error responses

Every endpoint now explicitly declares ErrorEnvelope responses for 401 / 403 / 404 / 409 / 422 / 429 / 500 in the OpenAPI spec. The wire shape is unchanged — this is a spec-hardening change for SDK generators to emit typed error classes. No migration action required for direct HTTP callers.

Webhook event taxonomy

Webhook event.type values moved from the legacy job.* prefix to task.* / export.*. Legacy spelling task.cancelled (two Ls) is retired — canonical uses task.canceled (matching the PublicTaskStatus.CANCELED enum value). Non-terminal states (queued, running, expired) no longer fire webhooks — the v1 taxonomy is terminal-only.
Legacy eventCanonical event
job.completedtask.succeeded
job.failedtask.failed
job.cancelledtask.canceled
job.dead_lettertask.failed (plus data.error.retryable: false)
job.runningremoved (poll /v1/tasks/{id} for progress)
job.queuedremoved
See Webhooks for the full event envelope.

What’s covered by a version bump

New versions only ship for breaking response-shape changes. Additive changes — new endpoints, new optional request parameters, new response fields, new error codes within an existing error type — land at the current canonical version without a bump. Your client should tolerate unknown fields in responses so these additions don’t surprise it.

Breaking changes (new version ships)

  • Removing or renaming a response field
  • Removing an endpoint, path, or parameter
  • Changing a field’s type or shape
  • Tightening validation (new 400s for inputs that used to succeed)
  • Changing status vocabulary (e.g. completedsucceeded)
  • Changing default behavior of an existing parameter

Additive changes (no version bump)

  • New endpoint, path, or optional parameter
  • New response field (existing fields unchanged)
  • New optional request header
  • New code within an existing type on error responses
  • Loosened validation (fewer 400s)

Version bump policy

We ship additive version bumps. When a new canonical version lands:
  1. The new version becomes the canonical (newest) — routes emit it natively.
  2. The previous default stays supported for a sunset window (typically 2–3 weeks for small shape changes; longer for substantial ones).
  3. On the sunset date, the older version retires (pinning it returns 400), and the default advances to whichever supported version is next-oldest.
Practical upshot: if you pin explicitly, you upgrade on your schedule. If you stay unpinned, you’ll track the current default and will see a shape change whenever the default advances — which we announce here with a calendar date well in advance.

Recommendations

  • Pin Moda-Version explicitly on every request in production. Don’t rely on the default — the default advances on sunset dates.
  • Log the response’s Moda-Version header so you can tell at a glance which version your traffic is actually hitting.
  • When you see 400 unsupported_version, read the supported list out of the error details — that’s the ground truth and updates automatically.
  • Subscribe to the changelog to hear about new versions and sunset dates as they’re announced.