Canva Connect API — Install & Auth
Overview
Set up a Canva Connect API integration with OAuth 2.0 Authorization Code flow with PKCE (SHA-256). The Canva Connect API is a REST API at https://api.canva.com/rest/v1/* — there is no SDK package. All calls use fetch or axios with Bearer tokens.
Prerequisites
- Node.js 18+ (for native
crypto.subtle and fetch)
- A Canva account at canva.com
- An integration registered at canva.dev
Instructions
Step 1: Register Your Integration
- Go to Settings > Integrations at canva.com/developers
- Create a new integration — note your Client ID and Client Secret
- Add redirect URI(s): e.g.
http://localhost:3000/auth/canva/callback
- Enable required scopes under Permissions
Step 2: Store Credentials
# .env (NEVER commit — add to .gitignore)
CANVA_CLIENT_ID=OCAxxxxxxxxxxxxxxxx
CANVA_CLIENT_SECRET=xxxxxxxxxxxxxxxx
CANVA_REDIRECT_URI=http://localhost:3000/auth/canva/callback
echo '.env' >> .gitignore
echo '.env.local' >> .gitignore
Step 3: Implement OAuth 2.0 PKCE Flow
// src/canva/auth.ts
import crypto from 'crypto';
// 1. Generate PKCE code verifier and challenge
export function generatePKCE(): { verifier: string; challenge: string } {
const verifier = crypto.randomBytes(64).toString('base64url'); // 43-128 chars
const challenge = crypto
.createHash('sha256')
.update(verifier)
.digest('base64url');
return { verifier, challenge };
}
// 2. Build the authorization URL
export function getAuthorizationUrl(opts: {
clientId: string;
redirectUri: string;
scopes: string[];
codeChallenge: string;
state: string;
}): string {
const params = new URLSearchParams({
response_type: 'code',
client_id: opts.clientId,
redirect_uri: opts.redirectUri,
scope: opts.scopes.join(' '),
code_challenge: opts.codeChallenge,
code_challenge_method: 'S256',
state: opts.state,
});
return `https://www.canva.com/api/oauth/authorize?${params}`;
}
// 3. Exchange authorization code for access token
export async function exchangeCodeForToken(opts: {
code: string;
codeVerifier: string;
clientId: string;
clientSecret: string;
redirectUri: string;
}): Promise<{ access_token: string; refresh_token: string; expires_in: number }> {
const basicAuth = Buffer.from(
`${opts.clientId}:${opts.clientSecret}`
).toString('base64');
const res = await fetch('https://api.canva.com/rest/v1/oauth/token', {
method: 'POST',
headers: {
'Authorization': `Basic ${basicAuth}`,
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
grant_type: 'authorization_code',
code: opts.code,
code_verifier: opts.codeVerifier,
redirect_uri: opts.redirectUri,
}),
});
if (!res.ok) {
const err = await res.json();
throw new Error(`Token exchange failed: ${err.error} — ${err.error_description}`);
}
return res.json();
}
// 4. Refresh an expired access token (access tokens expire in ~4 hours)
export async function refreshAccessToken(opts: {
refreshToken: string;
clientId: string;
clientSecret: string;
}): Promise<{ access_token: string; refresh_token: string; expires_in: number }> {
const basicAuth = Buffer.from(
`${opts.clientId}:${opts.clientSecret}`
).toString('base64');
const res = await fetch('https://api.canva.com/rest/v1/oauth/token', {
method: 'POST',
headers: {
'Authorization': `Basic ${basicAuth}`,
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
grant_type: 'refresh_token',
refresh_token: opts.refreshToken,
}),
});
if (!res.ok) throw new Error('Token refresh failed');
return res.json();
}
Step 4: Verify Connection
// Verify token works by calling GET /v1/users/me (no scopes required)
async function verifyConnection(accessToken: string): Promise<void> {
const res = await fetch('https://api.canva.com/rest/v1/users/me', {
headers: { 'Authorization': `Bearer ${accessToken}` },
});
if (!res.ok) throw new Error(`Verification failed: ${res.status}`);
const { team_user } = await res.json();
console.log(`Connected — user_id: ${team_user.user_id}, team_id: ${team_user.team_id}`);
}
Available OAuth Scopes
| Scope |
Description |
design:content:read |
Read design contents, export designs |
design:content:write |
Create designs, autofill brand templates |
design:meta:read |
List designs, get design metadata |
asset:read |
View uploaded asset metadata |
asset:write |
Upload, update, delete assets |
brandtemplate:content:read |
Read brand template content |
brandtemplate:meta:read |
List and view brand template metadata |
folder:read |
View folder contents |
folder:write |
Create, update, delete folders |
folder:permission:write |
Manage folder permissions |
comment:read |
Read design comments |
comment:write |
Create comments and replies |
collaboration:event |
Receive webhook notifications |
profile:read |
Read user profile information |
Error Handling
| Error |
Cause |
Solution |
invalid_client |
Wrong client_id or secret |
Verify credentials in Canva dashboard |
invalid_grant |
Expired or reused auth code |
Restart OAuth flow — codes are single-use |
invalid_scope |
Scope not enabled |
Enable scope in integration settings |
access_denied |
User rejected consent |
Prompt user again |
| Token expired (401) |
Access token > 4 hours old |
Call refresh token endpoint |
Resources
Next Steps
After successful auth, proceed to canva-hello-world for your first API call.
1---2name: canva-install-auth3description: |4 Set up Canva Connect API OAuth 2.0 PKCE authentication and project scaffolding.5 Use when creating a new Canva integration, setting up OAuth credentials,6 or initializing a Canva Connect API project.7 Trigger with phrases like "install canva", "setup canva",8 "canva auth", "configure canva API", "canva OAuth".9allowed-tools: Read, Write, Edit, Bash(npm:*), Bash(pnpm:*), Bash(npx:*), Grep10version: 1.0.011license: MIT12author: Jeremy Longshore <jeremy@intentsolutions.io>13tags: [saas, design, canva]14compatible-with: claude-code15---1617# Canva Connect API — Install & Auth1819## Overview2021Set up a Canva Connect API integration with OAuth 2.0 Authorization Code flow with PKCE (SHA-256). The Canva Connect API is a REST API at `https://api.canva.com/rest/v1/*` — there is no SDK package. All calls use `fetch` or `axios` with Bearer tokens.2223## Prerequisites2425- Node.js 18+ (for native `crypto.subtle` and `fetch`)26- A Canva account at [canva.com](https://www.canva.com)27- An integration registered at [canva.dev](https://www.canva.dev/docs/connect/creating-integrations/)2829## Instructions3031### Step 1: Register Your Integration32331. Go to **Settings > Integrations** at [canva.com/developers](https://www.canva.com/developers)342. Create a new integration — note your **Client ID** and **Client Secret**353. Add redirect URI(s): e.g. `http://localhost:3000/auth/canva/callback`364. Enable required scopes under **Permissions**3738### Step 2: Store Credentials3940```bash41# .env (NEVER commit — add to .gitignore)42CANVA_CLIENT_ID=OCAxxxxxxxxxxxxxxxx43CANVA_CLIENT_SECRET=xxxxxxxxxxxxxxxx44CANVA_REDIRECT_URI=http://localhost:3000/auth/canva/callback45```4647```bash48echo '.env' >> .gitignore49echo '.env.local' >> .gitignore50```5152### Step 3: Implement OAuth 2.0 PKCE Flow5354```typescript55// src/canva/auth.ts56import crypto from 'crypto';5758// 1. Generate PKCE code verifier and challenge59export function generatePKCE(): { verifier: string; challenge: string } {60 const verifier = crypto.randomBytes(64).toString('base64url'); // 43-128 chars61 const challenge = crypto62 .createHash('sha256')63 .update(verifier)64 .digest('base64url');65 return { verifier, challenge };66}6768// 2. Build the authorization URL69export function getAuthorizationUrl(opts: {70 clientId: string;71 redirectUri: string;72 scopes: string[];73 codeChallenge: string;74 state: string;75}): string {76 const params = new URLSearchParams({77 response_type: 'code',78 client_id: opts.clientId,79 redirect_uri: opts.redirectUri,80 scope: opts.scopes.join(' '),81 code_challenge: opts.codeChallenge,82 code_challenge_method: 'S256',83 state: opts.state,84 });85 return `https://www.canva.com/api/oauth/authorize?${params}`;86}8788// 3. Exchange authorization code for access token89export async function exchangeCodeForToken(opts: {90 code: string;91 codeVerifier: string;92 clientId: string;93 clientSecret: string;94 redirectUri: string;95}): Promise<{ access_token: string; refresh_token: string; expires_in: number }> {96 const basicAuth = Buffer.from(97 `${opts.clientId}:${opts.clientSecret}`98 ).toString('base64');99100 const res = await fetch('https://api.canva.com/rest/v1/oauth/token', {101 method: 'POST',102 headers: {103 'Authorization': `Basic ${basicAuth}`,104 'Content-Type': 'application/x-www-form-urlencoded',105 },106 body: new URLSearchParams({107 grant_type: 'authorization_code',108 code: opts.code,109 code_verifier: opts.codeVerifier,110 redirect_uri: opts.redirectUri,111 }),112 });113114 if (!res.ok) {115 const err = await res.json();116 throw new Error(`Token exchange failed: ${err.error} — ${err.error_description}`);117 }118 return res.json();119}120121// 4. Refresh an expired access token (access tokens expire in ~4 hours)122export async function refreshAccessToken(opts: {123 refreshToken: string;124 clientId: string;125 clientSecret: string;126}): Promise<{ access_token: string; refresh_token: string; expires_in: number }> {127 const basicAuth = Buffer.from(128 `${opts.clientId}:${opts.clientSecret}`129 ).toString('base64');130131 const res = await fetch('https://api.canva.com/rest/v1/oauth/token', {132 method: 'POST',133 headers: {134 'Authorization': `Basic ${basicAuth}`,135 'Content-Type': 'application/x-www-form-urlencoded',136 },137 body: new URLSearchParams({138 grant_type: 'refresh_token',139 refresh_token: opts.refreshToken,140 }),141 });142143 if (!res.ok) throw new Error('Token refresh failed');144 return res.json();145}146```147148### Step 4: Verify Connection149150```typescript151// Verify token works by calling GET /v1/users/me (no scopes required)152async function verifyConnection(accessToken: string): Promise<void> {153 const res = await fetch('https://api.canva.com/rest/v1/users/me', {154 headers: { 'Authorization': `Bearer ${accessToken}` },155 });156157 if (!res.ok) throw new Error(`Verification failed: ${res.status}`);158159 const { team_user } = await res.json();160 console.log(`Connected — user_id: ${team_user.user_id}, team_id: ${team_user.team_id}`);161}162```163164## Available OAuth Scopes165166| Scope | Description |167|-------|-------------|168| `design:content:read` | Read design contents, export designs |169| `design:content:write` | Create designs, autofill brand templates |170| `design:meta:read` | List designs, get design metadata |171| `asset:read` | View uploaded asset metadata |172| `asset:write` | Upload, update, delete assets |173| `brandtemplate:content:read` | Read brand template content |174| `brandtemplate:meta:read` | List and view brand template metadata |175| `folder:read` | View folder contents |176| `folder:write` | Create, update, delete folders |177| `folder:permission:write` | Manage folder permissions |178| `comment:read` | Read design comments |179| `comment:write` | Create comments and replies |180| `collaboration:event` | Receive webhook notifications |181| `profile:read` | Read user profile information |182183## Error Handling184185| Error | Cause | Solution |186|-------|-------|----------|187| `invalid_client` | Wrong client_id or secret | Verify credentials in Canva dashboard |188| `invalid_grant` | Expired or reused auth code | Restart OAuth flow — codes are single-use |189| `invalid_scope` | Scope not enabled | Enable scope in integration settings |190| `access_denied` | User rejected consent | Prompt user again |191| Token expired (401) | Access token > 4 hours old | Call refresh token endpoint |192193## Resources194195- [Canva Connect API Docs](https://www.canva.dev/docs/connect/)196- [Authentication Guide](https://www.canva.dev/docs/connect/authentication/)197- [Scopes Reference](https://www.canva.dev/docs/connect/appendix/scopes/)198- [OpenAPI Spec](https://www.canva.dev/sources/connect/api/latest/api.yml)199200## Next Steps201202After successful auth, proceed to `canva-hello-world` for your first API call.203