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
| Version | Role | Status | Sunsets |
|---|
2026-05-01 | Canonical (newest) | Latest response shape | — |
2026-04-12 | Default (legacy) | Current default for unpinned traffic | 2026-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.
Pin explicitly on every request:
curl https://api.moda.app/v1/tasks \
-H "Authorization: Bearer moda_live_abc123..." \
-H "Moda-Version: 2026-05-01"
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.
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_id | response.id |
response.status == "completed" | response.status == "succeeded" |
response.canvas_id | response.result.canvas_id |
response.canvas_url | response.result.canvas_url |
response.conversation_id | response.result.conversation_id |
response.task | response.input.prompt |
response.progress_percent | response.progress.percent |
response.current_step | response.progress.step |
response.error (string) | response.error.message (object) |
response.is_terminal | derived: status in ("succeeded","failed","canceled","expired") |
response.can_export | derived: status == "succeeded" && result.canvas_id |
response.retry_after_seconds | response.retry_after_ms / 1000 |
Per-operation examples are on each endpoint’s reference page.
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 / jobs | response.data |
response.total | no longer computed — iterate until next_cursor null |
response.offset + response.limit | opaque cursor query param (server-managed position) |
response.has_more | response.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 event | Canonical event |
|---|
job.completed | task.succeeded |
job.failed | task.failed |
job.cancelled | task.canceled |
job.dead_letter | task.failed (plus data.error.retryable: false) |
job.running | removed (poll /v1/tasks/{id} for progress) |
job.queued | removed |
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.
completed → succeeded)
- 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:
- The new version becomes the canonical (newest) — routes emit it natively.
- The previous default stays supported for a sunset window (typically 2–3 weeks for small shape changes; longer for substantial ones).
- 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.