Canva Reliability Patterns
Overview
Production-grade reliability patterns for the Canva Connect API. The API has async operations (exports, uploads, autofills) that can fail or timeout, OAuth tokens that expire every 4 hours, and rate limits that require backoff.
Circuit Breaker
import CircuitBreaker from 'opossum';
const canvaBreaker = new CircuitBreaker(
async (fn: () => Promise<any>) => fn(),
{
timeout: 30000,
errorThresholdPercentage: 50,
resetTimeout: 60000,
volumeThreshold: 5,
}
);
canvaBreaker.on('open', () => {
console.warn('[canva] Circuit OPEN — Canva API unreachable, failing fast');
});
canvaBreaker.on('halfOpen', () => {
console.info('[canva] Circuit HALF-OPEN — testing Canva recovery');
});
canvaBreaker.on('close', () => {
console.info('[canva] Circuit CLOSED — Canva API recovered');
});
async function createDesignSafe(body: object, token: string) {
return canvaBreaker.fire(async () => {
return canvaAPI('/designs', token, {
method: 'POST',
body: JSON.stringify(body),
});
});
}
Graceful Degradation
async function getDesignWithFallback(
designId: string,
token: string,
cache: LRUCache<string, any>
): Promise<{ data: any; source: 'live' | 'cache' | 'placeholder' }> {
try {
const data = await canvaBreaker.fire(async () =>
canvaAPI(`/designs/${designId}`, token)
);
cache.set(designId, data);
return { data, source: 'live' };
} catch {
const cached = cache.get(designId);
if (cached) {
return { data: cached, source: 'cache' };
}
return {
data: {
design: {
id: designId,
title: 'Design temporarily unavailable',
urls: { edit_url: , : },
},
},
: ,
};
}
}
Async Job Resilience
async function resilientExport(
designId: string,
format: object,
token: string,
maxRetries = 2
): Promise<string[]> {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
const { job } = await canvaAPI('/exports', token, {
method: 'POST',
body: JSON.stringify({ design_id: designId, format }),
});
const urls = await pollWithTimeout(job.id, token, 60000);
return urls;
} catch (error: any) {
if (attempt === maxRetries) throw error;
if (error.status && error.status < 500 && error.status !== 429) throw error;
const delay = * .(, attempt);
.();
( (r, delay));
}
}
();
}
(): <[]> {
deadline = .() + timeoutMs;
(.() < deadline) {
{ job } = (, token);
(job. === ) job.;
(job. === ) ();
( (r, ));
}
();
}
Token Refresh Resilience
async function resilientTokenRefresh(
refreshToken: string,
config: { clientId: string; clientSecret: string }
): Promise<{ accessToken: string; refreshToken: string; expiresAt: number } | null> {
const basicAuth = Buffer.from(`${config.clientId}:${config.clientSecret}`).toString('base64');
for (let attempt = 0; attempt < 3; attempt++) {
try {
const res = await fetch('https://api.canva.com/rest/v1/oauth/token', {
method: 'POST',
headers: {
'Authorization': `Basic ${basicAuth}`,
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
grant_type: 'refresh_token',
refresh_token: refreshToken,
}),
signal: .(),
});
(res.) {
data = res.();
{
: data.,
: data.,
: .() + data. * ,
};
}
(res. === || res. === ) {
.();
;
}
} {
}
( (r, * .(, attempt)));
}
.();
;
}
Dead Letter Queue for Failed Operations
interface FailedOperation {
id: string;
operation: 'export' | 'autofill' | 'upload';
payload: any;
userId: string;
error: string;
attempts: number;
lastAttempt: Date;
}
class CanvaDeadLetterQueue {
constructor(private db: Database) {}
async add(op: Omit<FailedOperation, 'id' | 'lastAttempt'>): Promise<void> {
await this.db.dlq.insert({
...op,
id: crypto.randomUUID(),
lastAttempt: new Date(),
});
}
async processNext(getToken: (userId: string) => Promise<string | null>): <> {
entry = ...({ : { : } });
(!entry) ;
token = (entry.);
(!token) {
.();
;
}
{
.(entry, token);
...(entry.);
;
} {
...(entry., {
: entry. + ,
: (),
});
;
}
}
() {
(entry.) {
: (, token, { : , : .(entry.) });
: (, token, { : , : .(entry.) });
}
}
}
Error Handling
| Issue | Cause | Solution |
|---|
| Circuit stays open | Threshold too low | Increase volumeThreshold |
| Token refresh fails | Single-use refresh token reused | Always store new token |
| Export retries waste quota | Re-starting export | Track export job IDs |
| DLQ growing | Persistent issue | Investigate root cause |
Resources
Next Steps
For policy enforcement, see canva-policy-guardrails.