Overview
Canva exposes two automation surfaces, and picking the wrong one wastes an afternoon:
| AI Connector MCP | Connect REST API | |
|---|---|---|
| Endpoint | https://mcp.canva.com/mcp | https://api.canva.com/rest/v1/ |
| Auth | client-managed OAuth (browser once) | OAuth 2.0 + PKCE you implement |
| Best for | interactive work in a chat/agent session | batch jobs, servers, CI, other people's accounts |
| Editing | yes — editing transactions | no (create / autofill / export only) |
Use the MCP connector with a human in the loop; use the Connect API for unattended runs, hundred-row fan-outs, or acting for users other than the developer. (A third server, the Dev MCP at canva.dev, only serves docs for building Canva apps — it does not touch designs.)
Instructions
Setup A — AI Connector MCP
claude mcp add --transport http canva https://mcp.canva.com/mcp
Clients without HTTP transport go through the bridge:
npx -y mcp-remote@latest https://mcp.canva.com/mcp. The first call opens a browser to
authorize the Canva account; the client stores the token. Verify with tools/list.
Tools (rate limit per user per minute in parentheses):
| Group | Tools |
|---|---|
| Designs | generate-design (20), create-design-from-candidate (20), copy-design (20), search-designs (100), get-design (100), get-design-pages (100), get-design-content (100), get-presenter-notes (100), get-export-formats (100), get-design-thumbnail (100) |
| Editing | start-editing-transaction (20), perform-editing-operations (50), commit-editing-transaction (20), cancel-editing-transaction (20) |
| Export / import | export-design (20), import-design-from-url (20), resolve-shortlink (unlimited) |
| Assets | upload-asset-from-url (30), get-assets (100) |
| Folders | create-folder (20), list-folder-items (100), search-folders (100), move-item-to-folder (100) |
| Comments | comment-on-design (100), reply-to-comment (20), list-comments (100), list-replies (100) |
| Pro+ | resize-design (20), search-brand-templates (100), list-brand-kits (100), create-design-from-brand-template (20) |
| Enterprise | autofill-design (60), get-brand-template-dataset (100) |
Editing is transactional. start-editing-transaction → perform-editing-operations
(repeatable) → commit-editing-transaction. Nothing lands in the design until the commit;
cancel explicitly rather than dropping an open transaction.
Setup B — Connect API (OAuth 2.0 + PKCE)
Create an integration at canva.com/developers, set the redirect URI and scopes. The flow is PKCE-mandatory:
# 1. verifier (43–128 chars) + S256 challenge
VERIFIER=$(openssl rand -base64 96 | tr -d '\n=+/' | cut -c1-96)
CHALLENGE=$(printf %s "$VERIFIER" | openssl dgst -binary -sha256 | basenc --base64url | tr -d '=')
# 2. send the user here (scopes space-separated, state = CSRF nonce)
https://www.canva.com/api/oauth/authorize?response_type=code&client_id=$CLIENT_ID\
&code_challenge=$CHALLENGE&code_challenge_method=S256&state=$NONCE\
&scope=design:content:write%20design:meta:read%20asset:write
# 3. exchange the code — Basic auth is base64("client_id:client_secret")
curl -X POST https://api.canva.com/rest/v1/oauth/token \
-u "$CLIENT_ID:$CLIENT_SECRET" \
-d grant_type=authorization_code -d code="$CODE" -d code_verifier="$VERIFIER"
Access tokens last 4 hours; refresh with grant_type=refresh_token. Each refresh
token is single-use — the response returns a new one, and losing it means re-consent,
so persist the rotation atomically before using the new access token.
Scopes — nothing is implied
asset:read · asset:write · design:meta:read · design:content:read ·
design:content:write · brandtemplate:meta:read · brandtemplate:content:read ·
brandtemplate:content:write · folder:read · folder:write ·
folder:permission:write · comment:read · comment:write · collaboration:event ·
profile:read · openid · profile · email
asset:write does not grant asset:read. Request each one you call.
The async job pattern — exports, autofills, uploads, imports, resizes, merges
Every heavy Connect operation is a job: the POST returns
{"job": {"id", "status": "in_progress"|"success"|"failed"}}, and the result exists only
once a GET on the job returns success. Never treat the POST response as the result.
| Operation | POST | Poll |
|---|---|---|
| Export | /v1/exports | /v1/exports/{exportId} |
| Autofill | /v1/autofills | /v1/autofills/{jobId} |
| Asset upload | /v1/asset-uploads | /v1/asset-uploads/{jobId} |
| URL asset upload | /v1/url-asset-uploads | /v1/url-asset-uploads/{jobId} |
| Design import | /v1/imports · /v1/url-imports | same path + /{jobId} |
| Resize | /v1/resizes | /v1/resizes/{jobId} |
Poll on a back-off (1s → 2s → 4s, cap ~5s). Tight loops burn the 100 req/min read budget long before the job finishes.
Endpoint map (Connect API)
- Designs —
POST /v1/designs(design_typepresetdoc|email|presentation|whiteboardor custom 40–8000 px, max 25 MP area; or fromdesign_id/brand_template_id),GET /v1/designs(query,ownership=any|owned|shared,sort_by,limit1–100,continuation),GET /v1/designs/{id},/pages,/export-formats,/dataset - Assets —
POST /v1/asset-uploadswithContent-Type: application/octet-stream, the raw bytes as the body, and the name in a header:Asset-Upload-Metadata: {"name_base64":"<base64 of a ≤50-char name>"}.GET/PATCH/DELETE /v1/assets/{id} - Autofill —
POST /v1/autofillswithbrand_template_id+datakeyed by the template's field names - Brand templates —
GET /v1/brand-templates,/{id},/{id}/dataset(the dataset is the field list autofill expects — read it, never guess field names) - Exports —
POST /v1/exports, formatspdf,png,jpg(quality1–100 required),gif,pptx,mp4(qualityrequired),csv,html_bundle,html_standalone - Folders —
POST /v1/folders,GET /v1/folders/{id}/items,POST /v1/folders/move - Comments — threads and replies on a design; Analytics — views, viewers, links
- User —
GET /v1/users/me,/me/profile,/me/capabilities(check tier here before calling a gated endpoint)
Paginate with continuation: pass the token from the previous response until it is absent.
Examples
Example 1: 240 localized social posts from one brand template
"Fill our 'Q3 Promo' brand template with every row in offers.csv and export PNGs."
# 1. Read the template's real field names — never guess them
curl -H "Authorization: Bearer $TOKEN" \
https://api.canva.com/rest/v1/brand-templates/DAFxyz123/dataset
# → { "dataset": { "headline": {"type":"text"}, "price": {"type":"text"},
# "hero": {"type":"image"} } }
# 2. Product shot → asset (poll the job, capture asset.id)
curl -X POST https://api.canva.com/rest/v1/asset-uploads \
-H "Authorization: Bearer $TOKEN" \
-H 'Asset-Upload-Metadata: {"name_base64":"aGVyby1hdXR1bW4ucG5n"}' \
-H "Content-Type: application/octet-stream" --data-binary @hero-autumn.png
# 3. One autofill job per row (60/min — throttle the loop)
curl -X POST https://api.canva.com/rest/v1/autofills \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"brand_template_id":"DAFxyz123","title":"Autumn promo — Berlin",
"data":{"headline":{"type":"text","text":"30% off through October"},
"price":{"type":"text","text":"€24.99"},
"hero":{"type":"image","asset_id":"MSGxyz789"}}}'
# → {"job":{"id":"af_01H...","status":"in_progress"}}
# 4. Poll → success gives job.result.design.id → POST /v1/exports → poll → job.urls[]
Scopes needed: brandtemplate:meta:read, asset:write, design:content:write.
Autofill is Enterprise-only — on a lower tier this returns permission_denied, and
the fallback is create-design-from-brand-template (Pro) followed by manual editing.
Example 2: Same job, one prompt, over MCP
"Find our Q3 Promo brand template, make a version for the Berlin offer with this product shot, and export it as a PNG."
search-brand-templates("Q3 Promo") → template id
get-brand-template-dataset(id) → headline / price / hero (Enterprise)
upload-asset-from-url("https://cdn.acme.dev/hero-autumn.png") → asset id
autofill-design(id, { headline, price, hero }) → design id + edit_url
export-design(design_id, format: "png") → download URL (valid 24 h)
Give the user the edit_url — it lasts 30 days and opens in Canva, while the export URL
dies in 24 hours. If autofill-design is missing from tools/list, the account is not
Enterprise: say so instead of retrying.
Guidelines
- Every URL expires, at a different rate. Thumbnails ~15 min, export downloads 24 h,
edit_url/view_url30 days. Download export bytes at once; hand humans theedit_url, never a signed export link. - Read the dataset before autofilling.
GET /v1/brand-templates/{id}/dataset(orget-brand-template-dataset) returns the exact field names and types. A guessed key is silently ignored, producing a design with empty placeholders rather than an error. - Tier gates are hard. Resize and brand templates need Pro; autofill and brand kits
need Enterprise. Check
GET /v1/users/me/capabilitiesbefore a batch and readpermission_deniedas "wrong plan", not "wrong request". - Limits are per user, per minute: creates and exports 20, autofill 60, reads 100.
A 240-row batch is a throttled queue, not a
Promise.all. Back off on 429. - Jobs are not results. A
200onPOST /v1/exportsmeans accepted. Poll untilsuccess, and handlefailedby readingjob.error.coderather than re-submitting. - Blank designs vanish. A design created via the API and never edited is deleted after 7 days. Autofill or edit it in the same run if it needs to persist.
- Refresh tokens are single-use. Store the rotated token before the next call; a lost rotation means the user must re-consent through the full PKCE flow.
- PKCE is mandatory; the
code_verifierandclient_secretstay server-side. A Canva integration cannot be driven safely from client-side code. - Design ids (
DAF…), asset ids (MSG…) and job ids are scoped to the authorized account; a 404 on a valid-looking id usually means it belongs to a different team.