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