> canva-known-pitfalls

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".

fetch
$curl "https://skillshub.wtf/jeremylongshore/claude-code-plugins-plus-skills/canva-known-pitfalls?format=md"
SKILL.mdcanva-known-pitfalls

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 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 canvaAPI(`/exports/${req.params.jobId}`, token);
  res.json({ status: job.status, urls: job.urls });
});

Pitfall #4: Ignoring Rate Limits

// WRONG — blast requests, crash on 429
for (const design of designs) {
  await canvaAPI(`/exports`, token, { method: 'POST', body: ... }); // 75/5min limit
}

// RIGHT — queue with rate awareness
import PQueue from 'p-queue';
const queue = new PQueue({ concurrency: 1, interval: 4000, intervalCap: 1 });

for (const design of designs) {
  await queue.add(() =>
    canvaAPI(`/exports`, token, { method: 'POST', body: ... })
  );
}

Pitfall #5: Caching Temporary URLs

// WRONG — URLs expire silently
const design = await canvaAPI(`/designs/${id}`, token);
cache.set(id, design, { ttl: 86400 }); // Cache for 24 hours
// But thumbnail URLs expire in 15 minutes!

// RIGHT — cache metadata but refresh URLs
const design = await canvaAPI(`/designs/${id}`, token);
cache.set(`design:meta:${id}`, {
  id: design.design.id,
  title: design.design.title,
  pageCount: design.design.page_count,
  // DON'T cache: thumbnail.url (15 min), edit_url (30 days), view_url (30 days)
}, { ttl: 300 }); // 5 min cache

Pitfall #6: Client-Side OAuth

// WRONG — client secret exposed in browser
// frontend.js
const tokens = await fetch('https://api.canva.com/rest/v1/oauth/token', {
  body: new URLSearchParams({
    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 403
const result = await canvaAPI('/autofills', token, { method: 'POST', body: ... });
// 403: "User must be a member of a Canva Enterprise organization"

// RIGHT — check capabilities first
const capabilities = await canvaAPI('/users/me/capabilities', token);
if (!capabilities.capabilities?.includes('autofill')) {
  throw new Error('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 = await verifyCanvaWebhook(req.body); // JWK verification
  if (!payload) return res.status(401).send('Invalid');
  res.status(200).send('OK'); // Return 200 first
  processEvent(payload).catch(console.error); // Process async
});

Pitfall #9: Ignoring Blank Design Auto-Delete

// WRONG — create designs and expect them to persist
const { design } = await canvaAPI('/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 designs
await notifyUser(`Edit your design before ${sevenDaysFromNow}: ${design.urls.edit_url}`);

Pitfall #10: Not Handling Export Failures

// WRONG — assumes exports always succeed
const { job } = await canvaAPI('/exports', token, { method: 'POST', body: ... });
const urls = (await pollExport(job.id)).urls; // Crashes if failed

// RIGHT — handle all export error codes
const result = await pollExport(job.id);
if (result.status === 'failed') {
  switch (result.error?.code) {
    case 'license_required':
      throw new Error('Design uses premium elements — user needs Canva Pro');
    case 'approval_required':
      throw new Error('Design requires approval before export');
    case 'internal_failure':
      // Retry after delay
      break;
  }
}

Quick Reference

PitfallDetectionPrevention
Token expiry401 errors after 4hAuto-refresh before expiry
Reused refresh tokenToken exchange failsStore new token every refresh
Sync export pollingSlow API responsesReturn job ID, poll separately
Rate limit ignored429 errorsQueue with p-queue
Cached expired URLsBroken images/linksDon't cache temp URLs
Client-side OAuthSecurity auditServer-side only
Missing Enterprise check403 on autofillCheck capabilities first
Unsigned webhooksSecurity auditJWK verification
Blank design deletedDesign disappearsWarn about 7-day window
Export error ignoredCrashesHandle all error codes

Resources

┌ stats

installs/wk0
░░░░░░░░░░
github stars1.7K
██████████
first seenMar 23, 2026
└────────────

┌ repo

jeremylongshore/claude-code-plugins-plus-skills
by jeremylongshore
└────────────