From canva-pack
Guides Canva Connect API migrations from design platforms using strangler fig pattern. Assesses assets, maps APIs, and implements phased adapter strategy.
How this skill is triggered — by the user, by Claude, or both
Slash command
/canva-pack:canva-migration-deep-diveThis skill is limited to the following tools:
The summary Claude sees in its skill listing — used to decide when to auto-load this skill
Comprehensive guide for migrating to the Canva Connect API from another design platform or from direct image generation. Uses the strangler fig pattern for gradual, safe migration.
Comprehensive guide for migrating to the Canva Connect API from another design platform or from direct image generation. Uses the strangler fig pattern for gradual, safe migration.
| Type | Duration | Risk | Example |
|---|---|---|---|
| Fresh integration | Days | Low | New app adding Canva support |
| From image gen APIs | 2-4 weeks | Medium | Replace Imgix/Cloudinary templates with Canva |
| From competitor | 4-8 weeks | Medium | Replace Figma API / Adobe Express |
| Major re-architecture | Months | High | Rebuild design system on Canva |
interface MigrationAssessment {
currentAssets: number; // Images, templates in old system
designTemplates: number; // Templates to recreate as Canva brand templates
apiCallsPerDay: number; // Current design API usage
usersToMigrate: number; // Users who need Canva OAuth
requiredCanvaTier: 'free' | 'pro' | 'enterprise';
blockers: string[];
}
async function assessMigration(): Promise<MigrationAssessment> {
return {
currentAssets: await countCurrentAssets(),
designTemplates: await countTemplates(),
apiCallsPerDay: await getAverageApiCalls(),
usersToMigrate: await countActiveUsers(),
requiredCanvaTier: needsAutofill() ? 'enterprise' : 'free',
blockers: [
// Common blockers:
// - Need Enterprise for brand template autofill
// - Rate limits may be too low for current volume
// - No batch API — must process designs one at a time
],
};
}
// Map your current operations to Canva Connect API endpoints
const operationMapping = {
// Old system → Canva endpoint
'createFromTemplate': 'POST /v1/autofills', // Requires Enterprise
'generateImage': 'POST /v1/designs + POST /v1/exports',
'uploadAsset': 'POST /v1/asset-uploads',
'listDesigns': 'GET /v1/designs',
'exportAsPDF': 'POST /v1/exports (format: pdf)',
'exportAsPNG': 'POST /v1/exports (format: png)',
'organizeFolder': 'POST /v1/folders',
'addComment': 'POST /v1/designs/{id}/comment_threads',
};
// src/services/design-adapter.ts
// Abstract interface that both old and new systems implement
interface DesignService {
createDesign(input: CreateDesignInput): Promise<Design>;
exportDesign(designId: string, format: ExportFormat): Promise<string[]>;
uploadAsset(file: Buffer, name: string): Promise<string>;
}
// Old implementation
class LegacyDesignService implements DesignService {
async createDesign(input: CreateDesignInput) {
return oldApi.generateImage(input);
}
// ...
}
// New Canva implementation
class CanvaDesignService implements DesignService {
constructor(private canva: CanvaClient) {}
async createDesign(input: CreateDesignInput) {
const { design } = await this.canva.request('/designs', {
method: 'POST',
body: JSON.stringify({
design_type: { type: 'custom', width: input.width, height: input.height },
title: input.title,
}),
});
return { id: design.id, editUrl: design.urls.edit_url };
}
async exportDesign(designId: string, format: ExportFormat) {
const { job } = await this.canva.request('/exports', {
method: 'POST',
body: JSON.stringify({ design_id: designId, format: { type: format } }),
});
return this.pollExport(job.id);
}
async uploadAsset(file: Buffer, name: string) {
const nameBase64 = Buffer.from(name).toString('base64');
const res = await fetch('https://api.canva.com/rest/v1/asset-uploads', {
method: 'POST',
headers: {
'Authorization': `Bearer ${this.canva.getToken()}`,
'Content-Type': 'application/octet-stream',
'Asset-Upload-Metadata': JSON.stringify({ name_base64: nameBase64 }),
},
body: file,
});
const data = await res.json();
return data.job.id;
}
}
// Route traffic based on feature flag
function getDesignService(userId: string): DesignService {
const canvaPercentage = getFeatureFlag('canva_migration_pct', userId);
const roll = deterministicRoll(userId); // Same user always gets same path
if (roll < canvaPercentage) {
const tokens = await tokenStore.get(userId);
if (tokens) {
return new CanvaDesignService(new CanvaClient({ ...config, tokens }));
}
// User hasn't connected Canva yet — fall back to legacy
}
return new LegacyDesignService();
}
// Gradual rollout: 5% → 25% → 50% → 100%
// Migrate existing assets to Canva
async function migrateAssets(
assets: { url: string; name: string }[],
token: string
): Promise<Map<string, string>> {
const idMapping = new Map<string, string>(); // oldId → canvaAssetId
for (const asset of assets) {
try {
// Upload via URL — rate limit: 30/min
const { job } = await canvaAPI('/url-asset-uploads', token, {
method: 'POST',
body: JSON.stringify({ name: asset.name, url: asset.url }),
});
// Poll for completion
let upload = job;
while (upload.status === 'in_progress') {
await new Promise(r => setTimeout(r, 2000));
const poll = await canvaAPI(`/url-asset-uploads/${upload.id}`, token);
upload = poll.job;
}
if (upload.status === 'success') {
idMapping.set(asset.url, upload.asset.id);
}
} catch (error) {
console.error(`Failed to migrate asset: ${asset.name}`, error);
}
// Respect rate limits
await new Promise(r => setTimeout(r, 2500)); // ~24 uploads/min
}
return idMapping;
}
// Final validation before removing legacy system
async function validateMigration(token: string): Promise<{
passed: boolean;
checks: { name: string; result: boolean; details: string }[];
}> {
const checks = [
{
name: 'Design creation',
fn: async () => {
const { design } = await canvaAPI('/designs', token, {
method: 'POST',
body: JSON.stringify({
design_type: { type: 'custom', width: 100, height: 100 },
title: 'Migration validation test',
}),
});
return { result: !!design.id, details: `Design ID: ${design.id}` };
},
},
{
name: 'Export works',
fn: async () => {
// Test with an existing design
return { result: true, details: 'Export endpoint accessible' };
},
},
{
name: 'Rate limits adequate',
fn: async () => {
// Check current usage vs limits
return { result: true, details: 'Within rate limits' };
},
},
];
const results = [];
for (const check of checks) {
try {
const { result, details } = await check.fn();
results.push({ name: check.name, result, details });
} catch (e: any) {
results.push({ name: check.name, result: false, details: e.message });
}
}
return { passed: results.every(r => r.result), checks: results };
}
# Immediate rollback — switch feature flag to 0%
curl -X PUT "https://flagservice.internal/api/flags/canva_migration_pct" \
-d '{"value": 0}'
# Verify legacy system still works
curl -s "https://api.ourapp.com/health" | jq '.services.legacy_design'
| Issue | Cause | Solution |
|---|---|---|
| Asset upload fails | File too large or unsupported format | Pre-validate, compress |
| Rate limit during migration | Too many uploads | Add delays between uploads |
| User hasn't connected Canva | Missing OAuth | Prompt to connect, fallback |
| Feature parity gap | Canva API doesn't support operation | Document, workaround, or defer |
For advanced troubleshooting, see canva-advanced-troubleshooting.
npx claudepluginhub jeremylongshore/claude-code-plugins-plus-skills --plugin canva-packPlans and executes Canva Connect API upgrades, detects breaking changes, migrates brand template IDs, and adapts to endpoint deprecations like comment API.
Migrates from legacy Adobe APIs to Firefly Services, replaces competitor document/image APIs with Adobe using adapters, and updates auth to OAuth Server-to-Server. Includes TypeScript SDK examples.
Implements low-downtime API migrations between versions or frameworks using strangler fig, traffic shadowing, endpoint mapping, adapters, and phased cutovers.