Examples
App elements
Assets and media
Fundamentals
Intents
Design interaction
Drag and drop
Design elements
Localization
Content replacement
Design token
Secure token-based authentication and design access.
Running this example
To run this example locally:
-
If you haven't already, create a new app in the Developer Portal(opens in a new tab or window). For more information, refer to our Quickstart guide.
-
In your app's configuration on the Developer Portal(opens in a new tab or window), ensure the "Development URL" is set to
http://localhost:8080
. -
Clone the starter kit:
git clone https://github.com/canva-sdks/canva-apps-sdk-starter-kit.gitcd canva-apps-sdk-starter-kitSHELL -
Install dependencies:
npm installSHELL -
Run the example:
npm run start design_tokenSHELL -
Click the Preview URL link shown in the terminal to open the example in the Canva editor.
Example app source code
// For usage information, see the README.md file.import {Button,FormField,MultilineInput,Rows,Text,TextInput,} from "@canva/app-ui-kit";import { useState, useEffect } from "react";import * as styles from "styles/components.css";// Canva SDKs for accessing user authentication and design metadataimport { auth } from "@canva/user";import { getDesignMetadata, getDesignToken } from "@canva/design";// Type definition for design data stored in the backendtype DesignData = {title: string;defaultDimensions: {width: number;height: number;};};export const App = () => {const [title, setTitle] = useState("");const [state, setState] = useState<"loading" | "idle">("idle");const [designData, setDesignData] = useState<DesignData | undefined>();const [error, setError] = useState<string | undefined>();// Retrieves design data from the backend using design tokens for secure accessconst getDesignData = async () => {setState("loading");setError(undefined);try {// Get both design token and user authentication token from Canva// Design tokens provide secure access to design-specific data// User tokens authenticate the current userconst [designToken, authToken] = await Promise.all([getDesignToken(),auth.getCanvaUserToken(),]);const response = await fetch(`${BACKEND_HOST}/design/${designToken.token}`,{headers: {Authorization: `Bearer ${authToken}`,},},);return response.json();} catch {setError("Failed to get design data from server.");} finally {setState("idle");}};// Saves design data to the backend using tokens and metadata from Canvaconst saveDesignData = async () => {setState("loading");setError(undefined);try {// Collect design token, user token, and current design metadata// getDesignMetadata() provides access to design properties like dimensionsconst [designToken, authToken, { defaultPageDimensions: dimensions }] =await Promise.all([getDesignToken(),auth.getCanvaUserToken(),getDesignMetadata(),]);await fetch(`${BACKEND_HOST}/design/${designToken.token}`, {headers: {Authorization: `Bearer ${authToken}`,"Content-Type": "application/json",},method: "POST",body: JSON.stringify({designData: {title,dimensions,},}),});await refreshDesignData();} catch {setError("Failed to save design data to server.");} finally {setState("idle");}};const refreshDesignData = () => getDesignData().then(setDesignData);useEffect(() => {refreshDesignData();}, []);return (<div className={styles.scrollContainer}><Rows spacing="3u"><Text>This example demonstrates how apps can use design tokens to save andretrieve data on a per-design basis.</Text><Text>Data stored for this design:</Text><MultilineInput autoGrow readOnly value={JSON.stringify(designData)} /><Buttonloading={state === "loading"}disabled={state === "loading"}variant="primary"onClick={refreshDesignData}>Refresh</Button><FormFieldcontrol={(props) => <TextInput {...props} onChange={setTitle} />}label="Title"/><Buttonloading={state === "loading"}disabled={state === "loading"}variant="primary"onClick={saveDesignData}>Save</Button>{error && <Text tone="critical">{error}</Text>}</Rows></div>);};
TYPESCRIPT
// For usage information, see the README.md file.// Database type definitions for storing design data per brand and usertype BrandId = string;type BrandData = {name: string;users: Map<UserId, UserData>;};type UserId = string;type UserData = {name: string;designs: Map<DesignId, DesignData>;};type DesignId = string;type DesignData = {title: string;defaultDimensions: {width: number;height: number;};};/*** Create a Map object that will act as an in-memory database for this example. This DB stores user data on a per-brand* basis. Each brand contains multiple users, and each user contains multiple designs.*/export const createInMemoryDatabase = () => {return new Map<BrandId, BrandData>();};let brandCounter = 1;export const createBrand = () => ({name: `FooBar's Brand ${brandCounter++}`,users: new Map<UserId, UserData>(),});let userCounter = 1;export const createUser = () => ({name: `Foo Bar ${userCounter++}`,designs: new Map<DesignId, DesignData>(),});
TYPESCRIPT
// For usage information, see the README.md file.import * as jwt from "jsonwebtoken";import { JwksClient, SigningKeyNotFoundError } from "jwks-rsa";const CACHE_EXPIRY_MS = 60 * 60 * 1_000; // 60 minutesconst TIMEOUT_MS = 30 * 1_000; // 30 secondsconst CANVA_BASE_URI = "https://api.canva.com";/*** The JWT payload we'll decode contains:* designId - The ID of the Canva Design where this token was issued.* aud (audience) - We use the App ID to identify the targeted audience for this payload.* exp (expiry) - The expiry timestamp for this JWT, in seconds.* iat (issuedAt) - The timestamp at which this JWT was issued, in seconds.* nbf (notBefore) - The JWT should only be valid after this timestamp, in seconds.** See the JWT specification for more details on each claim and what it represents.* https://datatracker.ietf.org/doc/html/rfc7519#section-4.1*/type DesignToken = Omit<jwt.Jwt, "payload"> & {payload: {designId: string;aud: string;exp: number;iat: number;nbf: number;};};/*** A helper function for decoding the JWT and verifying that it originated from Canva by checking its signature.* @param appId - The ID of the app* @param designToken - The DesignToken JWT that contains the Design ID, App ID and User ID.* @returns A Promise that resolves to the payload contained by the DesignToken.*/export const decodeAndVerifyDesignToken = async (appId: string,designToken: string,) => {const unverifiedDecodedToken = jwt.decode(designToken, {complete: true,});if (unverifiedDecodedToken?.header?.kid == null) {throw new SigningKeyNotFoundError("Error verifying DesignToken: expected token to contain 'kid' claim header in order to produce a signing key.",);}const jwksClient = new JwksClient({cache: true,cacheMaxAge: CACHE_EXPIRY_MS,timeout: TIMEOUT_MS,rateLimit: true,jwksUri: `${CANVA_BASE_URI}/rest/v1/apps/${appId}/jwks`,});const key = await jwksClient.getSigningKey(unverifiedDecodedToken.header.kid);const publicKey = key.getPublicKey();const { payload } = jwt.verify(designToken, publicKey, {audience: appId,complete: true,}) as DesignToken;if (payload.designId == null || payload.aud == null) {throw new jwt.JsonWebTokenError("Invalid JWT payload");}/*** Convert current timestamp to seconds, as determined by the NumericDate object in the JWT specifications* See https://datatracker.ietf.org/doc/html/rfc7519#section-2*/const now = convertMillisecondsToSeconds(Date.now());/*** Dates provided in a JWT payload are in seconds, as per the NumericDate object in the JWT specification.* See https://datatracker.ietf.org/doc/html/rfc7519#section-2* We convert them to milliseconds before creating JS Date objects.*/if (payload.exp < now) {throw new jwt.TokenExpiredError("The provided DesignToken has expired.",new Date(convertSecondsToMilliseconds(payload.exp)),);}if (payload.iat > now) {throw new jwt.NotBeforeError("Invalid issue date for DesignToken",new Date(convertSecondsToMilliseconds(payload.iat)),);}if (payload.nbf > now) {throw new jwt.NotBeforeError("Cannot verify DesignToken prior to the NotBefore date",new Date(convertSecondsToMilliseconds(payload.nbf)),);}return payload;};const convertSecondsToMilliseconds = (seconds: number) => seconds * 1000;const convertMillisecondsToSeconds = (milliseconds: number) =>milliseconds / 1000;
TYPESCRIPT
// For usage information, see the README.md file.import * as cors from "cors";import "dotenv/config";import * as express from "express";import { createBaseServer } from "../../../../utils/backend/base_backend/create";import { createJwtMiddleware } from "../../../../utils/backend/jwt_middleware";import { createBrand, createInMemoryDatabase, createUser } from "./database";import { decodeAndVerifyDesignToken } from "./decode_jwt";import { SigningKeyNotFoundError } from "jwks-rsa";import * as jwt from "jsonwebtoken";/*** Retrieve the CANVA_APP_ID from environment variables.* Set this in your .env file at the root level of the project.*/const APP_ID = process.env.CANVA_APP_ID;if (!APP_ID) {throw new Error(`The CANVA_APP_ID environment variable is undefined. Set the variable in the project's .env file.`,);}/*** Initialize ExpressJS router.*/const router = express.Router();/*** Instantiate JWT middleware to be used to parse the auth token from the header.*/const jwtMiddleware = createJwtMiddleware(APP_ID);/*** In-memory database for demonstration purposes.* Production apps should use a persistent database solution.*/const data = createInMemoryDatabase();/*** IMPORTANT: You must configure your CORS Policy** Cross-Origin Resource Sharing* ([CORS](https://developer.mozilla.org/en-US/docs/Glossary/CORS)) is an* [HTTP](https://developer.mozilla.org/en-US/docs/Glossary/HTTP)-header based* mechanism that allows a server to indicate any* [origins](https://developer.mozilla.org/en-US/docs/Glossary/Origin)* (domain, scheme, or port) other than its own from which a browser should* permit loading resources.** A basic CORS configuration would include the origin of your app in the* following example:* const corsOptions = {* origin: 'https://app-abcdefg.canva-apps.com',* optionsSuccessStatus: 200* }** The origin of your app is https://app-${APP_ID}.canva-apps.com, and note* that the APP_ID should to be converted to lowercase.** https://www.npmjs.com/package/cors#configuring-cors** You may need to include multiple permissible origins, or dynamic origins* based on the environment in which the server is running. Further* information can be found* [here](https://www.npmjs.com/package/cors#configuring-cors-w-dynamic-origin).*/router.use(cors());/*** JWT middleware for authenticating requests from Canva apps.* This should be applied to all routes that require user authentication.*/router.use(jwtMiddleware);/*** Endpoint for retrieving the data associated with a particular design.* Design data is stored per-user. Users are also separated into brands.*/router.get("/design/:token", async (req, res) => {/*** Ensure to retrieve Design ID by decoding a Canva-generated DesignToken JWT. We can trust the content of these* tokens and safely assume that the current user has access.** We strongly recommend you do not expose endpoints that receive plain Design IDs. Doing so will mean that anyone* could pass through an arbitrary Design ID, including the IDs of designs they don't actually have access to* within Canva.*/let designId: string;try {({ designId } = await decodeAndVerifyDesignToken(APP_ID, req.params.token));} catch (e) {return res.status(401).json({ error: "unauthorized", message: getErrorMessage(e) });}const { userId, brandId } = req.canva;const brand = data.get(brandId);const user = brand?.users?.get(userId);res.send(user?.designs?.get(designId) || {});});/*** Endpoint for saving the data associated with a particular design.* Design data is stored per-user. Users are also separated into brands.*/router.post("/design/:token", async (req, res) => {/*** Ensure to retrieve Design ID by decoding a Canva-generated DesignToken JWT. We can trust the content of these* tokens and safely assume that the current user has access.** We strongly recommend you do not expose endpoints that receive plain Design IDs. Doing so will mean that anyone* could pass through an arbitrary Design ID, including the IDs of designs they don't actually have access to* within Canva.*/let designId: string;try {({ designId } = await decodeAndVerifyDesignToken(APP_ID, req.params.token));} catch (e) {return res.status(401).json({ error: "unauthorized", message: getErrorMessage(e) });}const { userId, brandId } = req.canva;let brand = data.get(brandId);if (brand == null) {brand = createBrand();data.set(brandId, brand);}let user = brand.users.get(userId);if (user == null) {user = createUser();brand.users.set(userId, user);}user.designs.set(designId, req.body);res.sendStatus(200);});const server = createBaseServer(router);server.start(process.env.CANVA_BACKEND_PORT);/*** Gets a readable error message to send back to the caller.* @param e - The Error object from which we derive the error message.*/const getErrorMessage = (e: unknown) => {if (e instanceof SigningKeyNotFoundError) {return "Public key not found";}if (e instanceof jwt.JsonWebTokenError) {return "Invalid token";}if (e instanceof jwt.TokenExpiredError) {return "Token expired";}return "An error has occurred while decoding the token.";};
TYPESCRIPT
// For usage information, see the README.md file.import { AppUiProvider } from "@canva/app-ui-kit";import { createRoot } from "react-dom/client";import { App } from "./app";import "@canva/app-ui-kit/styles.css";const root = createRoot(document.getElementById("root") as Element);function render() {root.render(<AppUiProvider><App /></AppUiProvider>,);}render();// Hot Module Replacement for development (automatically reloads the app when changes are made)if (module.hot) {module.hot.accept("./app", render);}
TYPESCRIPT
# Design tokenDemonstrates how to obtain design tokens for accessing design metadata through external APIs. Shows token generation, authentication integration, and design data retrieval patterns.For API reference docs and instructions on running this example, see: https://www.canva.dev/docs/apps/examples/design-token/.Related examples: See fundamentals/fetch for general API communication, or design_interaction/export for design export functionality.NOTE: This example differs from what is expected for public apps to pass a Canva review:- **In-memory database**: Uses a simple in-memory storage for demonstration. Production apps should use persistent database solutions like PostgreSQL, MongoDB, or similar- **CORS configuration**: Uses permissive CORS settings. Production apps must restrict CORS to only allow requests from your app's specific origin (https://app-{app-id}.canva-apps.com)- **Token management**: Token usage patterns are simplified for demonstration purposes. Production apps should implement proper token refresh mechanisms, secure backend communication, and appropriate rate limiting for API calls- **Error handling**: Error handling is simplified for demonstration. Production apps must implement comprehensive error handling with clear user feedback and graceful failure modes- **Internationalization**: Not implemented in this example. Production apps must support multiple languages using the `@canva/app-i18n-kit` package to pass Canva review requirements
MARKDOWN
API reference
Need help?
- Join our Community Forum(opens in a new tab or window)
- Report issues with this example on GitHub(opens in a new tab or window)