GET /v1/users
Execute the Canva design creation and export pipeline via the Connect API.
ReadWriteEditBash(npm:*)Grep
Canva Core Workflow A — Design Creation & Export
Overview
The primary Canva integration workflow: create designs via the REST API, let users edit them in Canva's editor, then export finished designs as PDF/PNG/JPG for downstream use (email campaigns, social posts, print orders).
Prerequisites
- Completed
canva-install-auth setup with valid access token
- Scopes:
design:content:write, design:content:read, design:meta:read
Instructions
Step 1: Create a Design
// POST https://api.canva.com/rest/v1/designs
// Rate limit: 20 req/min per user
// Scope: design:content:write
interface CreateDesignRequest {
design_type:
| { type: 'preset'; name: 'doc' | 'whiteboard' | 'presentation' }
| { type: 'custom'; width: number; height: number }; // 40-8000 px
title?: string; // 1-255 characters
asset_id?: string; // Image asset to insert
}
// Create a social media post (custom dimensions)
const { design } = await canvaAPI('/designs', token, {
method: 'POST',
body: JSON.stringify({
design_type: { type: 'custom', width: 1080, height: 1080 },
title: 'Instagram Post — Q1 Campaign',
}),
});
// design.id — unique identifier for all future operations
// design.urls.edit_url — redirect user here to edit (expires 30 days)
// design.urls.view_url — read-only link (expires 30 days)
// design.thumbnail.url — preview image (expires 15 minutes)
Note: Blank designs are auto-deleted after 7 days if never edited.
Step 2: Redirect User to Edit
// Redirect the user to Canva's editor
// The edit_url is user-specific and expires after 30 days
res.redirect(design.urls.edit_url);
Step 3: Get Design Metadata
// GET https://api.canva.com/rest/v1/designs/{designId}
// Rate limit: 100 req/min per user
// Scope: design:meta:read
const { design: meta } = await canvaAPI(`/designs/${designId}`, token);
console.log(`Title: ${meta.title}`);
console.log(`Pages: ${meta.page_count}`);
console.log(`Created: ${new Date(meta.created_at * 1000).toISOString()}`);
console.log(`Updated: ${new Date(meta.updated_at * 1000).toISOString()}`);
console.log(`Owner: user=${meta.owner.user_id}, team=${meta.owner.team_id}`);
Step 4: Export the Finished Design
// POST https://api.canva.com/rest/v1/exports
// Rate limits:
// Per user: 75 exports/5min, 500/24hr
// Per integration: 750 exports/5min, 5000/24hr
// Per document: 75 exports/5min
// Scope: design:content:read
// Export as high-quality PDF
const { job } = await canvaAPI('/exports', token, {
method: 'POST',
body: JSON.stringify({
design_id: designId,
Execute Canva asset management, brand template autofill, and folder organization.
ReadWriteEditBash(npm:*)Grep
Canva Core Workflow B — Assets, Autofill & Folders
Overview
Secondary workflow: upload assets to Canva, autofill brand templates with dynamic data (text, images, charts), and organize content with folders. Autofill requires a Canva Enterprise organization.
Prerequisites
- Completed
canva-install-auth with valid access token
- Scopes:
asset:read, asset:write, brandtemplate:meta:read, brandtemplate:content:read, design:content:write, folder:read, folder:write
Instructions
- Authorize the caller, tenant, template/asset rights, data class, and destination before any upload, autofill, or folder mutation.
- Validate input files and template data, use a durable idempotency key, and process one bounded approved job at a time.
- Store generated assets in encrypted expiry-controlled storage and reconcile the authorized job before retrying.
Asset Management
Upload an Asset (Binary)
// POST https://api.canva.com/rest/v1/asset-uploads
// Rate limit: 30 req/min per user
// Scope: asset:write
// Content-Type: application/octet-stream
import { readFileSync } from 'fs';
async function uploadAsset(
filePath: string,
name: string,
token: string
): Promise<{ id: string; status: string }> {
// Asset name must be Base64-encoded, max 50 chars unencoded
const nameBase64 = Buffer.from(name).toString('base64');
const fileData = readFileSync(filePath);
const res = await fetch('https://api.canva.com/rest/v1/asset-uploads', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/octet-stream',
'Asset-Upload-Metadata': JSON.stringify({ name_base64: nameBase64 }),
},
body: fileData,
});
if (!res.ok) throw new Error(`Upload failed: ${res.status}`);
return res.json();
}
// Upload returns a job — poll for asset ID
const uploadJob = await uploadAsset('./hero-banner.png', 'Hero Banner Q1', token);
Upload an Asset via URL
// POST https://api.canva.com/rest/v1/url-asset-uploads
// Rate limit: 30 req/min per user
const { job } = await canvaAPI('/url-asset-uploads', token, {
method: 'POST',
body: JSON.stringify({
name: 'Product Photo',
url: 'https://example.com/images/product-shot.jpg',
}),
});
// Poll GET /v1/url-asset-uploads/{jobId} for completion
Get, Update, Delete Assets
// GET /v1/assets/{assetId} — scope: asset:read
const asset = await canvaAPI(`/assets/${assetId}`, token);
// PATCH /v1/assets/{assetId} — scope: asset:write
await canvaAPI(`/assets/${assetId}`, token, {
method:
Optimize Canva Connect API usage costs through efficient API patterns and monitoring.
ReadGrep
Canva Cost Tuning
Overview
Optimize Canva Connect API usage. While the Connect API itself is free to call, rate limits constrain throughput. Canva Enterprise (required for autofill) has per-seat licensing costs. Optimize by reducing unnecessary calls, caching effectively, and batching operations.
Prerequisites
- Current pricing and account-limit evidence from Canva and the organization; the examples below are not a purchase quote.
- An approved workload scope, aggregate telemetry, and named spend/usage owner.
Instructions
- Verify current plan terms, features, and limits before estimating or changing workload behavior.
- Measure aggregate authorized usage, minimize requests and export fields, and set conservative budget/rate ceilings.
- Test one optimization behind a rollback flag and retain redacted reconciliation evidence before wider rollout.
Canva Pricing Model
| Tier |
Cost |
Connect API Access |
Autofill API |
Brand Templates |
| Canva Free |
$0/user |
Yes |
No |
No |
| Canva Pro |
$15/user/mo |
Yes |
No |
No |
| Canva Teams |
$10/user/mo (5+) |
Yes |
No |
Limited |
| Canva Enterprise |
Custom |
Yes |
Yes |
Yes |
Key insight: The REST API is free — costs come from Canva subscriptions. Autofill and brand template APIs require Enterprise.
API Call Reduction Strategies
Cache Design Metadata
// Design metadata rarely changes — cache aggressively
// Save: ~100 GET /designs/{id} calls/min per user
const designMetadata = await cachedCanvaCall(
`design:${designId}`,
() => canvaAPI(`/designs/${designId}`, token),
300 // 5 min TTL
);
Avoid Redundant Exports
// Track exported designs to prevent duplicate exports
class ExportTracker {
private exportedDesigns = new Map<string, { urls: string[]; expiresAt: number }>();
async exportIfNeeded(designId: string, format: object, token: string): Promise<string[]> {
const cached = this.exportedDesigns.get(designId);
// Export URLs valid for 24 hours — reuse if still valid
if (cached && Date.now() < cached.expiresAt) {
return cached.urls;
}
const { job } = await canvaAPI('/exports', token, {
method: 'POST',
body: JSON.stringify({ design_id: designId, format }),
});
const urls = await pollExport(job.id, token);
this.exportedDesigns.set(designId, {
urls,
expiresAt: Date.now() + 23 * 60 * 60 * 1000, // 23 hours (1h buffer)
});
return urls;
}
}
Pagination with Early Exit
Implement Canva Connect API data handling, PII protection, and GDPR/CCPA compliance.
ReadWriteEdit
Canva Data Handling
Overview
Handle Canva Connect API data responsibly. The API exposes user identifiers, design metadata, design content (via exports), uploaded assets, and comments. Apply proper classification, retention, and privacy controls.
Prerequisites
- A documented inventory for design metadata/content, exports, assets, comments, OAuth tokens, and signed URLs.
- Approved encrypted storage, retention/deletion workflow, and a privacy/incident owner.
Instructions
- Collect only fields needed for the approved purpose and classify them before persistence or sharing.
- Keep tokens and temporary export URLs server-side, encrypted, access-controlled, and excluded from logs and analytics.
- Apply retention/deletion policy to exports and caches, then retain only a redacted execution receipt.
Data Classification — Canva API Responses
| Data Type |
Source Endpoint |
Sensitivity |
Handling |
| User ID, Team ID |
GET /v1/users/me |
Internal |
Don't expose externally |
| User profile |
GET /v1/users/me/profile |
PII |
Encrypt at rest, minimize |
| Design metadata |
GET /v1/designs |
Business |
Standard protection |
| Design content |
Export URLs from /v1/exports |
Confidential |
Time-limited URLs, don't cache |
| OAuth tokens |
/v1/oauth/token |
Secret |
Encrypt, never log |
| Asset files |
/v1/asset-uploads |
Business |
Validate, scan for malware |
| Comments |
/v1/designs/{id}/comment_threads |
PII |
May contain personal data |
| Webhook payloads |
Incoming POST |
Mixed |
Verify signature first |
Token Protection
// NEVER log tokens — they grant full access to a user's Canva account
function redactCanvaData(data: any): any {
const sensitiveKeys = [
'access_token', 'refresh_token', 'authorization',
'client_secret', 'code_verifier',
];
if (typeof data !== 'object' || data === null) return data;
const redacted = Array.isArray(data) ? [...data] : { ...data };
for (const key of Object.keys(redacted)) {
if (sensitiveKeys.includes(key.toLowerCase())) {
redacted[key] = '[REDACTED]';
} else if (typeof redacted[key] === 'object') {
redacted[key] = redactCanvaData(redacted[key]);
}
}
return redacted;
}
// Safe logging
console.log('Canva response:', JSON.stringify(redactCanvaData(apiResponse)));
Tempora
Collect Canva Connect API debug evidence for troubleshooting and support.
ReadBash(grep:*)Bash(curl:*)Bash(tar:*)Grep
Canva Debug Bundle
Overview
Collect diagnostic information for Canva Connect API issues. Tests connectivity to api.canva.com/rest/v1/*, validates OAuth tokens, checks rate limits, and packages evidence for support tickets.
Prerequisites
- Incident-owner approval, access to protected redacted telemetry, and an encrypted access-controlled evidence destination.
- A scoped incident/request reference; diagnostic collection must not enumerate users, designs, assets, or tokens.
Instructions
Step 1: Connectivity & Auth Check Script
#!/bin/bash
# canva-debug.sh — Run with: bash canva-debug.sh
set -euo pipefail
TOKEN="${CANVA_ACCESS_TOKEN:-}"
BUNDLE="canva-debug-$(date +%Y%m%d-%H%M%S)"
mkdir -p "$BUNDLE"
echo "=== Canva Connect API Debug Bundle ===" | tee "$BUNDLE/summary.txt"
echo "Generated: $(date -u +%Y-%m-%dT%H:%M:%SZ)" | tee -a "$BUNDLE/summary.txt"
echo "" >> "$BUNDLE/summary.txt"
# 1. Check API reachability
echo "--- API Connectivity ---" >> "$BUNDLE/summary.txt"
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" \
-H "Authorization: Bearer ${TOKEN}" \
"https://api.canva.com/rest/v1/users/me")
echo "GET /v1/users/me: HTTP $HTTP_CODE" | tee -a "$BUNDLE/summary.txt"
# 2. Get user identity (if token valid)
if [ "$HTTP_CODE" = "200" ]; then
curl -s -H "Authorization: Bearer $TOKEN" \
"https://api.canva.com/rest/v1/users/me" | tee "$BUNDLE/user-identity.json" \
| python3 -m json.tool 2>/dev/null || true
echo "Token: VALID" >> "$BUNDLE/summary.txt"
else
echo "Token: INVALID or EXPIRED (HTTP $HTTP_CODE)" >> "$BUNDLE/summary.txt"
fi
# 3. Check response headers for rate limit info
echo "" >> "$BUNDLE/summary.txt"
echo "--- Rate Limit Headers ---" >> "$BUNDLE/summary.txt"
curl -s -D - -o /dev/null -H "Authorization: Bearer $TOKEN" \
"https://api.canva.com/rest/v1/designs?limit=1" 2>&1 \
| grep -iE "(x-ratelimit|retry-after|content-type|date)" \
>> "$BUNDLE/summary.txt" 2>/dev/null || echo "No rate limit headers" >> "$BUNDLE/summary.txt"
# 4. DNS resolution
echo "" >> "$BUNDLE/summary.txt"
echo "--- DNS Resolution ---" >> "$BUNDLE/summary.txt"
nslookup api.canva.com >> "$BUNDLE/summary.txt" 2>&1 || echo "nslookup not available" >> "$BUNDLE/summary.txt"
# 5. TLS check
echo "" >> "$BUNDLE/summary.txt"
echo "--- TLS Handshake ---" >> "$BUNDLE/summary.txt"
cu
Deploy Canva Connect API integrations to Vercel, Fly.
ReadWriteEditBash(vercel:*)Bash(fly:*)Bash(gcloud:*)
Canva Deploy Integration
Overview
Deploy Canva Connect API integrations to popular platforms with secure OAuth credential management. The Canva API requires server-side token exchange — client secrets and refresh tokens must never reach the browser.
Prerequisites
- Canva OAuth credentials (client ID + secret)
- Platform CLI installed (vercel, fly, or gcloud)
- HTTPS domain for OAuth redirect URIs
- Application code ready for deployment
Instructions
- Deploy an immutable, reviewed artifact with OAuth credentials injected only at runtime and redirect URIs exactly matching approved configuration.
- Verify a read-only health path and synthetic-asset flow before enabling exports, writes, or publication actions.
- Promote gradually under rate/cost limits with a tested rollback path; keep deployment receipts redacted.
Vercel
Secrets
# Add Canva OAuth credentials
vercel env add CANVA_CLIENT_ID production
vercel env add CANVA_CLIENT_SECRET production
vercel env add CANVA_REDIRECT_URI production # e.g. https://your-app.vercel.app/auth/canva/callback
vercel.json
{
"functions": {
"api/**/*.ts": {
"maxDuration": 30
}
},
"headers": [
{
"source": "/api/(.*)",
"headers": [
{ "key": "Cache-Control", "value": "no-store" }
]
}
]
}
API Route (Next.js / Vercel Functions)
// api/canva/callback.ts — OAuth callback
export async function GET(req: Request) {
const url = new URL(req.url);
const code = url.searchParams.get('code');
const state = url.searchParams.get('state');
// Exchange code for tokens (server-side only)
const tokens = await exchangeCodeForToken({
code: code!,
codeVerifier: await getVerifierFromSession(state!),
clientId: process.env.CANVA_CLIENT_ID!,
clientSecret: process.env.CANVA_CLIENT_SECRET!,
redirectUri: process.env.CANVA_REDIRECT_URI!,
});
// Store tokens in your database
await saveTokens(userId, tokens);
return Response.redirect('/dashboard');
}
Fly.io
fly.toml
app = "my-canva-app"
primary_region = "iad"
[env]
NODE_ENV = "production"
CANVA_REDIRECT_URI = "https://my-canva-app.fly.dev/auth/canva/callback"
[http_service]
internal_port = 3000
force_https = true
auto_stop_machines = true
auto_start_machines = true
Secrets
fly secrets set CANVA_CLIENT_ID=OCAxxxxxxxxxxxxxxxx
fly secrets set CANVA_CLIENT_SECRET=xxxxxxxxxxxxxxxx
fly deploy
Google Cloud Run
Deploy Script
Configure Canva Enterprise organization access control and scope management.
ReadWriteEdit
Canva Enterprise RBAC
Overview
Manage access control for Canva Connect API integrations at the organization level. The Canva API uses OAuth scopes (not roles) — your application layer implements RBAC on top of Canva's scope system.
Prerequisites
- An external identity source, role/policy owner, protected mapping of subjects to approved tenants/assets/actions, and audit-retention policy.
- Enterprise entitlement confirmation where a feature requires it; folder/team conventions alone are not authorization.
Instructions
- Authenticate and authorize every action in the application layer before invoking Canva; default-deny unknown roles, tenants, assets, and operations.
- Separate read, edit, export, publish, and delete approvals, and require elevated review for destructive or externally visible actions.
- Audit redacted allow/deny results and regularly test revocation; OAuth consent and Enterprise membership do not replace application authorization.
Canva Enterprise Requirements
| Feature |
Canva Free/Pro |
Canva Enterprise |
| Design Create/Read |
Yes |
Yes |
| Export Designs |
Yes |
Yes |
| Asset Upload |
Yes |
Yes |
| Brand Templates |
No |
Yes |
| Autofill API |
No |
Yes |
| Folders (advanced) |
Limited |
Yes |
| Comments API |
Yes |
Yes |
Key: Autofill and brand template APIs require the user to be a member of a Canva Enterprise organization.
Application-Level RBAC
// Your application controls what each user role can do with Canva
interface CanvaRole {
name: string;
scopes: string[]; // OAuth scopes to request
allowedOperations: string[]; // Application-level operations
}
const CANVA_ROLES: Record<string, CanvaRole> = {
viewer: {
name: 'Viewer',
scopes: ['design:meta:read'],
allowedOperations: ['listDesigns', 'getDesign'],
},
creator: {
name: 'Creator',
scopes: ['design:meta:read', 'design:content:write', 'design:content:read', 'asset:write', 'asset:read'],
allowedOperations: ['listDesigns', 'getDesign', 'createDesign', 'exportDesign', 'uploadAsset'],
},
admin: {
name: 'Admin',
scopes: [
'design:meta:read', 'design:content:write', 'design:content:read',
'asset:write', 'asset:read',
'brandtemplate:meta:read', 'brandtemplate:content:read',
'folder:read', 'folder:write',
'comment:read', 'c
Create a minimal working Canva Connect API example.
ReadWriteEdit
Canva Hello World
Overview
Minimal working example: authenticate, get user profile, create a design, and export it as PNG. All via the Canva Connect REST API at api.canva.com/rest/v1/*.
Prerequisites
- Completed
canva-install-auth — valid OAuth access token
- Scopes enabled:
design:meta:read, design:content:write, design:content:read
Instructions
Step 1: Create a Reusable API Helper
// src/canva/client.ts
const CANVA_BASE = 'https://api.canva.com/rest/v1';
export async function canvaAPI(
path: string,
accessToken: string,
options: RequestInit = {}
): Promise<any> {
const res = await fetch(`${CANVA_BASE}${path}`, {
...options,
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json',
...options.headers,
},
});
if (!res.ok) {
const body = await res.text();
throw new Error(`Canva API ${res.status}: ${body}`);
}
return res.status === 204 ? null : res.json();
}
Step 2: Get Your User Profile
// GET /v1/users/me — no scopes required, rate limit: 10 req/min
const me = await canvaAPI('/users/me', accessToken);
console.log(`User ID: ${me.team_user.user_id}`);
console.log(`Team ID: ${me.team_user.team_id}`);
Step 3: Create a Design
// POST /v1/designs — scope: design:content:write, rate limit: 20 req/min
const design = await canvaAPI('/designs', accessToken, {
method: 'POST',
body: JSON.stringify({
design_type: { type: 'preset', name: 'presentation' },
title: 'Hello Canva API',
}),
});
console.log(`Design created: ${design.design.id}`);
console.log(`Edit URL: ${design.design.urls.edit_url}`); // expires in 30 days
console.log(`View URL: ${design.design.urls.view_url}`); // expires in 30 days
Step 4: Export the Design as PNG
// POST /v1/exports — scope: design:content:read, rate limit: 20 req/min
const exportJob = await canvaAPI('/exports', accessToken, {
method: 'POST',
body: JSON.stringify({
design_id: design.design.id,
format: { type: 'png', transparent_background: false },
}),
});
// Poll for completion — GET /v1/exports/{jobId}
let job = exportJob.job;
while (job.status === 'in_progress') {
await new Promise(r => setTimeout(r, 2000));
const poll = await canvaAPI(`/exports/${job.id}`, accessToken);
job = poll.job;
}
if (job.status === 'success') {
console.log('Download URLs (valid 24 hours):');
job.urls.forEach((url: string, i: number) => console.log(` Page ${i + 1}: ${url}`));
} else {
console.error('Export failed:', job.error);
}
Execute Canva Connect API incident response with triage, mitigation, and postmortem.
ReadGrepBash(kubectl:*)Bash(curl:*)
Canva Incident Runbook
Overview
Rapid incident response for Canva Connect API integration failures. Covers triage, mitigation, escalation, and postmortem.
Prerequisites
- A named incident owner, access to redacted application telemetry, and an approved communication/escalation path.
- A paused or bounded mutation/export queue and a known rollback configuration.
Instructions
- Stop affected writes/exports, capture only redacted status and opaque request identifiers, and classify authorization, provider, webhook, or application failure.
- Check approved health/status signals without placing tokens, design IDs, URLs, or content in tickets or shell history.
- Apply the least-invasive supported mitigation, reconcile pending operations, and obtain owner approval before resuming the queue.
- Record the incident timeline, scope, rollback/recovery result, and preventive action in the protected incident record.
Quick Triage (First 5 Minutes)
#!/bin/bash
# canva-triage.sh — Run immediately when incident detected
echo "=== Canva Triage ==="
# 1. Is it Canva or us?
echo -n "Canva API: "
curl -s -o /dev/null -w "HTTP %{http_code} (%{time_total}s)\n" \
-H "Authorization: Bearer $CANVA_ACCESS_TOKEN" \
"https://api.canva.com/rest/v1/users/me"
# 2. Check our health endpoint
echo -n "Our health: "
curl -s -o /dev/null -w "HTTP %{http_code}\n" \
"https://api.ourapp.com/health"
# 3. Error rate (if Prometheus available)
echo "Error rate (5min):"
curl -s "localhost:9090/api/v1/query?query=rate(canva_api_errors_total[5m])" \
| python3 -c "import sys,json; d=json.load(sys.stdin); print(d['data']['result'])" 2>/dev/null \
|| echo "Prometheus not available"
# 4. Rate limit status
echo -n "Rate limit remaining: "
curl -sD - -o /dev/null -H "Authorization: Bearer $CANVA_ACCESS_TOKEN" \
"https://api.canva.com/rest/v1/designs?limit=1" 2>&1 \
| grep -i "x-ratelimit-remaining" || echo "unknown"
Decision Tree
API returning errors?
├── YES → What HTTP status?
│ ├── 401 → Token expired → Refresh token, check rotation
│ ├── 403 → Scope issue → Verify integration permissions
│ ├── 429 → Rate limited → Enable backoff, check Retry-After
│ ├── 5xx → Canva outage → Enable fallback, monitor status page
│ └── Other → Check request format against API docs
└── NO → Is our integration healthy?
├── YES → Likely resolved or intermittent → Monitor
└── NO → Check our infra (pods, memory, DNS, TLS)
Severity Levels
| Level |
Definition |
Response Time |
Example |
| P1 |
All design operations
Set up Canva Connect API OAuth 2.
ReadWriteEditBash(npm:*)Bash(pnpm:*)Bash(npx:*)Grep
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
- Register exact HTTPS redirect URIs, request only approved scopes, and perform PKCE/token exchange on the server.
- Store tokens in the approved encrypted secret/data store, bind them to the authorized user/tenant, and exclude them from logs and clients.
- Verify a read-only synthetic-tenant call first; use the owner-controlled revocation/reauthorization process on any mismatch.
Procedure
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/auth
Identify and avoid Canva Connect API anti-patterns and common integration mistakes.
ReadGrep
Canva Known Pitfalls
Overview
Common mistakes when integrating with the Canva Connect API. Each pitfall includes the anti-pattern, why it fails, and the correct approach with real API endpoints.
Prerequisites
- A reviewed OAuth/asset policy, test tenant, and protected configuration for rate, retention, and publication settings.
- A redacted test/incident record to avoid using customer content as a teaching or reproduction fixture.
Instructions
- Treat every pitfall as a pre-deployment review check against the actual scopes, asset rights, callback flow, and retention policy.
- Correct one control at a time in a synthetic environment, then validate redacted behavior and rollback readiness.
- Escalate unknown authorization, enterprise entitlement, or data-rights questions rather than assuming a workaround is permitted.
Pitfall #1: Not Handling Token Expiry
// WRONG — token expires after ~4 hours, then all calls fail
const token = await getTokenOnce();
// ... 5 hours later ...
await canvaAPI('/designs', token); // 401 Unauthorized
// RIGHT — auto-refresh before expiry
class CanvaClient {
async request(path: string, init?: RequestInit) {
if (Date.now() > this.tokens.expiresAt - 300_000) {
await this.refreshToken(); // Refresh 5 min before expiry
}
// ... make request
}
}
Pitfall #2: Reusing Refresh Tokens
// WRONG — refresh tokens are single-use in Canva's OAuth
const tokens = await refreshAccessToken(storedRefreshToken);
// Later, using the SAME refresh token again:
const tokens2 = await refreshAccessToken(storedRefreshToken); // FAILS
// RIGHT — always store the new refresh token immediately
const tokens = await refreshAccessToken(storedRefreshToken);
await db.saveTokens(userId, {
accessToken: tokens.access_token,
refreshToken: tokens.refresh_token, // NEW token — store it!
expiresAt: Date.now() + tokens.expires_in * 1000,
});
Pitfall #3: Synchronous Export Polling in Request Handler
// WRONG — user waits 5-30 seconds while export completes
app.post('/api/export', async (req, res) => {
const { job } = await canvaAPI('/exports', token, { method: 'POST', body: ... });
while (job.status === 'in_progress') { // Blocks entire request
await sleep(2000);
// ... poll ...
}
res.json({ urls: job.urls }); // User waited 15+ seconds
});
// RIGHT — return job ID, poll asynchronously
app.post('/api/export', async (req, res) => {
const { job } = await canvaAPI('/exports', token, { method: 'POST', body: ... });
res.json({ jobId: job.id, status: 'processing' }); // 200ms response
});
app.get('/api/export/:jobId/status', async (req, res) => {
const { job } = await
Implement Canva Connect API load testing, auto-scaling, and capacity planning.
ReadWriteEditBash(k6:*)Bash(kubectl:*)
Canva Load & Scale
Overview
Load test and scale Canva Connect API integrations. Since Canva enforces per-user rate limits, scaling means distributing load across users, not increasing per-user throughput.
Prerequisites
- Written authorization for load testing, a protected non-production tenant/assets, and current provider/account limits.
- A fixed budget, concurrency ceiling, abort threshold, and incident owner.
Instructions
- Use synthetic assets and least-privilege test identities; never spread load across users or integrations to circumvent provider limits.
- Start below the account limit, increase one dimension at a time, and stop automatically at error/rate/cost thresholds.
- Record aggregate results and reconcile all created jobs/artifacts before ending the test.
Canva Rate Limit Constraints
| Operation |
Per-User Limit |
Implication |
| Create design |
20/min |
Max 1,200 designs/hr per user |
| List designs |
100/min |
Generous for reads |
| Create export |
75/5min (500/24hr) |
Max 500 exports/day per user |
| Integration export |
750/5min (5,000/24hr) |
Shared across all users |
| Upload asset |
30/min |
Max 1,800/hr per user |
| Autofill |
60/min |
Max 3,600/hr per user |
Key insight: The integration-wide export limit of 5,000/day across ALL users is the most constraining for high-volume scenarios.
k6 Load Test
// canva-load-test.js
import http from 'k6/http';
import { check, sleep } from 'k6';
import { Rate, Trend } from 'k6/metrics';
const errorRate = new Rate('canva_error_rate');
const exportDuration = new Trend('canva_export_duration');
export const options = {
scenarios: {
design_operations: {
executor: 'ramping-vus',
startVUs: 1,
stages: [
{ duration: '1m', target: 5 }, // Ramp up slowly
{ duration: '3m', target: 5 }, // Steady state
{ duration: '1m', target: 10 }, // Test rate limits
{ duration: '3m', target: 10 }, // Sustained load
{ duration: '1m', target: 0 }, // Ramp down
],
},
},
thresholds: {
http_req_duration: ['p(95)<2000'], // P95 < 2s
canva_error_rate: ['rate<0.05'], // < 5% errors
canva_export_duration: ['p(95)<30000'], // Exports < 30s
},
};
const BASE = 'https://api.canva.com/rest/v1';
const TOKEN = __ENV.CANVA_ACCESS_TOKEN;
const headers = {
'Authorization': `Bearer ${TOKEN}`,
'Content-Type': 'application/j
Configure Canva Connect API local development with hot reload and mock server.
ReadWriteEditBash(npm:*)Bash(pnpm:*)Grep
Canva Local Dev Loop
Overview
Set up a fast local development environment for Canva Connect API integrations with a token management server, mock API for testing, and hot reload.
Prerequisites
- Completed
canva-install-auth setup
- Node.js 18+ with npm/pnpm
- ngrok or similar tunnel for OAuth callbacks (or use localhost with Canva dev settings)
Instructions
Step 1: Project Structure
my-canva-app/
├── src/
│ ├── canva/
│ │ ├── client.ts # REST client wrapper (from canva-sdk-patterns)
│ │ ├── auth.ts # OAuth PKCE flow
│ │ └── types.ts # API response types
│ ├── routes/
│ │ ├── auth.ts # OAuth callback handler
│ │ └── designs.ts # Design CRUD routes
│ └── index.ts
├── tests/
│ ├── canva-mock.ts # Mock Canva API server
│ └── designs.test.ts
├── .env.local # Local secrets (git-ignored)
├── .env.example # Template for team
├── package.json
└── tsconfig.json
Step 2: Environment Setup
# .env.example
CANVA_CLIENT_ID=
CANVA_CLIENT_SECRET=
CANVA_REDIRECT_URI=http://localhost:3000/auth/canva/callback
PORT=3000
# Copy and fill in
cp .env.example .env.local
Step 3: OAuth Callback Server
// src/routes/auth.ts
import express from 'express';
import { generatePKCE, getAuthorizationUrl, exchangeCodeForToken } from '../canva/auth';
const router = express.Router();
const pkceStore = new Map<string, string>(); // state → verifier
router.get('/auth/canva/start', (req, res) => {
const { verifier, challenge } = generatePKCE();
const state = crypto.randomUUID();
pkceStore.set(state, verifier);
const url = getAuthorizationUrl({
clientId: process.env.CANVA_CLIENT_ID!,
redirectUri: process.env.CANVA_REDIRECT_URI!,
scopes: ['design:content:write', 'design:content:read', 'design:meta:read', 'asset:write'],
codeChallenge: challenge,
state,
});
res.redirect(url);
});
router.get('/auth/canva/callback', async (req, res) => {
const { code, state } = req.query as { code: string; state: string };
const verifier = pkceStore.get(state);
if (!verifier) return res.status(400).send('Invalid state');
pkceStore.delete(state);
const tokens = await exchangeCodeForToken({
code,
codeVerifier: verifier,
clientId: process.env.CANVA_CLIENT_ID!,
clientSecret: process.env.CANVA_CLIENT_SECRET!,
redirectUri: process.env.CANVA_REDIRECT_URI!,
});
// Store tokens securely (database in production, file for dev)
console.log('Access token received, expires in', tokens.expires_in, 'seconds');
res.send('Authenticated! You can close this tab.');
});
Step 4: Hot Reload Config
Execute major Canva Connect API integration migrations with strangler fig pattern.
ReadWriteEditBash(npm:*)Bash(node:*)Bash(kubectl:*)
Canva Migration Deep Dive
Overview
Comprehensive guide for migrating to the Canva Connect API from another design platform or from direct image generation. Uses the strangler fig pattern for gradual, safe migration.
Prerequisites
- A migration owner, approved source/destination asset inventory, encrypted backup, retention policy, and rollback plan.
- A synthetic pilot corpus and a durable manifest mapping authorized source records to destination references.
Instructions
- Run extract, transform, and Canva import/export as separate, inspectable phases; do not stream unreviewed assets into production.
- Validate rights, data classification, template/input schema, and destination scope before each bounded batch.
- Reconcile manifests, obtain owner approval between batches, and stop on content, entitlement, authorization, or fidelity gaps.
- Retain protected artifacts only under the migration policy and keep the rollback path executable.
Migration Types
| Type |
Duration |
Risk |
Example |
| Fresh integration |
Days |
Low |
New app adding Canva support |
| From image gen APIs |
2-4 weeks |
Medium |
Replace Imgix/Cloudinary templates with Canva |
| From competitor |
4-8 weeks |
Medium |
Replace Figma API / Adobe Express |
| Major re-architecture |
Months |
High |
Rebuild design system on Canva |
Pre-Migration Assessment
Asset Inventory
interface MigrationAssessment {
currentAssets: number; // Images, templates in old system
designTemplates: number; // Templates to recreate as Canva brand templates
apiCallsPerDay: number; // Current design API usage
usersToMigrate: number; // Users who need Canva OAuth
requiredCanvaTier: 'free' | 'pro' | 'enterprise';
blockers: string[];
}
async function assessMigration(): Promise<MigrationAssessment> {
return {
currentAssets: await countCurrentAssets(),
designTemplates: await countTemplates(),
apiCallsPerDay: await getAverageApiCalls(),
usersToMigrate: await countActiveUsers(),
requiredCanvaTier: needsAutofill() ? 'enterprise' : 'free',
blockers: [
// Common blockers:
// - Need Enterprise for brand template autofill
// - Rate limits may be too low for current volume
// - No batch API — must process designs one at a time
],
};
}
Canva API Capability Mapping
// Map your current operations to Canva Connect API endpoints
const operationMapping = {
// Old system → Canva endpoint
'createFromTemplate': 'POST /v1
Configure Canva Connect API across development, staging, and production environments.
ReadWriteEditBash(aws:*)Bash(gcloud:*)Bash(vault:*)
Canva Multi-Environment Setup
Overview
Configure Canva Connect API integrations across development, staging, and production. Each environment needs separate OAuth integrations registered in the Canva developer portal with distinct redirect URIs.
Prerequisites
- Separate reviewed OAuth clients, redirect URIs, secret stores, tenant/asset scopes, and owners for each environment.
- Synthetic development/staging assets and a deployment process that cannot silently select production configuration.
Instructions
- Resolve the environment from an explicit allowlist and fail if its OAuth client, redirect URI, or scope configuration is absent or mismatched.
- Keep tokens, caches, queues, and webhook endpoints isolated by environment; never share production credentials with lower environments.
- Verify staging with synthetic assets, then promote immutable configuration through the reviewed release path.
Environment Strategy
| Environment |
Canva Integration |
Redirect URI |
Data |
| Development |
my-app-dev |
http://localhost:3000/auth/canva/callback |
Test account |
| Staging |
my-app-staging |
https://staging.myapp.com/auth/canva/callback |
Staging account |
| Production |
my-app-prod |
https://myapp.com/auth/canva/callback |
Real users |
Important: Register a separate Canva integration per environment. Each gets its own client ID and secret.
Configuration
// src/config/canva.ts
interface CanvaEnvConfig {
clientId: string;
clientSecret: string;
redirectUri: string;
baseUrl: string; // Always api.canva.com — Canva has no sandbox API
scopes: string[];
debug: boolean;
}
const configs: Record<string, CanvaEnvConfig> = {
development: {
clientId: process.env.CANVA_CLIENT_ID!,
clientSecret: process.env.CANVA_CLIENT_SECRET!,
redirectUri: 'http://localhost:3000/auth/canva/callback',
baseUrl: 'https://api.canva.com/rest/v1', // No sandbox exists
scopes: ['design:content:write', 'design:content:read', 'design:meta:read', 'asset:write', 'asset:read'],
debug: true,
},
staging: {
clientId: process.env.CANVA_CLIENT_ID!,
clientSecret: process.env.CANVA_CLIENT_SECRET!,
redirectUri: process.env.CANVA_REDIRECT_URI!,
baseUrl: 'https://api.canva.com/rest/v1',
scopes: ['design:content:write', 'design:content:read', 'design:meta:read', 'asset:write', 'asset:read'],
debug: false,
},
production: {
clientId: process.env.CANVA_CLIENT_ID!,
clientSecret: pro
Set up observability for Canva Connect API integrations with metrics, traces, and alerts.
ReadWriteEdit
Canva Observability
Overview
Instrument Canva Connect API calls with metrics, traces, and structured logging. Track latency, error rates, rate limit headroom, and export job completion times.
Prerequisites
- A protected telemetry destination, retention policy, and alert owner.
- A documented metric allowlist that excludes OAuth values, design contents, signed URLs, and personal identifiers.
Instructions
- Emit aggregate endpoint/status/latency metrics and opaque request identifiers only.
- Redact errors before logs/traces, bound log retention, and restrict dashboards to authorized operators.
- Alert on sustained authorization, rate, export, or webhook failures and link to the incident runbook.
- Do not automate retries or scope changes from a metric alone; reconcile through the protected operation ledger.
Key Metrics
| Metric |
Type |
Labels |
Description |
canva_api_requests_total |
Counter |
method, endpoint, status |
Total API calls |
canva_api_duration_seconds |
Histogram |
method, endpoint |
Request latency |
canva_api_errors_total |
Counter |
endpoint, error_code |
Error count |
canva_export_duration_seconds |
Histogram |
format |
Export completion time |
canva_token_refresh_total |
Counter |
status |
Token refresh attempts |
canva_rate_limit_remaining |
Gauge |
endpoint |
Rate limit headroom |
Prometheus Instrumentation
import { Registry, Counter, Histogram, Gauge } from 'prom-client';
const registry = new Registry();
const requestCounter = new Counter({
name: 'canva_api_requests_total',
help: 'Total Canva Connect API requests',
labelNames: ['method', 'endpoint', 'status'],
registers: [registry],
});
const requestDuration = new Histogram({
name: 'canva_api_duration_seconds',
help: 'Canva API request duration',
labelNames: ['method', 'endpoint'],
buckets: [0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10],
registers: [registry],
});
const rateLimitGauge = new Gauge({
name: 'canva_rate_limit_remaining',
help: 'Remaining rate limit for endpoint',
labelNames: ['endpoint'],
registers: [registry],
});
const exportDuration = new Histogram({
name: 'canva_export_duration_seconds',
help: 'Time from export request to completion',
labelNa
Optimize Canva Connect API performance with caching, pagination, and connection pooling.
ReadWriteEdit
Canva Performance Tuning
Overview
Optimize Canva Connect API performance. The REST API at api.canva.com/rest/v1/* has per-user rate limits and async operations (exports, uploads, autofills) that require polling.
Prerequisites
- A protected non-production benchmark workload, current account-limit evidence, and defined latency/freshness/reconciliation budgets.
- A feature flag and rollback plan for every cache, polling, batch, or concurrency change.
Instructions
- Measure a bounded synthetic workload before changing one performance variable.
- Minimize requested/persisted fields, encrypt and expire caches, and never cache OAuth values, signed URLs, or unapproved asset data.
- Respect provider limits, preserve idempotency/reconciliation state, and roll back on policy, freshness, or completion regression.
Caching Strategy
Design Metadata Cache
import { LRUCache } from 'lru-cache';
// Design metadata changes infrequently — cache aggressively
const designCache = new LRUCache<string, any>({
max: 500,
ttl: 5 * 60 * 1000, // 5 minutes
});
async function getDesignCached(designId: string, token: string) {
const cached = designCache.get(designId);
if (cached) return cached;
const data = await canvaAPI(`/designs/${designId}`, token);
designCache.set(designId, data);
return data;
}
// IMPORTANT: Do NOT cache these — they expire quickly:
// - Thumbnail URLs: expire in 15 minutes
// - Edit/view URLs: expire in 30 days
// - Export download URLs: expire in 24 hours
Redis Cache for Distributed Systems
import Redis from 'ioredis';
const redis = new Redis(process.env.REDIS_URL);
async function cachedCanvaCall<T>(
key: string,
fetcher: () => Promise<T>,
ttlSeconds = 300
): Promise<T> {
const cached = await redis.get(key);
if (cached) return JSON.parse(cached);
const result = await fetcher();
await redis.setex(key, ttlSeconds, JSON.stringify(result));
return result;
}
// Cache brand template list — rarely changes
const templates = await cachedCanvaCall(
'canva:brand-templates:list',
() => canvaAPI('/brand-templates', token),
3600 // 1 hour
);
Pagination Optimization
// Canva uses continuation-based pagination
async function* paginateDesigns(
token: string,
opts: { ownership?: string; limit?: number } = {}
): AsyncGenerator<any> {
let continuation: string | undefined;
do {
const params = new URLSearchParams({
limit: String(opts.limit || 100), // Max 100 per page
...(opts.ownership && { ownership: opts.ownership }),
...(continuation && { continuation }),
});
const data = await canvaAPI(`/designs?${params}`, token);
for (co
Implement Canva Connect API lint rules, policy enforcement, and automated guardrails.
ReadWriteEditBash(npx:*)
Canva Policy & Guardrails
Overview
Automated policy enforcement for Canva Connect API integrations — prevent token leaks, enforce rate limit handling, require error handling, and validate OAuth configuration.
Prerequisites
- Named owners for OAuth scopes, tenant access, design/asset rights, publication destinations, and retention policy.
- CI and runtime configuration capable of evaluating policy without exposing live tokens or design content.
Instructions
- Enforce reviewed allowlists for scopes, tenant/action combinations, and destinations before issuing an API request.
- Fail closed when authorization, provenance, data classification, or required policy configuration is unknown.
- Emit only redacted policy decisions and route exceptions through the named approval process.
ESLint Rules
No Hardcoded Credentials
// eslint-rules/no-canva-credentials.js
module.exports = {
meta: {
type: 'problem',
docs: { description: 'Disallow hardcoded Canva OAuth credentials' },
},
create(context) {
return {
Literal(node) {
if (typeof node.value !== 'string') return;
const val = node.value;
// Canva client IDs start with "OCA"
if (/^OCA[A-Za-z0-9]{10,}/.test(val)) {
context.report({ node, message: 'Hardcoded Canva client ID detected. Use environment variable.' });
}
// Canva access tokens start with "cnvat_"
if (/^cnvat_[A-Za-z0-9]{20,}/.test(val)) {
context.report({ node, message: 'Hardcoded Canva access token detected. Use environment variable.' });
}
},
};
},
};
Require Rate Limit Handling
// eslint-rules/require-canva-retry.js
module.exports = {
meta: {
type: 'suggestion',
docs: { description: 'Canva API calls should handle 429 responses' },
},
create(context) {
return {
CallExpression(node) {
// Check for fetch calls to api.canva.com
if (node.callee.name === 'fetch' &&
node.arguments[0]?.value?.includes('api.canva.com')) {
// Check if parent is try-catch or has .catch()
const parent = node.parent;
if (parent.type !== 'AwaitExpression' ||
parent.parent?.type !== 'TryStatement') {
context.report({
node,
message: 'Canva API calls should be wrapped in try-catch with 429 handling',
});
}
}
},
};
},
};
Pre-Commit Hooks
# .pre-commit-config.yaml
repos:
- repo: local
hooks:
- id: canva-no-tokens
name: Check for Canva tokens
entry: bash -c 'git diff --cached
Execute Canva Connect API production deployment checklist and go-live procedures.
ReadBash(kubectl:*)Bash(curl:*)Grep
Canva Production Checklist
Overview
Complete checklist for deploying Canva Connect API integrations to production, covering OAuth configuration, security, error handling, monitoring, and Canva's integration review process.
Prerequisites
- An approved release owner, exact artifact/configuration version, and tested rollback artifact.
- Mocked tests plus a protected integration environment using synthetic or approved non-production assets and least-privilege credentials.
Instructions
- Execute the checklist against the exact production candidate and policy configuration.
- Verify scopes, asset/publication authorization, retention, webhook validation, rate/cost controls, and redacted observability before enabling writes or exports.
- Keep traffic disabled and roll back if any evidence is missing; do not replace controlled tests with live design data.
Pre-Deployment
OAuth & Security
- [ ] Client ID and secret stored in secret manager (not env files)
- [ ] Redirect URIs use HTTPS and match production domains
- [ ] Only required OAuth scopes requested (least privilege)
- [ ] Access tokens stored encrypted at rest
- [ ] Refresh token rotation handled (single-use tokens)
- [ ] Token revocation implemented for user disconnect
- [ ] No client secrets in frontend code
API Integration
- [ ] All API calls use
api.canva.com/rest/v1/* endpoints
- [ ] Rate limits respected with exponential backoff (see
canva-rate-limits)
- [ ] Export polling implemented with timeout (don't poll forever)
- [ ] 429 responses handled with
Retry-After header
- [ ] 401 responses trigger automatic token refresh
- [ ] Error responses parsed and logged (without tokens)
- [ ] Blank designs auto-delete warning handled (7-day window)
- [ ] Export download URLs consumed within 24-hour window
Webhook Security
- [ ] Webhook endpoint uses HTTPS
- [ ] JWK signature verification implemented (see
canva-webhooks-events)
- [ ] Webhook handler returns 200 immediately
- [ ] Heavy processing done asynchronously
- [ ] Idempotency keys prevent duplicate processing
Data Handling
- [ ] No access tokens in log output
- [ ] User design metadata treated as sensitive
- [ ] Temporary URLs (thumbnails, exports) not cached beyond expiry
- [ ] Thumbnail URLs expire in 15 minutes — refresh as needed
- [ ] Edit/view URLs expire in 30 days — regenerate via API
Production Readiness Verification
#!/bin/bash
# canva-prod-verify.sh
echo "=== Canva Production Readiness ==="
# 1. Verify API connectivity from production
HTTP_CODE=$(curl -s -o /dev/null -w
Handle Canva Connect API rate limits with backoff, queuing, and monitoring.
ReadWriteEdit
Canva Rate Limits
Overview
The Canva Connect API enforces per-user, per-endpoint rate limits. Each endpoint has different thresholds. A 429 response means you must wait before retrying.
Prerequisites
- Current account/endpoint limits verified against Canva's authoritative documentation and the integration owner.
- A durable idempotency store, bounded queue, and named owner for budget/rate policy.
Instructions
- Start each approved workload below the current documented/account limit and apply per-user, endpoint, and job-class concurrency ceilings.
- Persist an idempotency key before dispatch, honor
Retry-After, and retry only explicitly transient operations within a bounded budget.
- Pause and reconcile on sustained throttling; never add accounts or clients to evade a limit.
Canva Connect API Rate Limits
| Endpoint |
Method |
Limit |
/v1/users/me |
GET |
10 req/min |
/v1/users/me/profile |
GET |
10 req/min |
/v1/designs |
GET |
100 req/min |
/v1/designs |
POST |
20 req/min |
/v1/designs/{id} |
GET |
100 req/min |
/v1/exports |
POST |
75 req/5min, 500/24hr per user |
/v1/exports (integration) |
POST |
750 req/5min, 5000/24hr |
/v1/exports (per document) |
POST |
75 req/5min |
/v1/asset-uploads |
POST |
30 req/min |
/v1/autofills |
POST |
60 req/min |
/v1/folders |
POST |
20 req/min |
/v1/brand-templates |
GET |
100 req/min |
All limits are per user of your integration unless noted otherwise.
Exponential Backoff with Jitter
async function canvaRequestWithBackoff<T>(
fn: () => Promise<T>,
config = { maxRetries: 5, baseDelayMs: 1000, maxDelayMs: 60000 }
): Promise<T> {
for (let attempt = 0; attempt <= config.maxRetries; attempt++) {
try {
return await fn();
} catch (error: any) {
if (attempt === config.maxRetries) throw error;
// Only retry on 429 or 5xx
const status = error.status || error.response?.status;
if (status !== 429 && (status < 500 || status >= 600)) throw error;
// Honor Retry-After header if present
const retryAfter = error.headers?.get?.('Retry-After');
const delay = retryAfter
? parseInt(retryAfter) * 1000
: Math.min(
Implement Canva Connect API reference architecture with best-practice project layout.
ReadGrep
Canva Reference Architecture
Overview
Production-ready architecture for Canva Connect API integrations. All interactions use the REST API at api.canva.com/rest/v1/* with OAuth 2.0 PKCE authentication.
Prerequisites
- Server-side OAuth application, reviewed scopes, tenant/asset authorization model, and protected secrets/data stores.
- Mocked tests plus a dedicated synthetic-asset integration environment.
Instructions
- Put caller authorization, asset-rights checks, input validation, and idempotency before the Canva adapter.
- Keep OAuth/token exchange and signed URLs server-side, minimize stored/exported fields, and encrypt controlled artifacts.
- Process webhooks through signature verification and durable queues, then recheck policy before side effects.
- Separate liveness from readiness and pause mutations whenever authorization, reconciliation, or provider health is uncertain.
Project Structure
my-canva-integration/
├── src/
│ ├── canva/
│ │ ├── client.ts # REST client wrapper with auto-refresh
│ │ ├── auth.ts # OAuth 2.0 PKCE flow
│ │ ├── types.ts # API request/response TypeScript types
│ │ └── errors.ts # CanvaAPIError class
│ ├── services/
│ │ ├── design.service.ts # Design creation, export, listing
│ │ ├── asset.service.ts # Asset upload and management
│ │ ├── template.service.ts # Brand template autofill (Enterprise)
│ │ └── folder.service.ts # Folder management
│ ├── routes/
│ │ ├── auth.ts # OAuth callback endpoints
│ │ ├── designs.ts # Design CRUD routes
│ │ ├── exports.ts # Export trigger/download routes
│ │ └── webhooks.ts # Webhook receiver
│ ├── middleware/
│ │ ├── auth.ts # Verify user has valid Canva token
│ │ └── rate-limit.ts # Client-side rate limit guard
│ ├── store/
│ │ └── tokens.ts # Encrypted token storage (DB)
│ └── index.ts
├── tests/
│ ├── mocks/
│ │ └── canva-server.ts # MSW mock server
│ ├── unit/
│ │ └── design.service.test.ts
│ └── integration/
│ └── canva-api.test.ts
├── .env.example
└── package.json
Layer Architecture
┌─────────────────────────────────────────┐
│ Routes Layer │
│ (Express/Next.js — HTTP in/out) │
├─────────────────────────────────────────┤
│ Service Layer │
│ (Business logic, caching, validation) │
├─────────────────────────────────────────┤
│ Canva Client Layer │
│ (REST calls, token refresh, retry) │
├─────────────────────────────────────────┤
│ Infrastructure Layer │
│ (Token store, cache, queue) │
└─────────────────────────────────────────┘
Service Layer Pattern
Implement reliability patterns for Canva Connect API — circuit breakers, idempotency, graceful degradation.
ReadWriteEdit
Canva Reliability Patterns
Overview
Production-grade reliability patterns for the Canva Connect API. The API has async operations (exports, uploads, autofills) that can fail or timeout, OAuth tokens that expire every 4 hours, and rate limits that require backoff.
Prerequisites
- Defined SLOs, queue ownership, idempotency storage, and a protected configuration for timeouts, backoff, and circuit limits.
- A synthetic or approved non-production workload for exercising failure paths without exposing design assets.
Instructions
- Bound every asynchronous operation with an idempotency key, timeout, retry budget, and redacted outcome record.
- Retry only explicitly transient requests; treat authorization, policy, schema, and asset-rights errors as fail-closed.
- Open circuits and pause dependent queues on sustained failure, then reconcile before recovery.
- Change reliability thresholds behind a rollback flag and measure aggregate results before rollout.
Circuit Breaker
import CircuitBreaker from 'opossum';
const canvaBreaker = new CircuitBreaker(
async (fn: () => Promise<any>) => fn(),
{
timeout: 30000, // 30s before failure
errorThresholdPercentage: 50, // Open after 50% failure rate
resetTimeout: 60000, // Try again after 60s
volumeThreshold: 5, // Min 5 requests before evaluating
}
);
canvaBreaker.on('open', () => {
console.warn('[canva] Circuit OPEN — Canva API unreachable, failing fast');
});
canvaBreaker.on('halfOpen', () => {
console.info('[canva] Circuit HALF-OPEN — testing Canva recovery');
});
canvaBreaker.on('close', () => {
console.info('[canva] Circuit CLOSED — Canva API recovered');
});
// Usage
async function createDesignSafe(body: object, token: string) {
return canvaBreaker.fire(async () => {
return canvaAPI('/designs', token, {
method: 'POST',
body: JSON.stringify(body),
});
});
}
Graceful Degradation
// When Canva is down, degrade gracefully instead of breaking the entire app
async function getDesignWithFallback(
designId: string,
token: string,
cache: LRUCache<string, any>
): Promise<{ data: any; source: 'live' | 'cache' | 'placeholder' }> {
try {
const data = await canvaBreaker.fire(async () =>
canvaAPI(`/designs/${designId}`, token)
);
cache.set(designId, data);
return { data, source: 'live' };
} catch {
// Try cached version
const cached = cache.get(designId);
if (cached) {
return { data: cached, source: 'cache' };
}
// Return placeholder
return {
data: {
design: {
id: designId,
title: 'Des
Apply production-ready Canva Connect API client patterns for TypeScript and Python.
ReadWriteEdit
Canva SDK Patterns
Overview
Production-ready patterns for wrapping the Canva Connect REST API. There is no official SDK — all integrations use fetch against api.canva.com/rest/v1/* with OAuth Bearer tokens. These patterns add automatic token refresh, retry logic, type safety, and multi-tenant support.
Prerequisites
- Completed
canva-install-auth setup
- Understanding of OAuth 2.0 token lifecycle
- TypeScript 5+ project (or Python 3.10+)
Instructions
- Resolve OAuth credentials and tenant context only on the server, then validate the caller's permission for each design or asset action.
- Use bounded retries only for explicitly transient, idempotent requests and retain an opaque operation key for reconciliation.
- Validate responses before storage or downstream side effects, minimize persisted fields, and never log access tokens or signed asset URLs.
Pattern 1: Type-Safe Client with Auto Token Refresh
// src/canva/client.ts
interface CanvaTokens {
accessToken: string;
refreshToken: string;
expiresAt: number; // Unix ms
}
interface CanvaClientConfig {
clientId: string;
clientSecret: string;
tokens: CanvaTokens;
onTokenRefresh?: (tokens: CanvaTokens) => Promise<void>; // Persist new tokens
}
export class CanvaClient {
private static BASE = 'https://api.canva.com/rest/v1';
private tokens: CanvaTokens;
constructor(private config: CanvaClientConfig) {
this.tokens = config.tokens;
}
async request<T = any>(path: string, init: RequestInit = {}): Promise<T> {
// Auto-refresh if token expires within 5 minutes
if (Date.now() > this.tokens.expiresAt - 300_000) {
await this.refreshToken();
}
const res = await fetch(`${CanvaClient.BASE}${path}`, {
...init,
headers: {
'Authorization': `Bearer ${this.tokens.accessToken}`,
'Content-Type': 'application/json',
...init.headers,
},
});
if (res.status === 401) {
await this.refreshToken();
return this.request(path, init); // Retry once after refresh
}
if (!res.ok) {
const body = await res.text();
throw new CanvaAPIError(res.status, body, path);
}
return res.status === 204 ? (null as T) : res.json();
}
private async refreshToken(): Promise<void> {
const basicAuth = Buffer.from(
`${this.config.clientId}:${this.config.clientSecret}`
).toString('base64');
const res = await fetch(`${CanvaClient.BASE}/oauth/token`, {
method: 'POST',
headers: {
'Authorization': `Basic ${basicAuth}`,
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
grant_type: 'refresh_token',
refresh_toke
Apply Canva Connect API security best practices for OAuth tokens and access control.
ReadWriteGrep
Canva Security Basics
Overview
Security best practices for Canva Connect API OAuth 2.0 tokens, client credentials, and webhook verification. The Canva API uses OAuth with PKCE — there are no static API keys.
Prerequisites
- A reviewed OAuth client, secret manager, asset/data classification, and incident/rotation owner.
- Server-side authorization controls and protected redacted telemetry.
Instructions
- Keep client secrets, refresh/access tokens, and signed URLs server-side in the approved secret/data stores only.
- Apply minimum scopes, validate webhook signatures before parsing, and recheck asset rights before downstream side effects.
- Redact secrets and design data from logs, rotate/revoke through the owner-controlled procedure, and fail closed on unknown scope or provenance.
Token Security
Never Expose Client Secrets
# .env (NEVER commit)
CANVA_CLIENT_ID=OCAxxxxxxxxxxxxxxxx
CANVA_CLIENT_SECRET=xxxxxxxxxxxxxxxx
# .gitignore — mandatory entries
.env
.env.local
.env.*.local
// WRONG — client-side JavaScript can't safely hold secrets
// Token exchange and refresh MUST happen server-side
// "Requests that require authenticating with your client ID and
// client secret can't be made from a web-browser client" — Canva docs
Token Storage
// Store tokens encrypted at rest — they grant access to user's Canva account
interface SecureTokenStore {
save(userId: string, tokens: {
accessToken: string; // Valid ~4 hours
refreshToken: string; // Single-use — always save the latest
expiresAt: number;
}): Promise<void>;
get(userId: string): Promise<CanvaTokens | null>;
delete(userId: string): Promise<void>;
}
// Production: use your database with encryption
// Never store tokens in: localStorage, cookies without httpOnly, log files, git
Token Revocation
// Revoke tokens when user disconnects your integration
async function revokeCanvaToken(token: string, clientId: string, clientSecret: string) {
const basicAuth = Buffer.from(`${clientId}:${clientSecret}`).toString('base64');
await fetch('https://api.canva.com/rest/v1/oauth/revoke', {
method: 'POST',
headers: {
'Authorization': `Basic ${basicAuth}`,
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({ token }),
});
}
Least-Privilege Scopes
// Request ONLY the scopes you need — scopes don't cascade
// e.g., asset:write does NOT grant asset:read
const SCOPE_PROFILES = {
// Read-only integration — view designs and templates
readonly: ['design:meta:read', '
Plan and execute Canva Connect API version upgrades and breaking change detection.
ReadWriteEditBash(npm:*)Bash(git:*)
Canva Upgrade & Migration
Overview
Guide for handling Canva Connect API changes. Canva uses a single REST API version (/rest/v1/) but evolves endpoints over time. Monitor the changelog for breaking changes.
Prerequisites
- A pinned current artifact, tested rollback version, and current official changelog evidence.
- Synthetic or approved non-production assets and a protected OAuth test tenant.
Instructions
- Map the announced change to scopes, endpoints, stored identifiers, and response contracts.
- Test behind a feature flag with only approved synthetic assets and minimum scopes.
- Compare redacted authorization, schema, rate/cost, and publication outcomes before promotion.
- Roll back on a policy, contract, or reconciliation difference.
Known Migrations
Brand Template ID Migration (September 2025)
Canva migrated brand templates to a new ID format. Old IDs accepted for 6 months.
// Check if your stored template IDs need updating
async function migrateBrandTemplateIds(
db: Database, token: string
): Promise<{ migrated: number; failed: string[] }> {
const stored = await db.getBrandTemplateIds();
let migrated = 0;
const failed: string[] = [];
// Fetch current templates from Canva
const { items } = await canvaAPI('/brand-templates', token);
const currentIds = new Set(items.map((t: any) => t.id));
for (const oldId of stored) {
if (!currentIds.has(oldId)) {
// Old ID — try to find matching template by title
const match = items.find((t: any) => t.title === await db.getTemplateName(oldId));
if (match) {
await db.updateTemplateId(oldId, match.id);
migrated++;
} else {
failed.push(oldId);
}
}
}
return { migrated, failed };
}
Comment API Migration
The Comment API endpoints were refactored — Create Comment and Create Reply are deprecated in favor of Create Thread and Create Reply (v2).
// OLD (deprecated)
// POST /v1/designs/{id}/comments — deprecated
// NEW
// POST /v1/designs/{id}/comment_threads — Create Thread
// POST /v1/designs/{id}/comment_threads/{threadId}/replies — Create Reply
Pre-Upgrade Assessment
async function assessCanvaIntegration(token: string): Promise<void> {
const checks = [
{ name: 'Users API', path: '/users/me' },
{ name: 'Designs List', path: '/designs?limit=1' },
{ name: 'Brand Templates', path: '/brand-templates?limit=1' },
{ name: 'Exports', path: '/exports' }, // Will 405 (POST only) but confirms route exists
];
for (const
Implement Canva Connect API webhook handling with JWK signature verification.
ReadWriteEditBash(curl:*)
Canva Webhooks & Events
Overview
Receive real-time notifications from Canva via webhooks when users comment on designs, request folder access, share designs, or interact with your integration. Canva sends signed JWT payloads verified with public JWK keys.
Prerequisites
- Canva integration with
collaboration:event scope enabled
- HTTPS endpoint accessible from the internet
- JWK verification library (
jose recommended)
- Webhook URL configured in your integration settings
Instructions
- Validate the raw signed JWT/JWK issuer, audience, signature, expiry, and replay/idempotency condition before trusting or parsing the event.
- Persist a redacted receipt and durable idempotency key before acknowledgement, then enqueue a worker that rechecks authorization and asset scope before any side effect.
- Keep handlers fast, bounded, and fail closed; never process raw event data or execute publication/access changes directly in the request path.
Setup
Step 1: Configure Webhooks in Canva
- Go to your integration settings at canva.dev
- Under Notifications, enable collaboration:event
- Enter your webhook URL:
https://your-app.com/webhooks/canva
- Save the configuration
Step 2: Implement JWK Signature Verification
// src/canva/webhooks.ts
import { createRemoteJWKSet, jwtVerify, JWTPayload } from 'jose';
// Canva publishes public keys at this endpoint
// GET https://api.canva.com/rest/v1/connect/keys
const CANVA_JWKS = createRemoteJWKSet(
new URL('https://api.canva.com/rest/v1/connect/keys')
);
interface CanvaWebhookPayload extends JWTPayload {
notification_type: string;
notification: Record<string, any>;
timestamp: string;
user_id: string;
team_id: string;
}
export async function verifyCanvaWebhook(
rawBody: string
): Promise<CanvaWebhookPayload | null> {
try {
const { payload } = await jwtVerify(rawBody, CANVA_JWKS, {
issuer: 'canva',
});
return payload as CanvaWebhookPayload;
} catch (error) {
console.error('Webhook verification failed:', error);
return null;
}
}
Step 3: Express Webhook Endpoint
import express from 'express';
import { verifyCanvaWebhook } from './canva/webhooks';
const app = express();
// IMPORTANT: Accept raw text body for JWT verification
app.post('/webhooks/canva',
express.text({ type: '*/*' }),
async (req, res) => {
const payload = await verifyCanvaWebhook(req.body);
if (!payload) {
return res.status(401).json({ error: 'Invalid signature' });
}
// Must return 200 to acknowledge — other codes = error
How It Works
Skills trigger automatically when you discuss Canva topics:
- "Help me set up Canva OAuth" triggers
canva-install-auth
- "Export this design as PDF" triggers
canva-core-workflow-a
- "Autofill a brand template" triggers
canva-core-workflow-b
- "Debug this Canva 429 error" triggers
canva-rate-limits
- "Deploy my Canva integration" triggers
canva-deploy-integration
|
|