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.subtleandfetch) - 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.