Skip to main content

Export pipeline

Problem: Archive every canvas on the team as a PDF. Upload each to S3 / Google Drive. Skip canvases that have an in-flight design task.

Primitives

  • GET /v1/canvases — cursor-paginate all canvases
  • POST /v1/canvases/{id}/export?format=pdf — returns a signed URL, or a task_id to poll
  • Handle 409 canvas_active_job — back off and retry
  • Upload the file bytes to your storage of choice

TypeScript (Node 20+)

Python (httpx)

Gotchas

  • Export has two response shapes. A cache hit or a quick render returns {status: "completed", url}; anything slower than the ~20s wait budget returns {status: "in_progress", task_id} and you poll /canvases/{id}/export-status until is_terminal. Full-team PDF archives hit the slow path routinely, so branch on status — reading url blindly gets you null.
  • A failed poll is not a failed export. A 429 or 5xx on /export-status says nothing about the render — retry the poll (or re-POST the export, which is cache-first) instead of treating it as a terminal failure.
  • A failed export is not always final. The poll response’s retryable says whether a fresh attempt can succeed; retryable: false (e.g. error_code: "no_renderable_content") means the canvas itself has to change first. Retrying those burns rate budget for nothing.
  • Signed URLs expire after 7 days. Download and re-upload immediately — don’t persist the signed URL itself.
  • 409 canvas_active_job is the retry signal when a design task is running on the canvas. Respect Retry-After (default 10s).
  • 429 rate_limited applies per-endpoint — exports have their own cap (25/min by default, raisable per-org; see Usage Limits). Respect Retry-After.
  • Cursor pagination is sequential. Can’t parallelize page fetches. Parallelize the per-canvas work within a page instead (cap to ~4–8 concurrent exports).
  • Scope requirements: canvases:read for listing, designs:export for the export endpoint. Team membership required — a share-token-only caller can’t export.
  • Don’t export the same canvas twice in a row. If you re-run this job often, compare updated_at against your archive and skip unchanged canvases.
If you only want canvases matching a pattern:
/canvases/search is the one offset-paginated endpoint: items are under canvases (not data), and you page with offset + limit while has_more is true.

See also