From canva-pack
Sets up Canva Connect API OAuth 2.0 PKCE authentication, credentials, and TypeScript scaffolding for new integrations.
How this skill is triggered — by the user, by Claude, or both
Slash command
/canva-pack:canva-install-authThis skill is limited to the following tools:
The summary Claude sees in its skill listing — used to decide when to auto-load this skill
Set up a Canva Connect API integration with OAuth 2.0 Authorization Code flow with PKCE (SHA-256). The Canva Connect API is a REST API at `https://api.canva.com/rest/v1/*` — there is no SDK package. All calls use `fetch` or `axios` with Bearer tokens.
Set up a Canva Connect API integration with OAuth 2.0 Authorization Code flow with PKCE (SHA-256). The Canva Connect API is a REST API at https://api.canva.com/rest/v1/* — there is no SDK package. All calls use fetch or axios with Bearer tokens.
crypto.subtle and fetch)http://localhost:3000/auth/canva/callback# .env (NEVER commit — add to .gitignore)
CANVA_CLIENT_ID=OCAxxxxxxxxxxxxxxxx
CANVA_CLIENT_SECRET=xxxxxxxxxxxxxxxx
CANVA_REDIRECT_URI=http://localhost:3000/auth/canva/callback
echo '.env' >> .gitignore
echo '.env.local' >> .gitignore
// src/canva/auth.ts
import crypto from 'crypto';
// 1. Generate PKCE code verifier and challenge
export function generatePKCE(): { verifier: string; challenge: string } {
const verifier = crypto.randomBytes(64).toString('base64url'); // 43-128 chars
const challenge = crypto
.createHash('sha256')
.update(verifier)
.digest('base64url');
return { verifier, challenge };
}
// 2. Build the authorization URL
export function getAuthorizationUrl(opts: {
clientId: string;
redirectUri: string;
scopes: string[];
codeChallenge: string;
state: string;
}): string {
const params = new URLSearchParams({
response_type: 'code',
client_id: opts.clientId,
redirect_uri: opts.redirectUri,
scope: opts.scopes.join(' '),
code_challenge: opts.codeChallenge,
code_challenge_method: 'S256',
state: opts.state,
});
return `https://www.canva.com/api/oauth/authorize?${params}`;
}
// 3. Exchange authorization code for access token
export async function exchangeCodeForToken(opts: {
code: string;
codeVerifier: string;
clientId: string;
clientSecret: string;
redirectUri: string;
}): Promise<{ access_token: string; refresh_token: string; expires_in: number }> {
const basicAuth = Buffer.from(
`${opts.clientId}:${opts.clientSecret}`
).toString('base64');
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: 'authorization_code',
code: opts.code,
code_verifier: opts.codeVerifier,
redirect_uri: opts.redirectUri,
}),
});
if (!res.ok) {
const err = await res.json();
throw new Error(`Token exchange failed: ${err.error} — ${err.error_description}`);
}
return res.json();
}
// 4. Refresh an expired access token (access tokens expire in ~4 hours)
export async function refreshAccessToken(opts: {
refreshToken: string;
clientId: string;
clientSecret: string;
}): Promise<{ access_token: string; refresh_token: string; expires_in: number }> {
const basicAuth = Buffer.from(
`${opts.clientId}:${opts.clientSecret}`
).toString('base64');
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: opts.refreshToken,
}),
});
if (!res.ok) throw new Error('Token refresh failed');
return res.json();
}
// Verify token works by calling GET /v1/users/me (no scopes required)
async function verifyConnection(accessToken: string): Promise<void> {
const res = await fetch('https://api.canva.com/rest/v1/users/me', {
headers: { 'Authorization': `Bearer ${accessToken}` },
});
if (!res.ok) throw new Error(`Verification failed: ${res.status}`);
const { team_user } = await res.json();
console.log(`Connected — user_id: ${team_user.user_id}, team_id: ${team_user.team_id}`);
}
| Scope | Description |
|---|---|
design:content:read | Read design contents, export designs |
design:content:write | Create designs, autofill brand templates |
design:meta:read | List designs, get design metadata |
asset:read | View uploaded asset metadata |
asset:write | Upload, update, delete assets |
brandtemplate:content:read | Read brand template content |
brandtemplate:meta:read | List and view brand template metadata |
folder:read | View folder contents |
folder:write | Create, update, delete folders |
folder:permission:write | Manage folder permissions |
comment:read | Read design comments |
comment:write | Create comments and replies |
collaboration:event | Receive webhook notifications |
profile:read | Read user profile information |
| Error | Cause | Solution |
|---|---|---|
invalid_client | Wrong client_id or secret | Verify credentials in Canva dashboard |
invalid_grant | Expired or reused auth code | Restart OAuth flow — codes are single-use |
invalid_scope | Scope not enabled | Enable scope in integration settings |
access_denied | User rejected consent | Prompt user again |
| Token expired (401) | Access token > 4 hours old | Call refresh token endpoint |
After successful auth, proceed to canva-hello-world for your first API call.
npx claudepluginhub jeremylongshore/claude-code-plugins-plus-skills --plugin canva-packProvides TypeScript reference architecture for Canva Connect API integrations with Express/Next.js project structure, services, routes, OAuth PKCE, and tests. Use for new apps or structure reviews.
Configures Figma REST API authentication with personal access tokens or OAuth 2.0, covering token generation, scopes, secure storage, and verification for integrations.
Creates, edits, and optimizes skills for Claude Code, including drafting, evaluating with test prompts, iterating on performance, and improving skill descriptions for better triggering accuracy.