agentsclimarketplace

Canva known pitfalls

Skill jeremylongshore/claude-code-plugins-plus-skills/plugins/saas-packs/canva-pack/skills/canva-known-pitfalls

'Identify and avoid Canva Connect API anti-patterns and common integration mistakes.From its SKILL.md

Install
npx -y skills add jeremylongshore/claude-code-plugins-plus-skills --skill canva-known-pitfalls

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

What its file declares

Copied from the file, not written here

The file declares its own license as MIT. That is the author’s claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.

SKILL.md

7.5 KB, ~1.8k tokens by cl100k_base, as published. Nobody here has run it

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

What ships with it

Read from the repository

Just SKILL.md. No reference files, no scripts.

Keep looking

Skills are one crate of 326,144. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.