agentsclimarketplace

Canva rate limits

Skill jeremylongshore/claude-code-plugins-plus-skills/plugins/saas-packs/canva-pack/skills/canva-rate-limits

'Handle Canva Connect API rate limits with backoff, queuing, and monitoring.From its SKILL.md

Install
npx -y skills add jeremylongshore/claude-code-plugins-plus-skills --skill canva-rate-limits

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

5.7 KB, ~1.5k tokens by cl100k_base, as published. Nobody here has run it

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.

Canva Connect API Rate Limits

EndpointMethodLimit
/v1/users/meGET10 req/min
/v1/users/me/profileGET10 req/min
/v1/designsGET100 req/min
/v1/designsPOST20 req/min
/v1/designs/{id}GET100 req/min
/v1/exportsPOST75 req/5min, 500/24hr per user
/v1/exports (integration)POST750 req/5min, 5000/24hr
/v1/exports (per document)POST75 req/5min
/v1/asset-uploadsPOST30 req/min
/v1/autofillsPOST60 req/min
/v1/foldersPOST20 req/min
/v1/brand-templatesGET100 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(
            config.baseDelayMs * Math.pow(2, attempt) + Math.random() * 1000,
            config.maxDelayMs
          );

      console.warn(`Rate limited (attempt ${attempt + 1}/${config.maxRetries}). Waiting ${(delay / 1000).toFixed(1)}s`);
      await new Promise(r => setTimeout(r, delay));
    }
  }
  throw new Error('Unreachable');
}

Queue-Based Rate Limiting

import PQueue from 'p-queue';

// Match per-user endpoint limits
const canvaQueues = {
  designs: new PQueue({ concurrency: 1, interval: 3000, intervalCap: 1 }),     // ~20/min
  exports: new PQueue({ concurrency: 1, interval: 4000, intervalCap: 1 }),     // ~15/min (conservative)
  assets:  new PQueue({ concurrency: 1, interval: 2000, intervalCap: 1 }),     // ~30/min
  reads:   new PQueue({ concurrency: 3, interval: 1000, intervalCap: 3 }),     // ~100/min (shared reads)
};

// Usage — automatically queued to stay under limits
const design = await canvaQueues.designs.add(() =>
  client.createDesign({ design_type: { type: 'custom', width: 1080, height: 1080 }, title: 'Queued' })
);

// Batch export with rate control
const designIds = ['DAV1', 'DAV2', 'DAV3', 'DAV4', 'DAV5'];
const exports = await Promise.all(
  designIds.map(id =>
    canvaQueues.exports.add(() =>
      client.createExport({ design_id: id, format: { type: 'pdf' } })
    )
  )
);

Rate Limit Monitor

class CanvaRateLimitTracker {
  private windows: Map<string, { count: number; resetAt: number }> = new Map();

  track(endpoint: string, response: Response): void {
    const remaining = response.headers.get('X-RateLimit-Remaining');
    const reset = response.headers.get('X-RateLimit-Reset');

    if (remaining !== null) {
      this.windows.set(endpoint, {
        count: parseInt(remaining),
        resetAt: reset ? parseInt(reset) * 1000 : Date.now() + 60000,
      });
    }
  }

  shouldThrottle(endpoint: string): boolean {
    const window = this.windows.get(endpoint);
    if (!window) return false;
    return window.count < 3 && Date.now() < window.resetAt;
  }

  getWaitMs(endpoint: string): number {
    const window = this.windows.get(endpoint);
    if (!window) return 0;
    return Math.max(0, window.resetAt - Date.now());
  }

  report(): Record<string, { remaining: number; resetsIn: string }> {
    const report: Record<string, any> = {};
    for (const [ep, w] of this.windows) {
      report[ep] = {
        remaining: w.count,
        resetsIn: `${Math.max(0, (w.resetAt - Date.now()) / 1000).toFixed(0)}s`,
      };
    }
    return report;
  }
}

Proactive Throttling

// Wrap the client to throttle before hitting limits
async function throttledCanvaRequest<T>(
  tracker: CanvaRateLimitTracker,
  endpoint: string,
  fn: () => Promise<T>
): Promise<T> {
  if (tracker.shouldThrottle(endpoint)) {
    const waitMs = tracker.getWaitMs(endpoint);
    console.log(`Proactively waiting ${waitMs}ms for ${endpoint}`);
    await new Promise(r => setTimeout(r, waitMs));
  }
  return fn();
}

Error Handling

ScenarioDetectionAction
Single 429HTTP statusWait Retry-After seconds, retry
Sustained 429sMultiple retries failReduce request rate, increase backoff
Export quota hit500/24hr per userQueue exports, spread across hours
Integration quota5000/24hr exportsDistribute across users

Resources

Next Steps

For security configuration, see canva-security-basics.

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.