curl --request POST \
--url https://api.moda.app/v1/canvases/{canvas_id}/export \
--header 'Authorization: Bearer <token>'import requests
url = "https://api.moda.app/v1/canvases/{canvas_id}/export"
headers = {"Authorization": "Bearer <token>"}
response = requests.post(url, headers=headers)
print(response.text)const options = {method: 'POST', headers: {Authorization: 'Bearer <token>'}};
fetch('https://api.moda.app/v1/canvases/{canvas_id}/export', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.moda.app/v1/canvases/{canvas_id}/export",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"net/http"
"io"
)
func main() {
url := "https://api.moda.app/v1/canvases/{canvas_id}/export"
req, _ := http.NewRequest("POST", url, nil)
req.Header.Add("Authorization", "Bearer <token>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.moda.app/v1/canvases/{canvas_id}/export")
.header("Authorization", "Bearer <token>")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.moda.app/v1/canvases/{canvas_id}/export")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
response = http.request(request)
puts response.read_body{
"canvas_id": "cvs_01HT9WK8N3M2J4A5Z6P7Q8R9TV",
"canvas_url": "<string>",
"format": "<string>",
"status": "completed",
"url": "<string>",
"task_id": "<string>",
"retry_after_seconds": 123,
"total_pages": 123,
"source": "<string>",
"warnings": [
{
"code": "<string>",
"message": "<string>",
"severity": "warning",
"details": {}
}
]
}{
"type": "invalid_request",
"code": "<string>",
"message": "<string>",
"doc_url": "<string>",
"request_id": "<string>",
"causes": [
"<unknown>"
],
"details": {},
"retry_after_ms": 123,
"retryable": true
}{
"type": "invalid_request",
"code": "<string>",
"message": "<string>",
"doc_url": "<string>",
"request_id": "<string>",
"causes": [
"<unknown>"
],
"details": {},
"retry_after_ms": 123,
"retryable": true
}{
"type": "invalid_request",
"code": "<string>",
"message": "<string>",
"doc_url": "<string>",
"request_id": "<string>",
"causes": [
"<unknown>"
],
"details": {},
"retry_after_ms": 123,
"retryable": true
}{
"type": "invalid_request",
"code": "<string>",
"message": "<string>",
"doc_url": "<string>",
"request_id": "<string>",
"causes": [
"<unknown>"
],
"details": {},
"retry_after_ms": 123,
"retryable": true
}{
"type": "invalid_request",
"code": "<string>",
"message": "<string>",
"doc_url": "<string>",
"request_id": "<string>",
"causes": [
"<unknown>"
],
"details": {},
"retry_after_ms": 123,
"retryable": true
}{
"type": "invalid_request",
"code": "<string>",
"message": "<string>",
"doc_url": "<string>",
"request_id": "<string>",
"causes": [
"<unknown>"
],
"details": {},
"retry_after_ms": 123,
"retryable": true
}{
"type": "invalid_request",
"code": "<string>",
"message": "<string>",
"doc_url": "<string>",
"request_id": "<string>",
"causes": [
"<unknown>"
],
"details": {},
"retry_after_ms": 123,
"retryable": true
}{
"type": "invalid_request",
"code": "<string>",
"message": "<string>",
"doc_url": "<string>",
"request_id": "<string>",
"causes": [
"<unknown>"
],
"details": {},
"retry_after_ms": 123,
"retryable": true
}Export Canvas
Export a canvas as PNG, JPEG, PDF, PPTX, MP4, or GIF. Returns a signed URL or a polling handle.
Exports require DB-level team access regardless of share-link status.
Share-link-only callers can read the canvas via GET /canvases/{id}
with a share_token query param but cannot export.
Large or slow exports (multi-page documents, mp4/gif animation renders)
may exceed the synchronous wait budget; in that case the response carries
status='in_progress' with a task_id — call
GET /canvases/{id}/export-status?task_id=... to retrieve the signed
URL once the background export finishes. Animation exports render one
page’s timeline server-side (mp4 muxes audible video-fill audio;
page-timeline duration, capped per artifact by
exports.animation_policy — see the scope parameter for the
numbers) and reject a page
with nothing to animate with a 422 no_animation error. Pass
scope=sequence (mp4 only) to stitch every visible page’s animation
into one video instead — same caps, applied to the whole stitched video —
or scope=main_edit (mp4 only) to render the canvas’s persisted Main
Edit timeline. Animation formats take an optional fps (mp4 24/30/60,
gif 10/12/15/24; defaults 30/12).
curl --request POST \
--url https://api.moda.app/v1/canvases/{canvas_id}/export \
--header 'Authorization: Bearer <token>'import requests
url = "https://api.moda.app/v1/canvases/{canvas_id}/export"
headers = {"Authorization": "Bearer <token>"}
response = requests.post(url, headers=headers)
print(response.text)const options = {method: 'POST', headers: {Authorization: 'Bearer <token>'}};
fetch('https://api.moda.app/v1/canvases/{canvas_id}/export', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.moda.app/v1/canvases/{canvas_id}/export",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"net/http"
"io"
)
func main() {
url := "https://api.moda.app/v1/canvases/{canvas_id}/export"
req, _ := http.NewRequest("POST", url, nil)
req.Header.Add("Authorization", "Bearer <token>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.moda.app/v1/canvases/{canvas_id}/export")
.header("Authorization", "Bearer <token>")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.moda.app/v1/canvases/{canvas_id}/export")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
response = http.request(request)
puts response.read_body{
"canvas_id": "cvs_01HT9WK8N3M2J4A5Z6P7Q8R9TV",
"canvas_url": "<string>",
"format": "<string>",
"status": "completed",
"url": "<string>",
"task_id": "<string>",
"retry_after_seconds": 123,
"total_pages": 123,
"source": "<string>",
"warnings": [
{
"code": "<string>",
"message": "<string>",
"severity": "warning",
"details": {}
}
]
}{
"type": "invalid_request",
"code": "<string>",
"message": "<string>",
"doc_url": "<string>",
"request_id": "<string>",
"causes": [
"<unknown>"
],
"details": {},
"retry_after_ms": 123,
"retryable": true
}{
"type": "invalid_request",
"code": "<string>",
"message": "<string>",
"doc_url": "<string>",
"request_id": "<string>",
"causes": [
"<unknown>"
],
"details": {},
"retry_after_ms": 123,
"retryable": true
}{
"type": "invalid_request",
"code": "<string>",
"message": "<string>",
"doc_url": "<string>",
"request_id": "<string>",
"causes": [
"<unknown>"
],
"details": {},
"retry_after_ms": 123,
"retryable": true
}{
"type": "invalid_request",
"code": "<string>",
"message": "<string>",
"doc_url": "<string>",
"request_id": "<string>",
"causes": [
"<unknown>"
],
"details": {},
"retry_after_ms": 123,
"retryable": true
}{
"type": "invalid_request",
"code": "<string>",
"message": "<string>",
"doc_url": "<string>",
"request_id": "<string>",
"causes": [
"<unknown>"
],
"details": {},
"retry_after_ms": 123,
"retryable": true
}{
"type": "invalid_request",
"code": "<string>",
"message": "<string>",
"doc_url": "<string>",
"request_id": "<string>",
"causes": [
"<unknown>"
],
"details": {},
"retry_after_ms": 123,
"retryable": true
}{
"type": "invalid_request",
"code": "<string>",
"message": "<string>",
"doc_url": "<string>",
"request_id": "<string>",
"causes": [
"<unknown>"
],
"details": {},
"retry_after_ms": 123,
"retryable": true
}{
"type": "invalid_request",
"code": "<string>",
"message": "<string>",
"doc_url": "<string>",
"request_id": "<string>",
"causes": [
"<unknown>"
],
"details": {},
"retry_after_ms": 123,
"retryable": true
}Authorizations
API key from Settings > Developer > REST API
Headers
Calendar-dated API version pin. New integrations should pin 2026-05-01 to opt into the newest response shapes. For back-compat the server also accepts requests with no header and resolves them to the current default (today: 2026-04-12); that default advances on each sunset date. Any unsupported value returns 400 unsupported_version.
2026-04-12, 2026-05-01 "2026-05-01"
Path Parameters
Prefixed cvs_ wire ID (Crockford base32 body) — the canonical, recommended form. For back-compat, a bare UUID string is also accepted in both path parameters and JSON request bodies (older integrations that stored raw UUIDs keep working), as is the prefix over a UUID body (cvs_00000000-0000-4000-8000-000000000000). All three are permanent, supported inputs; only the canonical form is ever emitted.
^cvs_[0-9A-HJKMNP-TV-Za-hjkmnp-tv-z]{26}$"cvs_01HT9WK8N3M2J4A5Z6P7Q8R9TV"
Query Parameters
Export format: png, jpeg, pdf, pptx, mp4, or gif. mp4/gif render a single page's animation timeline (mp4 muxes audible video-fill audio) and require the page to actually carry animation — a still page is rejected with no_animation. For one stitched video of every page see scope=sequence, and for the canvas's persisted Main Edit timeline see scope=main_edit (both mp4 only).
What the export renders: page (default — the historical single-page behavior, addressed by page_number), sequence (mp4 only — every visible page's animation stitched into ONE video, in canvas order, transitions included; the same lane as the editor's MP4 sequence export), or main_edit (mp4 only — the canvas's persisted Main Edit timeline, the NLE-style edit built in the editor or via the canvas edit API; it names its own pages). The stitched scopes take no page_number; sequence requires a multi-page canvas with animation timelines, main_edit requires a persisted Main Edit (single-page canvases are fine). Both share the per-artifact 18000-frame / 600 s mp4 ceiling with single-page video — the whole stitched video, not each page, must fit. Invalid combinations are rejected typed: gif/stitched and static-format/stitched as 400 invalid_request, a canvas with no animation document (or no persisted Main Edit) as 422 no_animation, an over-budget timeline as 422 animation_budget_exceeded.
1-indexed page number to export. Omit to export all pages — PDF/PPTX bundle every page natively, while multi-page PNG/JPEG are returned as a .zip of per-page files (page-1.png, page-2.png, …) since a single image container can't hold multiple pages. format in the response reflects what was actually delivered (zip in the bundled case). mp4/gif export exactly one page: required on a multi-page canvas, defaulted to 1 on a single-page one (unless scope=sequence, which stitches every page and takes no page_number).
x >= 1Render scale multiplier (1-4). Higher values produce sharper exports at larger file sizes. Supported for PNG, JPEG, PDF, MP4, and GIF exports (mp4/gif default to 1 — page resolution). PPTX ignores this parameter.
1 <= x <= 4PDF only: produce a raster-only PDF with no searchable text, vector elements, or hyperlinks.
MP4 only: encode quality tier. standard (default) is the fastest encode; high and max spend more encode time for a sharper, larger file. Nothing about the render changes.
standard, high, max MP4 only: video codec. h264 (default) plays everywhere; h265 (HEVC, hvc1-tagged) is markedly smaller on detailed footage but needs a modern player. The file is an .mp4 either way.
h264, h265 Animation formats only: capture frame rate. mp4 accepts 24, 30 (default), or 60; gif accepts 10, 12 (default), 15, or 24 — integer rates only (fractional rates like 29.97/59.94 are rejected, never silently rounded). The per-artifact frame ceiling is rate-independent and per-container (mp4 18000 frames / 600 s; gif 9000 frames / 300 s), so a higher fps shortens the longest exportable timeline (e.g. mp4 300 s at 60 fps vs 600 s at 30) — an over-budget request is rejected typed with the live bound.
When True (default), block up to ~20s for the export to finish before returning an in-progress handle. When False, return status='in_progress' with a task_id immediately — poll /canvases/{id}/export-status until terminal.
When False (default), reuse a cached export if the canvas is unchanged since it was rendered — fast, no browser render. Set True to force a fresh render.
Response
Successful Response
Response for POST /canvases/{id}/export.
Carries one of two shapes depending on status:
completed—urlis the signed download URL.in_progress—task_idlets the caller pollGET /canvases/{id}/export-status?task_id=...until terminal.
Prefixed canvas identifier for the canvas that was exported.
^cvs_[0-9A-HJKMNP-TV-Za-hjkmnp-tv-z]{26}$"cvs_01HT9WK8N3M2J4A5Z6P7Q8R9TV"
Full URL to open the source canvas in the Moda editor.
Delivered format — png, jpeg, pdf, pptx, mp4, gif, or zip. A multi-page PNG/JPEG export (page_number omitted, canvas has >1 page) is bundled into a .zip of per-page files, so format is zip even though the request asked for an image format.
completed when url is set, or in_progress when polling is required.
Signed download URL for the exported file (set when status='completed'). Expires in 7 days.
Export task ID set when status='in_progress'; poll the export-status endpoint with it.
Suggested poll interval when status='in_progress'.
Total page count for the source canvas (echoed for client convenience).
How the export was served: cache (existing artifact reused), slice (page extracted from a cached document, no render), or render (freshly rendered). null while status='in_progress'.
Quality caveats about the delivered file — empty when nothing was degraded. Set on status='completed' only (an in-progress response makes no claim yet); a cached or sliced artifact carries the same lane-level caveats as a freshly rendered one.
Show child attributes
Show child attributes