Agent Skills: Canva Rate Limits

|

UncategorizedID: jeremylongshore/claude-code-plugins-plus-skills/canva-rate-limits

Install this agent skill to your local

pnpm dlx add-skill https://github.com/jeremylongshore/claude-code-plugins-plus-skills/tree/HEAD/plugins/saas-packs/canva-pack/skills/canva-rate-limits

Skill Files

Browse the full folder contents for canva-rate-limits.

Download Skill

Loading file tree…

plugins/saas-packs/canva-pack/skills/canva-rate-limits/SKILL.md

Skill Metadata

Name
canva-rate-limits
Description
|

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

| Endpoint | Method | Limit | |----------|--------|-------| | /v1/users/me | GET | 10 req/min | | /v1/users/me/profile | GET | 10 req/min | | /v1/designs | GET | 100 req/min | | /v1/designs | POST | 20 req/min | | /v1/designs/{id} | GET | 100 req/min | | /v1/exports | POST | 75 req/5min, 500/24hr per user | | /v1/exports (integration) | POST | 750 req/5min, 5000/24hr | | /v1/exports (per document) | POST | 75 req/5min | | /v1/asset-uploads | POST | 30 req/min | | /v1/autofills | POST | 60 req/min | | /v1/folders | POST | 20 req/min | | /v1/brand-templates | GET | 100 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

| Scenario | Detection | Action | |----------|-----------|--------| | Single 429 | HTTP status | Wait Retry-After seconds, retry | | Sustained 429s | Multiple retries fail | Reduce request rate, increase backoff | | Export quota hit | 500/24hr per user | Queue exports, spread across hours | | Integration quota | 5000/24hr exports | Distribute across users |

Resources

Next Steps

For security configuration, see canva-security-basics.