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".
Installer avec Codex ou Claude Copiez ce prompt, collez-le dans Codex, Claude ou un autre assistant, puis laissez-le vérifier la page du skill et l'installer pour vous.
Une commande directe contourne le prompt de vérification. Examinez la source avant de l'exécuter.
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;
}
}