Canva — Design Automation via MCP and the Connect API
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_type preset doc|email|presentation|whiteboard
or custom 40–8000 px, max 25 MP area; or from design_id / brand_template_id),
GET /v1/designs (query, ownership=any|owned|shared, sort_by, limit 1–100,
continuation), GET /v1/designs/{id}, /pages, /export-formats, /dataset
- Assets —
POST /v1/asset-uploads with Content-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/autofills with brand_template_id + data keyed 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, formats pdf, png, jpg (quality 1–100 required),
gif, pptx, mp4 (quality required), 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_url 30 days. Download export bytes at once; hand humans the
edit_url, never a signed export link.
- Read the dataset before autofilling.
GET /v1/brand-templates/{id}/dataset (or
get-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/capabilities before a batch and read
permission_denied as "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
200 on POST /v1/exports means accepted. Poll until
success, and handle failed by reading job.error.code rather 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_verifier and client_secret stay 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.
1---2name: canva3description: Automate Canva from an agent — via the AI Connector MCP server (generate, find, edit, export designs in natural language) or the Connect REST API (OAuth 2.0 + PKCE, brand template autofill, asset uploads, exports, folders, comments). Use when: connecting an agent to Canva, batch-producing social posts or decks from a brand template and a spreadsheet, exporting designs to PDF/PNG/PPTX/MP4, uploading assets into a Canva account, building a Canva integration, or when the user mentions "Canva", "brand template", "autofill", "Connect API", or "mcp.canva.com".4license: Apache-2.05---6
7# Canva — Design Automation via MCP and the Connect API
8
9## Overview
10
11Canva exposes two automation surfaces, and picking the wrong one wastes an afternoon:
12
13| | **AI Connector MCP** | **Connect REST API** |
14|---|---|---|
15| Endpoint | `https://mcp.canva.com/mcp` | `https://api.canva.com/rest/v1/` |
16| Auth | client-managed OAuth (browser once) | OAuth 2.0 + PKCE you implement |
17| Best for | interactive work in a chat/agent session | batch jobs, servers, CI, other people's accounts |
18| Editing | yes — editing transactions | no (create / autofill / export only) |
19
20Use the **MCP connector** with a human in the loop; use the **Connect API** for
21unattended runs, hundred-row fan-outs, or acting for users other than the developer.
22(A third server, the *Dev MCP* at canva.dev, only serves docs for building Canva apps —
23it does not touch designs.)
24
25## Instructions
26
27### Setup A — AI Connector MCP
28
29```bash
30claude mcp add --transport http canva https://mcp.canva.com/mcp
31```
32
33Clients without HTTP transport go through the bridge:
34`npx -y mcp-remote@latest https://mcp.canva.com/mcp`. The first call opens a browser to
35authorize the Canva account; the client stores the token. Verify with `tools/list`.
36
37**Tools** (rate limit per user per minute in parentheses):
38
39| Group | Tools |
40|---|---|
41| 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) |
42| Editing | `start-editing-transaction` (20), `perform-editing-operations` (50), `commit-editing-transaction` (20), `cancel-editing-transaction` (20) |
43| Export / import | `export-design` (20), `import-design-from-url` (20), `resolve-shortlink` (unlimited) |
44| Assets | `upload-asset-from-url` (30), `get-assets` (100) |
45| Folders | `create-folder` (20), `list-folder-items` (100), `search-folders` (100), `move-item-to-folder` (100) |
46| Comments | `comment-on-design` (100), `reply-to-comment` (20), `list-comments` (100), `list-replies` (100) |
47| **Pro+** | `resize-design` (20), `search-brand-templates` (100), `list-brand-kits` (100), `create-design-from-brand-template` (20) |
48| **Enterprise** | `autofill-design` (60), `get-brand-template-dataset` (100) |
49
50**Editing is transactional.** `start-editing-transaction` → `perform-editing-operations`
51(repeatable) → `commit-editing-transaction`. Nothing lands in the design until the commit;
52cancel explicitly rather than dropping an open transaction.
53
54### Setup B — Connect API (OAuth 2.0 + PKCE)
55
56Create an integration at [canva.com/developers](https://www.canva.com/developers/), set
57the redirect URI and scopes. The flow is PKCE-mandatory:
58
59```bash
60# 1. verifier (43–128 chars) + S256 challenge
61VERIFIER=$(openssl rand -base64 96 | tr -d '\n=+/' | cut -c1-96)
62CHALLENGE=$(printf %s "$VERIFIER" | openssl dgst -binary -sha256 | basenc --base64url | tr -d '=')
63
64# 2. send the user here (scopes space-separated, state = CSRF nonce)
65https://www.canva.com/api/oauth/authorize?response_type=code&client_id=$CLIENT_ID\
66&code_challenge=$CHALLENGE&code_challenge_method=S256&state=$NONCE\
67&scope=design:content:write%20design:meta:read%20asset:write
68
69# 3. exchange the code — Basic auth is base64("client_id:client_secret")
70curl -X POST https://api.canva.com/rest/v1/oauth/token \
71 -u "$CLIENT_ID:$CLIENT_SECRET" \
72 -d grant_type=authorization_code -d code="$CODE" -d code_verifier="$VERIFIER"
73```
74
75Access tokens last **4 hours**; refresh with `grant_type=refresh_token`. **Each refresh
76token is single-use** — the response returns a new one, and losing it means re-consent,
77so persist the rotation atomically before using the new access token.
78
79### Scopes — nothing is implied
80
81`asset:read` · `asset:write` · `design:meta:read` · `design:content:read` ·
82`design:content:write` · `brandtemplate:meta:read` · `brandtemplate:content:read` ·
83`brandtemplate:content:write` · `folder:read` · `folder:write` ·
84`folder:permission:write` · `comment:read` · `comment:write` · `collaboration:event` ·
85`profile:read` · `openid` · `profile` · `email`
86
87`asset:write` does **not** grant `asset:read`. Request each one you call.
88
89### The async job pattern — exports, autofills, uploads, imports, resizes, merges
90
91Every heavy Connect operation is a job: the POST returns
92`{"job": {"id", "status": "in_progress"|"success"|"failed"}}`, and the result exists only
93once a GET on the job returns `success`. Never treat the POST response as the result.
94
95| Operation | POST | Poll |
96|---|---|---|
97| Export | `/v1/exports` | `/v1/exports/{exportId}` |
98| Autofill | `/v1/autofills` | `/v1/autofills/{jobId}` |
99| Asset upload | `/v1/asset-uploads` | `/v1/asset-uploads/{jobId}` |
100| URL asset upload | `/v1/url-asset-uploads` | `/v1/url-asset-uploads/{jobId}` |
101| Design import | `/v1/imports` · `/v1/url-imports` | same path + `/{jobId}` |
102| Resize | `/v1/resizes` | `/v1/resizes/{jobId}` |
103
104Poll on a back-off (1s → 2s → 4s, cap ~5s). Tight loops burn the 100 req/min read budget
105long before the job finishes.
106
107### Endpoint map (Connect API)
108
109- **Designs** — `POST /v1/designs` (`design_type` preset `doc|email|presentation|whiteboard`
110 or custom 40–8000 px, max 25 MP area; or from `design_id` / `brand_template_id`),
111 `GET /v1/designs` (`query`, `ownership=any|owned|shared`, `sort_by`, `limit` 1–100,
112 `continuation`), `GET /v1/designs/{id}`, `/pages`, `/export-formats`, `/dataset`
113- **Assets** — `POST /v1/asset-uploads` with `Content-Type: application/octet-stream`, the
114 raw bytes as the body, and the name in a header: `Asset-Upload-Metadata:
115 {"name_base64":"<base64 of a ≤50-char name>"}`. `GET`/`PATCH`/`DELETE /v1/assets/{id}`
116- **Autofill** — `POST /v1/autofills` with `brand_template_id` + `data` keyed by the
117 template's field names
118- **Brand templates** — `GET /v1/brand-templates`, `/{id}`, `/{id}/dataset` (the dataset
119 is the field list autofill expects — read it, never guess field names)
120- **Exports** — `POST /v1/exports`, formats `pdf`, `png`, `jpg` (`quality` 1–100 required),
121 `gif`, `pptx`, `mp4` (`quality` required), `csv`, `html_bundle`, `html_standalone`
122- **Folders** — `POST /v1/folders`, `GET /v1/folders/{id}/items`, `POST /v1/folders/move`
123- **Comments** — threads and replies on a design; **Analytics** — views, viewers, links
124- **User** — `GET /v1/users/me`, `/me/profile`, `/me/capabilities` (check tier here before
125 calling a gated endpoint)
126
127Paginate with `continuation`: pass the token from the previous response until it is absent.
128
129## Examples
130
131### Example 1: 240 localized social posts from one brand template
132
133> "Fill our 'Q3 Promo' brand template with every row in offers.csv and export PNGs."
134
135```bash
136# 1. Read the template's real field names — never guess them
137curl -H "Authorization: Bearer $TOKEN" \
138 https://api.canva.com/rest/v1/brand-templates/DAFxyz123/dataset
139# → { "dataset": { "headline": {"type":"text"}, "price": {"type":"text"},
140# "hero": {"type":"image"} } }
141
142# 2. Product shot → asset (poll the job, capture asset.id)
143curl -X POST https://api.canva.com/rest/v1/asset-uploads \
144 -H "Authorization: Bearer $TOKEN" \
145 -H 'Asset-Upload-Metadata: {"name_base64":"aGVyby1hdXR1bW4ucG5n"}' \
146 -H "Content-Type: application/octet-stream" --data-binary @hero-autumn.png
147
148# 3. One autofill job per row (60/min — throttle the loop)
149curl -X POST https://api.canva.com/rest/v1/autofills \
150 -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
151 -d '{"brand_template_id":"DAFxyz123","title":"Autumn promo — Berlin",
152 "data":{"headline":{"type":"text","text":"30% off through October"},
153 "price":{"type":"text","text":"€24.99"},
154 "hero":{"type":"image","asset_id":"MSGxyz789"}}}'
155# → {"job":{"id":"af_01H...","status":"in_progress"}}
156
157# 4. Poll → success gives job.result.design.id → POST /v1/exports → poll → job.urls[]
158```
159
160Scopes needed: `brandtemplate:meta:read`, `asset:write`, `design:content:write`.
161Autofill is **Enterprise-only** — on a lower tier this returns `permission_denied`, and
162the fallback is `create-design-from-brand-template` (Pro) followed by manual editing.
163
164### Example 2: Same job, one prompt, over MCP
165
166> "Find our Q3 Promo brand template, make a version for the Berlin offer with this
167> product shot, and export it as a PNG."
168
169```text
170search-brand-templates("Q3 Promo") → template id
171get-brand-template-dataset(id) → headline / price / hero (Enterprise)
172upload-asset-from-url("https://cdn.acme.dev/hero-autumn.png") → asset id
173autofill-design(id, { headline, price, hero }) → design id + edit_url
174export-design(design_id, format: "png") → download URL (valid 24 h)
175```
176
177Give the user the `edit_url` — it lasts 30 days and opens in Canva, while the export URL
178dies in 24 hours. If `autofill-design` is missing from `tools/list`, the account is not
179Enterprise: say so instead of retrying.
180
181## Guidelines
182
183- **Every URL expires, at a different rate.** Thumbnails ~15 min, export downloads 24 h,
184 `edit_url` / `view_url` 30 days. Download export bytes at once; hand humans the
185 `edit_url`, never a signed export link.
186- **Read the dataset before autofilling.** `GET /v1/brand-templates/{id}/dataset` (or
187 `get-brand-template-dataset`) returns the exact field names and types. A guessed key is
188 silently ignored, producing a design with empty placeholders rather than an error.
189- **Tier gates are hard.** Resize and brand templates need Pro; autofill and brand kits
190 need Enterprise. Check `GET /v1/users/me/capabilities` before a batch and read
191 `permission_denied` as "wrong plan", not "wrong request".
192- **Limits are per user, per minute:** creates and exports 20, autofill 60, reads 100.
193 A 240-row batch is a throttled queue, not a `Promise.all`. Back off on 429.
194- **Jobs are not results.** A `200` on `POST /v1/exports` means *accepted*. Poll until
195 `success`, and handle `failed` by reading `job.error.code` rather than re-submitting.
196- **Blank designs vanish.** A design created via the API and never edited is deleted after
197 7 days. Autofill or edit it in the same run if it needs to persist.
198- **Refresh tokens are single-use.** Store the rotated token before the next call; a lost
199 rotation means the user must re-consent through the full PKCE flow.
200- **PKCE is mandatory**; the `code_verifier` and `client_secret` stay server-side. A Canva
201 integration cannot be driven safely from client-side code.
202- Design ids (`DAF…`), asset ids (`MSG…`) and job ids are scoped to the authorized
203 account; a 404 on a valid-looking id usually means it belongs to a different team.