Identify and avoid Canva Connect API anti-patterns and common integration mistakes.
Use when reviewing Canva code, onboarding developers,
or auditing existing Canva integrations for best practices violations.
Trigger with phrases like "canva mistakes", "canva anti-patterns",
"canva pitfalls", "canva what not to do", "canva code review".
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
A direct command skips the review prompt. Inspect the source before running it.
Identify and avoid Canva Connect API anti-patterns and common integration mistakes.
Use when reviewing Canva code, onboarding developers,
or auditing existing Canva integrations for best practices violations.
Trigger with phrases like "canva mistakes", "canva anti-patterns",
"canva pitfalls", "canva what not to do", "canva code review".
allowed-tools
Read, Grep
version
1.5.0
license
MIT
author
Jeremy Longshore <jeremy@intentsolutions.io>
tags
["saas","design","canva"]
compatibility
Designed for Claude Code
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.
Pitfall #1: Not Handling Token Expiry
// WRONG โ token expires after ~4 hours, then all calls failconst token = awaitgetTokenOnce();
// ... 5 hours later ...awaitcanvaAPI('/designs', token); // 401 Unauthorized// RIGHT โ auto-refresh before expiryclassCanvaClient {
asyncrequest(path: string, init?: RequestInit) {
if (Date.now() > this.tokens.expiresAt - 300_000) {
awaitthis.refreshToken(); // Refresh 5 min before expiry
}
// ... make request
}
}
Pitfall #2: Reusing Refresh Tokens
// WRONG โ refresh tokens are single-use in Canva's OAuthconst tokens = awaitrefreshAccessToken(storedRefreshToken);
// Later, using the SAME refresh token again:const tokens2 = awaitrefreshAccessToken(storedRefreshToken); // FAILS// RIGHT โ always store the new refresh token immediately
tokens = (storedRefreshToken);
db.(userId, {
: tokens.,
: tokens.,
: .() + tokens. * ,
});
const
await
refreshAccessToken
await
saveTokens
accessToken
access_token
refreshToken
refresh_token
// NEW token โ store it!
expiresAt
Date
now
expires_in
1000
Pitfall #3: Synchronous Export Polling in Request Handler
// WRONG โ client secret exposed in browser// frontend.jsconst tokens = awaitfetch('https://api.canva.com/rest/v1/oauth/token', {
body: newURLSearchParams({
client_secret: 'EXPOSED_TO_USERS', // Anyone can see this// ...
}),
});
// RIGHT โ token exchange MUST happen server-side// Canva docs: "Requests that require authenticating with your client ID// and client secret can't be made from a web-browser client"
Pitfall #7: Not Checking Enterprise Requirements
// WRONG โ calling autofill without Enterprise, getting 403const result = awaitcanvaAPI('/autofills', token, { method: 'POST', body: ... });
// 403: "User must be a member of a Canva Enterprise organization"// RIGHT โ check capabilities firstconst capabilities = awaitcanvaAPI('/users/me/capabilities', token);
if (!capabilities.capabilities?.includes('autofill')) {
thrownewError('Autofill requires Canva Enterprise subscription');
}
Pitfall #8: Not Validating Webhook Signatures
// WRONG โ accepts any POST as a valid webhook
app.post('/webhooks/canva', (req, res) => {
processEvent(req.body); // Attacker can send fake events!
res.status(200).send();
});
// RIGHT โ verify JWK signature
app.post('/webhooks/canva', express.text({ type: '*/*' }), async (req, res) => {
const payload = awaitverifyCanvaWebhook(req.body); // JWK verificationif (!payload) return res.status(401).send('Invalid');
res.status(200).send('OK'); // Return 200 firstprocessEvent(payload).catch(console.error); // Process async
});
Pitfall #9: Ignoring Blank Design Auto-Delete
// WRONG โ create designs and expect them to persistconst { design } = awaitcanvaAPI('/designs', token, {
method: 'POST',
body: JSON.stringify({ design_type: { type: 'custom', width: 1080, height: 1080 } }),
});
// Design auto-deleted after 7 days if user never edits it!// RIGHT โ warn users or track unedited designsawaitnotifyUser(`Edit your design before ${sevenDaysFromNow}: ${design.urls.edit_url}`);
Pitfall #10: Not Handling Export Failures
// WRONG โ assumes exports always succeedconst { job } = awaitcanvaAPI('/exports', token, { method: 'POST', body: ... });
const urls = (awaitpollExport(job.id)).urls; // Crashes if failed// RIGHT โ handle all export error codesconst result = awaitpollExport(job.id);
if (result.status === 'failed') {
switch (result.error?.code) {
case'license_required':
thrownewError('Design uses premium elements โ user needs Canva Pro');
case'approval_required':
thrownewError('Design requires approval before export');
case'internal_failure':
// Retry after delaybreak;
}
}