The KAGAMI mark КАГАМИ
kagami.bg/academy · lesson · machine-readable viewVERIFIED 2026-10-01 · UPDATED 2026-10-01
IDENTITY
module
DevStation-04 · Canva Connect API and n8n: automated designs
series
DevStation · lesson 4 of 6
level
Intermediate
duration
1–2 h
prerequisites
A running n8n instance (lesson 3); a Canva account with MFA enabled; for autofill, a Canva plan with brand templates (see KEY CONCEPTS)
trust_label
VERIFIED 2026-10-01 (endpoints, scopes, OAuth flow, rate limits and plan notes checked against canva.dev and the public OpenAPI description) · UPDATED 2026-10-01 · NOT run end-to-end against a live Canva account
language
human view: bg · english edition: /en/academy/devstation/ (same file name)
previous
DevStation-03 · Docker, n8n, OpenClaw
next
DevStation-05 · Marketing AI agent
PURPOSE

Generate on-brand visuals programmatically: publish a Canva brand template with named data fields, call the Canva Connect REST API from n8n (OAuth 2.0 authorization code flow with PKCE), autofill the template with generated text and uploaded images, poll the asynchronous jobs, export the finished design as PNG or PDF and save the file. Includes rate-limit planning for batches, the Canva MCP server as an alternative for AI assistants, and a plan-independent fallback (HTML to PNG with a headless browser).

KEY CONCEPTS
COMMANDS / PATHS
CHECKLIST
NEXT MODULE

DevStation-05 · Marketing AI agent (idea → copy → Canva design → publishing time) · offer: Quick experiment (kagami.bg/stalbata/)

SOURCES
TAGS
canvaconnect-apioauth2pkceautofillbrand-templatesn8nrate-limitsmcp
VERIFIED · 01.10.2026 UPDATED · 01.10.2026

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”.

⏱ 1–2 h Intermediate DevStation · Lesson 4/6 Canva · OAuth · autofill · n8n
n8n (the workflow)🔒 local Canva Connect API🌐 global Canva MCP (for AI assistants)🌐 global Puppeteer (fallback)🔒 local
🔄
UPDATED · 01.10.2026 — what changed
The 2024 version of this lesson had several wrong details; they are now fixed against Canva's official documentation (canva.dev) and its public OpenAPI description. Endpoints: autofill is 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

02Before you start

What you wantWhat Canva's documentation says
List, read and export designs, upload filesWorks 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 autofillThe 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 teamPrivate 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”.
⚠️
Do not build a whole process on a plan you have not checked
Before you build ten workflows around autofill, make one manual request to /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

  1. 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 carries Authorization: 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.id with status in_progress, and ask a second endpoint until it becomes success or failed.

    Method and pathWhat it is forLimit per user
    GET /designsList designs (up to 100 per page, continuation for the next)100 / min
    GET /brand-templatesList the brand templates you can access100 / min
    GET /brand-templates/{id}/datasetNames and types of the fillable fields100 / min
    POST /asset-uploads · GET /asset-uploads/{jobId}Upload an image, then check the job30 / min · 180 / min
    POST /autofills · GET /autofills/{jobId}Fill a template → new design; check the job60 / min · 120 / min
    POST /exports · GET /exports/{exportId}Export a design (PNG, JPG, PDF, PPTX, GIF, MP4…); check and get download URLs20 / min · 120 / min
    POST /foldersCreate a folder20 / min
    the path in one line
    brand template → POST /autofills → GET /autofills/{id} → design.id
                   → POST /exports  → GET /exports/{id}   → urls[0] → file
  2. 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 lesson
    design: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 rule
    List them explicitly: asset:write does not grant asset: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>, not localhost (with localhost you get CORS errors). ⚠️ Whether localhost is explicitly rejected has not been verified. For production use an https address you control, and remove the local addresses before submitting a public app.

  3. 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-time code. You exchange it for a token by showing the original string. Why: a stolen code without the code_verifier is 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 traps
    The access token is short-lived. For a new one you use the refresh_token with grant_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 the client_secret come 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.
  4. 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_verifier and code_challenge itself. 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:     Header

    Copy 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 the code_verifier check.
  5. 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 fields
    curl --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 rule
    You 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 image you pass the asset_id of an already uploaded file — an external address is not accepted directly. You upload through POST /asset-uploads (binary content and an Asset-Upload-Metadata header with the name in Base64), then ask GET /asset-uploads/{jobId} until success — the response holds asset.id. If the picture is at a web address, use POST /url-asset-uploads.

  6. 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 order
    1 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_failure

    Last: an HTTP Request with GET on {{ $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.

  7. 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. On failed or when the cap is reached — stop with an error.

    n8n · Code node “Backoff” · Run Once for Each Item
    const 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 checks
    At 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.
  8. 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 limitValue
    User (through your integration)20 / min · 75 per 5 min · 500 per 24 h
    The whole integration750 per 5 min · 5,000 per 24 h
    One and the same design75 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 returns 429 with code too_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.

  9. 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.

  10. 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.js
    mkdir -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 n8n
    The Execute Command node is turned off by default in n8n (it is in the NODES_EXCLUDE list) 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-sandbox flag — use it only there and only for your own HTML.

04Check

Checklist

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

  1. Canva: Authentication 🌐 global — OAuth 2.0 with PKCE, consent URL, tokens.
  2. Canva: Autofill guide — brand templates, dataset, the autofill job, the plan notes.
  3. Canva: Create design autofill job — request body, modes, 60/min limit.
  4. Canva: Create design export job — formats, quality, limits and errors.
  5. Canva: Scopes — the full list of permissions.
  6. Canva: API requests and responses — asynchronous jobs and polling.
  7. Canva REST APIs — OpenAPI description — paths, limits, availability by plan.
  8. Canva MCP: tools and limits — by plan.
  9. Canva Help: connect an AI assistant — steps for specific assistants.
  10. n8n: HTTP Request credentials 🔒 — OAuth2, PKCE, Header.
  11. n8n: node environment variables — NODES_EXCLUDE and Execute Command.
  12. Puppeteer 🔒 — documentation for the fallback path.