Skip to main content

Canvases and exports

All canvas-shaped read + export flows live under /v1/canvases and /v1/remix. Plus the share-token read pattern for public canvases and the /v1/share_links/resolve helper.

Endpoints

GET /canvases is cursor-paginated ({data, next_cursor}), sorted (created_at DESC, id DESC). GET /canvases/search is the one offset-paginated lane in this catalog: it takes q / limit / offset and returns {canvases, returned, limit, offset, has_more}. See pagination.md. List items include id, name, url, category, visibility, created_at, updated_at.

Reading a canvas

Returns the canvas as semantic pseudo-HTML with CSS properties + embedded design tokens. Same format as MCP’s get_moda_canvas. Requires team access.

Reading a public share

Share-token-authenticated callers can read the canvas (view / view_remix permission) without team access. Use in combination with POST /v1/share_links/resolve to parse a share URL first:
Share-link-only callers can read but cannot export (see below).

Tokens only

Returns {variables, colors, fonts, radii, dimensions} — fast path for theme regeneration in CI.

Pages metadata

Returns {canvas_name, total_pages, pages: [{page_number, id, name, width, height, node_count}], id_note?}. Call before GET /v1/canvases/{id}?page_number=N on multi-page canvases to plan per-page fetches. Which identifier to use depends on the endpoint. The design read (GET /v1/canvases/{id}?page_number=N) and the export take page_number; the authoring and capture endpoints take id. Prefer id where it is accepted — an ordinal renumbers when a page is deleted or reordered, so it names a different page over time. id is either a durable real page id (page-1786730909610-172273165) or a session-scoped short ref (p_a); id_note is returned only in the latter case and states the lifetime, so re-list rather than storing one. A page-less canvas lists one entry with no id.

Updating a canvas

Partial update — only send the fields you want to change. description and template_type both treat an explicit null as “clear the field.” template_type is template (any canvas) or theme (slides canvases only). Returns the updated canvas record.

Export — two response shapes

Query parameters: The response carries one of two shapes, discriminated by status:
Branch on status. A cache hit or a quick render finishes inside the ~20s wait budget and returns completed. A slow one (long documents, mp4 / gif animation renders) returns in_progress — the work continues in the background; poll for the URL:
Poll until is_terminal is true; status is then completed (with url) or failed. A failed task carries error, sometimes a typed error_code, and — decisively — retryable: false means a fresh export of the same canvas cannot succeed either (e.g. no_renderable_content, native_export_declined — fix the content first), true means a transient fault worth one more attempt. Branch on retryable rather than treating every failure as final. wait=false skips the blocking wait and hands back the in_progress handle immediately. format in either shape reports what was actually delivered — zip for a bundled multi-page image export, even though the request asked for png / jpeg. The URL expires after 7 days. Download promptly.

Caching

Exports are cache-first. A repeat export of an unchanged canvas — same format, page_number, pixel_ratio, flatten — returns the previously rendered artifact with no new browser render. The source field reports how the response was served:
  • render — freshly rendered.
  • cache — a previously rendered artifact was reused.
  • slice — a single page extracted server-side from a cached full-document export (no render).
The cache invalidates automatically when the canvas changes. Pass force_refresh=true only to skip the cache deliberately (e.g. you changed something the cache key doesn’t capture) — the default path is faster and cheaper. Scope: designs:export and team membership. Share-token-only callers (no team access) cannot export — reads via share_token are not enough.

Export when a design task is running

If the target canvas has an in-flight design task, export returns:
Back off for Retry-After seconds and retry. But in a task → export pipeline you often don’t need a separate export call at all: a programmatic design task auto-exports its result when it finishes, and the completion webhook payload carries it at data.result.export ({status, url, format, page_count}). Read that instead of polling, retrying the 409, or issuing your own export. The auto-export uses the canvas’s category-default format — pass export_on_complete to POST /v1/tasks to override the format or disable it.

An export is not a Task

Unlike design / remix / brand-kit extraction, an export never returns a Task envelope — even the async shape. Its task_id belongs to the export lane, so poll GET /v1/canvases/{id}/export-status?task_id=…, never GET /v1/tasks/{id}. The export task kind and the export.* webhook event types are declared in the public taxonomy but nothing emits them today, so don’t wait on a webhook for an export either.

Sharing — blocking thumbnail default

Returns the share URL + share token. By default, this call blocks until a thumbnail is generated so the URL unfurls properly on social media / Slack. Thumbnail generation usually takes a few seconds; pass wait_for_thumbnail: false to skip if you don’t care about unfurls.

Remix

Returns a Task envelope (kind: "remix"). Without a prompt: the task is synchronous, returns status: "succeeded" inline, result.canvas_id is the new canvas. With a prompt: queues a design task on the copy, returns non-terminal; poll same as /tasks/{id}. The source canvas is never modified.

Design-to-code walk

For visual reference on complex layouts, pair with POST /v1/canvases/{id}/export?format=png.

Common wrong guesses

  • Expecting POST /v1/canvases/{id}/export to return a Task envelope. It returns its own two-shape payload — {status: "completed", url, format, source} or {status: "in_progress", task_id, retry_after_seconds}.
  • Reading url without checking status. url is null on the in_progress shape. Poll /export-status with the task_id.
  • Assuming page_number defaults to page 1. Omitting it exports every page; multi-page PNG/JPEG arrive as a .zip (format: "zip").
  • Passing force_refresh=true on every export. The default is cache-first — a repeat export of an unchanged canvas is served from cache with no render. Only force a refresh when you deliberately need to bypass the cache.
  • Issuing a separate export after a design task. A programmatic task auto-exports; the completion webhook carries it at data.result.export. Re-exporting just hits the cache, but reading the webhook field is one fewer call.
  • Treating the 409 canvas_active_job response as a bug. It’s the intended retry signal when chaining task → export.
  • Ignoring Retry-After on the 409 response. Back off the suggested seconds; don’t hammer.
  • Using share_token= to export. Only reads are permitted via share token. Export requires team access.
  • Skipping wait_for_thumbnail: false on /share calls in a script. By default it blocks — in a batch share-link generator, the latency adds up. Pass false if you don’t care about unfurls.
  • Treating GET /v1/canvases/{id} without page_number as cheap on multi-page designs. It returns all pages concatenated.
  • Holding an export URL longer than 7 days. Expires. Re-export if needed.
  • Storing the X-Request-ID header from a successful export to use as an idempotency key. That’s what the body-field idempotency_key on POST /v1/tasks is for — exports don’t need it (stateless + fast).

Upstream