Canva Connect API and n8n: automated designs
One template, a thousand variants. You prepare a design in Canva, and n8n fills it with text and images, waits for Canva to build it, exports it as PNG and saves it. Here we walk the whole path: signing in with OAuth 2.0, templates, requests, limits and the honest question of “which plan do I need”.
POST /autofills with brand_template_id in the body (not /designs/{id}/autofill); export is POST /exports with design_id in the body; uploading a file is POST /asset-uploads; there is no OPTIONS request for fields — there is GET /brand-templates/{id}/dataset. Sign-in: Canva requires OAuth 2.0 with PKCE and the authorization URL https://www.canva.com/api/oauth/authorize — the old /oauth/authorize address was wrong; localhost in the redirect was replaced by the 127.0.0.1 that Canva's examples use. Scopes: export:read does not exist; you need design:content:read for export and brandtemplate:* for templates. Plans: “Teams or Pro only” was inaccurate — see “Before you start”. Limits: “~60 requests per minute” is not a global limit; every endpoint has its own (export — 20 per minute per user). Added: polling with backoff, image upload, Canva MCP and n8n notes. Security: the old code had Bearer YOUR_TOKEN on the command line — now tokens live only in an n8n credential.
01What you'll learn
- How the “template → autofill → export → file” path is built and which requests make it up.
- How Canva lets you in: an integration in the Developer Portal, scopes, OAuth 2.0 with PKCE and tokens that get renewed.
- How to wire this into n8n without writing your own sign-in server.
- How to prepare a brand template with named fields and how to upload an image so it can go into it.
- How to wait for an asynchronous job with polling and backoff, and how to plan bulk generation so the limits do not stop you.
- Which plan you need for what — and what to do if you do not have one (Canva MCP or HTML → PNG).
02Before you start
- A working n8n 🔒 local from Lesson 3 and access to its interface.
- A Canva 🌐 global account with multi-factor authentication (MFA) turned on — the autofill guide requires it for the account you develop with.
- Access to Canva's Developer Portal, where you will create your app.
- A clear answer to the plan question — the table below.
| What you want | What Canva's documentation says |
|---|---|
| List, read and export designs, upload files | Works with a free account too. A paid plan is needed only for premium parts: export_quality: "pro", PNG with a transparent background and lossy PNG. A free account can upscale an export by at most 1.125 times. |
| Brand templates and autofill | The API reference and the autofill guide now say the same thing: you need a plan with autofill — Canva Pro (including Canva Education and Canva for Nonprofits), Canva Teams or Canva Enterprise. Canva warns that usage limits will be introduced in the future. Check with your own account using the first request to /brand-templates. |
| A private app only for your team | Private apps are available only to teams on Canva Enterprise. A public app goes through a Canva review. |
| Canva through an AI assistant (MCP) | The core — search, create, export, comments — is for all plans. The autofill and brand-template tools are “Pro and above”. |
/brand-templates with your token. If you get a 403 with text about a missing capability, your plan does not include it — then use the fallback at the end of the lesson.03Steps
-
The map of the path
Canva Connect API is a REST interface. Everything goes through the base address
https://api.canva.com/rest/v1, and every request carriesAuthorization: Bearer <token>— a token issued on behalf of a specific Canva user. Why this matters: your automation has no “own” access; it acts as the person who allowed it — with their plan and their folders.The two long operations — autofill and export — are asynchronous jobs: you send a request, get a
job.idwith statusin_progress, and ask a second endpoint until it becomessuccessorfailed.Method and path What it is for Limit per user GET /designsList designs (up to 100 per page, continuationfor the next)100 / min GET /brand-templatesList the brand templates you can access 100 / min GET /brand-templates/{id}/datasetNames and types of the fillable fields 100 / min POST /asset-uploads·GET /asset-uploads/{jobId}Upload an image, then check the job 30 / min · 180 / min POST /autofills·GET /autofills/{jobId}Fill a template → new design; check the job 60 / min · 120 / min POST /exports·GET /exports/{exportId}Export a design (PNG, JPG, PDF, PPTX, GIF, MP4…); check and get download URLs 20 / min · 120 / min POST /foldersCreate a folder 20 / min the path in one linebrand template → POST /autofills → GET /autofills/{id} → design.id → POST /exports → GET /exports/{id} → urls[0] → file -
Create an app in the Developer Portal
In Your apps choose Create an app: a name up to 18 characters; for “who can use it” choose Private only if you are on an Enterprise team — otherwise Public (this cannot be changed later). Then: Outside Canva → Start integrating, in Configuration check that Canva REST APIs is on, click Generate secret and store the secret safely — it is shown only once.
Turn on scopes (permissions) on the same screen. Why keep them minimal: if a token leaks, the damage equals its permissions.
scopes · for this lessondesign:content:read # export designs design:content:write # autofill: creates new designs design:meta:read # autofill job status, metadata brandtemplate:meta:read # list brand templates brandtemplate:content:read # the template's fields (dataset) asset:read # check uploaded files asset:write # upload images for autofill✅Scope ruleList them explicitly:asset:writedoes not grantasset:read. A request for a scope that is not enabled in the portal is rejected.In Redirect URLs enter the address Canva returns the user to after consent. The route for n8n: n8n shows its own “OAuth Redirect URL” in the credential form — you copy it from there. Canva's official examples use
http://127.0.0.1:<port>, notlocalhost(withlocalhostyou get CORS errors). ⚠️ Whetherlocalhostis explicitly rejected has not been verified. For production use anhttpsaddress you control, and remove the local addresses before submitting a public app. -
How sign-in works: OAuth 2.0 with PKCE
Canva uses the Authorization Code flow with PKCE (SHA-256). In short: you create a secret string (
code_verifier), send only its hash (code_challenge) and send the user to Canva. After consent Canva returns them to you with a one-timecode. You exchange it for a token by showing the original string. Why: a stolencodewithout thecode_verifieris useless.In n8n you do not write this by hand — the credential does it for you (next step). But it is worth seeing once so you can read the errors:
Node.js · code_verifier and code_challenge (per Canva's documentation)import crypto from "crypto"; const codeVerifier = crypto.randomBytes(96).toString("base64url"); const codeChallenge = crypto .createHash("sha256") .update(codeVerifier) .digest("base64url"); const state = crypto.randomBytes(96).toString("base64url");consent URL (line breaks for readability; replace the<…>parts)https://www.canva.com/api/oauth/authorize ?code_challenge=<codeChallenge> &code_challenge_method=s256 &scope=design:content:read%20design:content:write%20design:meta:read &response_type=code &client_id=<CLIENT_ID> &state=<state> &redirect_uri=<REDIRECT_URI>bash · exchange the code for a token (from a server only, never a browser)curl --request POST 'https://api.canva.com/rest/v1/oauth/token' \ --header "Authorization: Basic $(printf '%s:%s' "$CLIENT_ID" "$CLIENT_SECRET" | base64 -w0)" \ --header 'Content-Type: application/x-www-form-urlencoded' \ --data-urlencode 'grant_type=authorization_code' \ --data-urlencode "code=$AUTH_CODE" \ --data-urlencode "code_verifier=$CODE_VERIFIER" \ --data-urlencode "redirect_uri=$REDIRECT_URI"⚠️Token trapsThe access token is short-lived. For a new one you use therefresh_tokenwithgrant_type=refresh_token. Each refresh token works only once: the response returns a new access and a new refresh token — the old one no longer works, so you must store both. Requests with theclient_secretcome only from your server; Canva blocks them from a browser (CORS). Secrets and tokens do not go into scripts, git or the exported JSON of a workflow. -
Connect Canva in n8n
In n8n: Credentials → New → OAuth2 API (the generic type for HTTP Request). Choose Grant Type: PKCE — n8n supports it and creates the
code_verifierandcode_challengeitself. Authentication: Header sends the client as Basic — exactly what Canva's token endpoint wants.n8n · OAuth2 API credential · “Canva”Grant Type: PKCE Authorization URL: https://www.canva.com/api/oauth/authorize Access Token URL: https://api.canva.com/rest/v1/oauth/token Client ID: <CLIENT_ID from the Developer Portal> Client Secret: <CLIENT_SECRET — only here, in n8n> Scope: design:content:read design:content:write design:meta:read brandtemplate:meta:read brandtemplate:content:read asset:read asset:write Authentication: HeaderCopy the “OAuth Redirect URL” from the form into the Redirect URLs field at Canva, save and click Connect my account. You will see Canva's consent screen; after it the credential turns green. ⚠️ This connection was not run live for this lesson — if Canva returns an error about
code_challenge_method, review the Auth URI Query Parameters field in the credential.💡Why not plain “Authorization Code”The old version of this lesson chose “Authorization Code”. Canva requires PKCE, so that choice cannot pass thecode_verifiercheck. -
Prepare a brand template and learn its fields
Autofill does not work on an arbitrary design but on a published brand template with named data fields. Design a 1080×1080 post with texts (title, subtitle, call to action) and a place for a picture, mark them as data fields (Canva help on data autofill) and publish the design as a brand template. The ID is the end of the address:
canva.com/brand/brand-templates/AEN3TrQftXo.Field names are not guessed. You ask the template:
bash · the template's fieldscurl --request GET \ --url "https://api.canva.com/rest/v1/brand-templates/<TEMPLATE_ID>/dataset" \ --header "Authorization: Bearer $CANVA_TOKEN" # example response: # { "dataset": { "TITLE": {"type":"text"}, "SUBTITLE": {"type":"text"}, "BACKGROUND": {"type":"image"} } }✅Field ruleYou do not have to fill them all — unfilled ones keep the template's value. But a name that does not exist is silently skipped by Canva. So ask for the dataset first, then write the request.Images: for a field of type
imageyou pass theasset_idof an already uploaded file — an external address is not accepted directly. You upload throughPOST /asset-uploads(binary content and anAsset-Upload-Metadataheader with the name in Base64), then askGET /asset-uploads/{jobId}untilsuccess— the response holdsasset.id. If the picture is at a web address, usePOST /url-asset-uploads. -
The workflow: autofill → export → file
Eight steps are enough. Note the two waiting loops — they replace the “Wait 3s” of the old lesson, which was a guess, not a guarantee.
nodes in order1 Manual Trigger 2 Edit Fields (title, subtitle, asset_id) 3 HTTP Request "Autofill" 4 Code "Backoff" → Wait → HTTP Request "Autofill job" → If success? (loop) 5 HTTP Request "Export" 6 Code "Backoff" → Wait → HTTP Request "Export job" → If success? (loop) 7 HTTP Request "Download file" (Response Format: File) 8 Read/Write Files from Disk (Write)In the HTTP Request nodes: Authentication → Generic Credential Type → OAuth2 API → “Canva”, Send Body → JSON.
n8n · HTTP Request “Autofill” · POST https://api.canva.com/rest/v1/autofills{ "brand_template_id": "<TEMPLATE_ID>", "data": { "TITLE": { "type": "text", "text": "{{ $json.title }}" }, "SUBTITLE": { "type": "text", "text": "{{ $json.subtitle }}" }, "BACKGROUND": { "type": "image", "asset_id": "{{ $json.asset_id }}" } } } // response: { "job": { "id": "…", "status": "in_progress" } }n8n · HTTP Request “Autofill job” · GET …/autofills/{{ $('Autofill').item.json.job.id }}// success: job.status = "success" // the design is in job.result.design : { id, url, thumbnail… } // failure: job.status = "failed" → job.error.code (e.g. autofill_error)n8n · HTTP Request “Export” · POST https://api.canva.com/rest/v1/exports{ "design_id": "{{ $('Autofill job').item.json.job.result.design.id }}", "format": { "type": "png", "width": 1080 } } // export_quality defaults to "regular"; "pro" can fail without a paid plan // response: { "job": { "id": "…", "status": "in_progress" } }n8n · HTTP Request “Export job” · GET …/exports/{{ $('Export').item.json.job.id }}// success: job.status = "success", job.urls = ["https://…"] (valid for 24 hours) // failure: job.error.code = license_required | approval_required | internal_failureLast: an HTTP Request with
GETon{{ $json.job.urls[0] }}and Response Format → File, then Read/Write Files from Disk (Write) into a folder that is mounted into the n8n container — otherwise the file stays inside the container and you lose it on restart. -
Waiting: polling with backoff
Canva recommends an exponentially growing interval: start fast, lengthen the pause up to a cap. That way you do not hit the limits (the export status check is 120 per minute) and do not idle needlessly. The loop in n8n: Code → Wait → HTTP (check) → If (
success?). On “no” — go back to Code. Onfailedor when the cap is reached — stop with an error.n8n · Code node “Backoff” · Run Once for Each Itemconst attempt = $runIndex + 1; // which pass through this node if (attempt > 8) { throw new Error("Canva: the job did not finish after 8 checks"); } return { json: { ...$json, attempt, waitSeconds: Math.min(2 ** attempt, 20) } }; // Wait node: Resume = After Time Interval, Amount = {{ $json.waitSeconds }}, Unit = Seconds💡Why a cap of 8 checksAt 2, 4, 8, 16, 20… seconds that is about two minutes in total — more than enough for one design. If a job hangs longer, it is more likely an error than a slow server. The numbers are a sensible choice, not a Canva rule. -
Bulk generation and the limits
Ten products — ten designs. You take rows from Google Sheets or a CSV, feed them through Loop Over Items (formerly Split In Batches) with size 1 and run the whole path for each. The narrow point is export: the limit is 20 requests per minute per user, so at most one every 3 seconds.
Export limit Value User (through your integration) 20 / min · 75 per 5 min · 500 per 24 h The whole integration 750 per 5 min · 5,000 per 24 h One and the same design 75 per 5 min ⚠️The limit is not “60 per minute for everything”Every endpoint has its own per-user per-minute limit (the table in step 1). Autofill is 60, export is 20. When exceeded, Canva returns429with codetoo_many_requests— in n8n turn on Retry On Fail for the HTTP nodes and add a Wait of 3–4 seconds between designs.A day with 100 designs is safe: 100 exports is far below 500 per day per user. For thousands — split across days or users.
-
Canva through an AI assistant: Canva MCP
If you want to tell an assistant “make me a post from this template”, you do not need your own n8n process. Canva offers a remote MCP server at
https://mcp.canva.com/mcp🌐 global: the assistant gets tools to search and create designs, upload files, comment and export; each has its own per-minute limit (e.g. export and creation — 20, search — 100). Autofilling brand templates is “Pro and above”; export works on all plans, but premium quality does not.How to connect a specific assistant (Claude, ChatGPT, Cursor…) is shown in Canva's help center. Every user signs in with their own account. When to use which: MCP — for conversation and single designs; Connect API through n8n — for a repeating process not driven by a person.
-
Without a suitable plan: HTML → PNG
If autofill is not available to you, there is a path that does not depend on Canva: an HTML template plus a headless browser. It is visually poorer but free and 🔒 fully local. An example with Puppeteer (requires Node.js 22 or newer):
bash + Node.js · html-to-image.jsmkdir -p ~/designs && cd ~/designs npm install puppeteer cat > html-to-image.js << 'EOF' const puppeteer = require('puppeteer'); const [htmlFile, outFile = 'design.png'] = process.argv.slice(2); (async () => { const browser = await puppeteer.launch(); const page = await browser.newPage(); await page.setViewport({ width: 1080, height: 1080 }); await page.goto('file://' + htmlFile); await page.screenshot({ path: outFile }); await browser.close(); console.log(outFile); })(); EOF node html-to-image.js "$PWD/template.html" "$PWD/post.png"⚠️Two caveats for n8nThe Execute Command node is turned off by default in n8n (it is in theNODES_EXCLUDElist) because it can run anything on the server. Enabling it is a conscious decision for an instance you trust. Cleaner: run the script separately (a scheduler, a second container) and let n8n read the finished file. Also — render only your own HTML: a page from a foreign source in a headless browser is a risk. If Puppeteer refuses to start as root in a container, it needs the--no-sandboxflag — use it only there and only for your own HTML.
04Check
Checklist
- The app is created, REST APIs is on, scopes are listed, the redirect URL is saved, the secret is stored safely.
- The n8n credential uses PKCE and Header; “Connect my account” goes through the consent screen.
- The template is published, and
/datasetreturns the expected field names. - The autofill job reaches
successand returns adesign.id. - The export job reaches
successand the file is downloaded within 24 hours. - The waiting loop has backoff and a hard cap.
- The bulk process sends no more than about one export every 3 seconds.
- The exported workflow JSON contains no secrets or tokens.
Quiz
1. What kind of sign-in does the Canva Connect API use?
2. POST /autofills returned in_progress. What do you do?
3. How many POST /exports requests can one user make per minute?
4. What is true about Canva's refresh token?
05What's next
06Sources
- Canva: Authentication 🌐 global — OAuth 2.0 with PKCE, consent URL, tokens.
- Canva: Autofill guide — brand templates, dataset, the autofill job, the plan notes.
- Canva: Create design autofill job — request body, modes, 60/min limit.
- Canva: Create design export job — formats, quality, limits and errors.
- Canva: Scopes — the full list of permissions.
- Canva: API requests and responses — asynchronous jobs and polling.
- Canva REST APIs — OpenAPI description — paths, limits, availability by plan.
- Canva MCP: tools and limits — by plan.
- Canva Help: connect an AI assistant — steps for specific assistants.
- n8n: HTTP Request credentials 🔒 — OAuth2, PKCE, Header.
- n8n: node environment variables —
NODES_EXCLUDEand Execute Command. - Puppeteer 🔒 — documentation for the fallback path.