Agent Skills: Canva Connect API — Install & Auth

|

UncategorizedID: jeremylongshore/claude-code-plugins-plus-skills/canva-install-auth

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-install-auth

Skill Files

Browse the full folder contents for canva-install-auth.

Download Skill

Loading file tree…

plugins/saas-packs/canva-pack/skills/canva-install-auth/SKILL.md

Skill Metadata

Name
canva-install-auth
Description
|

Canva Connect API — Install & Auth

Overview

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.

Prerequisites

  • Node.js 18+ (for native crypto.subtle and fetch)
  • A Canva account at canva.com
  • An integration registered at canva.dev

Instructions

Step 1: Register Your Integration

  1. Go to Settings > Integrations at canva.com/developers
  2. Create a new integration — note your Client ID and Client Secret
  3. Add redirect URI(s): e.g. http://localhost:3000/auth/canva/callback
  4. Enable required scopes under Permissions

Step 2: Store Credentials

# .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

Step 3: Implement OAuth 2.0 PKCE Flow

// 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();
}

Step 4: Verify Connection

// 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}`);
}

Available OAuth Scopes

| 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 Handling

| 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 |

Resources

Next Steps

After successful auth, proceed to canva-hello-world for your first API call.