Brand templates

Choose a brand template, preview its details, and apply it to the current design using a securely verified brand template token.

Running this example

To run this example locally:

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

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

  3. Clone the starter kit:

    git clone https://github.com/canva-sdks/canva-apps-sdk-starter-kit.git
    cd canva-apps-sdk-starter-kit
    SHELL
  4. Install dependencies:

    npm install
    SHELL
  5. Run the example:

    npm run start brand_templates
    SHELL
  6. 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 { Alert, Button, Rows, Text, Title } from "@canva/app-ui-kit";
import type { BrandTemplateId, BrandTemplateMetadata } from "@canva/design";
import {
applyTemplate,
getBrandTemplateMetadata,
requestBrandTemplates,
} from "@canva/design";
import { useState } from "react";
import * as styles from "styles/components.css";
// Canva SDK for accessing user authentication
import { auth } from "@canva/user";
// Data the backend returns for a brand template. brandTemplateId is decoded
// and verified server-side from the token — see backend/server.ts — since
// the raw token from requestBrandTemplates isn't itself a usable
// BrandTemplateId. applyCount is tracked per-brand.
type BrandTemplateData = {
brandTemplateId: BrandTemplateId;
applyCount: number;
};
type State =
| { status: "idle" }
| { status: "loading" }
| {
status: "preview";
token: string;
metadata: BrandTemplateMetadata;
data: BrandTemplateData;
}
| {
status: "applying";
token: string;
metadata: BrandTemplateMetadata;
data: BrandTemplateData;
};
export function App() {
const [state, setState] = useState<State>({ status: "idle" });
const [error, setError] = useState<string | null>(null);
// Sends the brand template token to the backend, where
// brandTemplate.verifyToken() decodes and verifies it before returning the
// real brandTemplateId, along with how many times this brand has applied
// the template.
const getBrandTemplateData = async (
brandTemplateToken: string,
): Promise<BrandTemplateData> => {
const authToken = await auth.getCanvaUserToken();
const response = await fetch(
`${BACKEND_HOST}/brand-template?brandTemplateToken=${brandTemplateToken}`,
{
headers: {
Authorization: `Bearer ${authToken}`,
},
},
);
return response.json();
};
const handleChooseTemplate = async () => {
setError(null);
setState({ status: "loading" });
try {
const response = await requestBrandTemplates();
if (response.status === "aborted") {
setState({ status: "idle" });
return;
}
// The token is an opaque, signed JWT — not itself a usable
// BrandTemplateId. It's sent to the backend, where
// brandTemplate.verifyToken() confirms it's a genuine, unmodified
// token issued by Canva for this app, and decodes it into the real ID,
// which is what the client-side calls below need.
const token = response.brandTemplates[0]?.token;
if (!token) {
setState({ status: "idle" });
setError("No brand template was selected.");
return;
}
const data = await getBrandTemplateData(token);
const metadata = await getBrandTemplateMetadata(data.brandTemplateId);
setState({ status: "preview", token, metadata, data });
} catch (err) {
setState({ status: "idle" });
setError(
err instanceof Error ? err.message : "Failed to load brand template.",
);
}
};
const handleApplyTemplate = async () => {
if (state.status !== "preview") return;
const { token, metadata, data } = state;
setError(null);
setState({ status: "applying", token, metadata, data });
try {
const result = await applyTemplate({
template: { id: data.brandTemplateId },
mode: "overwrite",
});
if (result.status === "aborted") {
setState({ status: "preview", token, metadata, data });
return;
}
// Record the successful apply against the brand template. The backend
// re-verifies the token via brandTemplate.verifyToken() before
// trusting the brand template ID, rather than accepting it as a plain
// request parameter.
const authToken = await auth.getCanvaUserToken();
await fetch(
`${BACKEND_HOST}/brand-template?brandTemplateToken=${token}`,
{
headers: {
Authorization: `Bearer ${authToken}`,
},
method: "POST",
},
);
const refreshedData = await getBrandTemplateData(token);
setState({
status: "preview",
token,
metadata,
data: refreshedData,
});
} catch (err) {
setState({ status: "preview", token, metadata, data });
setError(
err instanceof Error ? err.message : "Failed to apply template.",
);
}
};
return (
<div className={styles.scrollContainer}>
<Rows spacing="2u">
<Title>Brand templates</Title>
<Text>
Pick a brand template, preview its dataset fields, then apply it to
the current design.
</Text>
<Button
variant="primary"
onClick={handleChooseTemplate}
loading={state.status === "loading"}
stretch
>
Choose brand template
</Button>
{error && <Alert tone="critical">{error}</Alert>}
{(state.status === "preview" || state.status === "applying") && (
<TemplatePreview
metadata={state.metadata}
data={state.data}
isApplying={state.status === "applying"}
onApply={handleApplyTemplate}
/>
)}
</Rows>
</div>
);
}
function TemplatePreview({
metadata,
data,
isApplying,
onApply,
}: {
metadata: BrandTemplateMetadata;
data: BrandTemplateData;
isApplying: boolean;
onApply: () => void;
}) {
const dataset = metadata.dataset ?? [];
return (
<Rows spacing="1u">
{metadata.keywords.length > 0 && (
<Text tone="secondary" size="small">
Keywords: {metadata.keywords.join(", ")}
</Text>
)}
{dataset.length === 0 ? (
<Text tone="secondary">This template has no dataset fields.</Text>
) : (
<Rows spacing="0.5u">
<Text size="small" tone="secondary">
Dataset fields:
</Text>
{dataset.map((field) => (
<Text key={field.label} size="small">
{`${field.label} (${field.type})`}
</Text>
))}
</Rows>
)}
<Text tone="secondary" size="small">
Applied {data.applyCount} time{data.applyCount === 1 ? "" : "s"} by this
brand, as tracked by the backend using the decoded brand template ID.
</Text>
<Button variant="primary" onClick={onApply} loading={isApplying} stretch>
Apply template
</Button>
</Rows>
);
}
TYPESCRIPT
// For usage information, see the README.md file.
// Database type definitions for storing data per brand and brand template
type BrandId = string;
type BrandData = {
name: string;
brandTemplates: Map<BrandTemplateId, BrandTemplateData>;
};
type BrandTemplateId = string;
type BrandTemplateData = {
applyCount: number;
};
/**
* Create a Map object that will act as an in-memory database for this example. This DB stores
* data on a per-brand basis. Each brand contains multiple brand templates, keyed by brand
* template ID.
*/
export const createInMemoryDatabase = () => {
return new Map<BrandId, BrandData>();
};
let brandCounter = 1;
export const createBrand = () => ({
name: `FooBar's Brand ${brandCounter++}`,
brandTemplates: new Map<BrandTemplateId, BrandTemplateData>(),
});
TYPESCRIPT
// For usage information, see the README.md file.
/* eslint-disable @typescript-eslint/no-non-null-assertion -- user and brandTemplate are guaranteed by verifyToken middleware */
import cors from "cors";
import "dotenv/config";
import express from "express";
import { createBaseServer } from "../../../../utils/backend/base_backend/create";
import {
user,
brandTemplate,
tokenExtractors,
} from "@canva/app-middleware/express";
import { createBrand, createInMemoryDatabase } from "./database";
/**
* 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();
/**
* 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(user.verifyToken({ appId: APP_ID }));
/**
* Endpoint for decoding a brand template token and retrieving the data
* associated with that brand template. Brand template data is stored
* per-brand, since brand templates belong to a brand rather than an
* individual user.
*
* The brandTemplate.verifyToken() middleware verifies the brand template token from the
* query parameter and populates req.canva.brandTemplate with { brandTemplateId, appId }.
* This ensures we only accept valid, Canva-generated brand template tokens and prevents
* unauthorized access to arbitrary brand template IDs.
*
* The response includes the decoded brandTemplateId, since the token itself
* isn't a usable BrandTemplateId — the client needs this real, verified ID to
* call getBrandTemplateMetadata and applyTemplate from @canva/design.
*/
router.get(
"/brand-template",
brandTemplate.verifyToken({
appId: APP_ID,
tokenExtractor: tokenExtractors.fromQuery("brandTemplateToken"),
}),
async (req, res) => {
const { brandTemplateId } = req.canva.brandTemplate!;
const { brandId } = req.canva.user!;
const brand = data.get(brandId);
const brandTemplateData = brand?.brandTemplates?.get(brandTemplateId) ?? {
applyCount: 0,
};
return res.send({ brandTemplateId, ...brandTemplateData });
},
);
/**
* Endpoint for recording that a brand template was applied to a design.
* Brand template data is stored per-brand, since brand templates belong to a brand
* rather than an individual user.
*
* The brandTemplate.verifyToken() middleware verifies the brand template token from the
* query parameter and populates req.canva.brandTemplate with { brandTemplateId, appId }.
* This ensures we only increment the apply count for valid, Canva-generated brand template
* tokens, and prevents unauthorized access to arbitrary brand template IDs.
*/
router.post(
"/brand-template",
brandTemplate.verifyToken({
appId: APP_ID,
tokenExtractor: tokenExtractors.fromQuery("brandTemplateToken"),
}),
async (req, res) => {
const { brandTemplateId } = req.canva.brandTemplate!;
const { brandId } = req.canva.user!;
let brand = data.get(brandId);
if (brand == null) {
brand = createBrand();
data.set(brandId, brand);
}
const brandTemplateData = brand.brandTemplates.get(brandTemplateId) ?? {
applyCount: 0,
};
brandTemplateData.applyCount += 1;
brand.brandTemplates.set(brandTemplateId, brandTemplateData);
return res.sendStatus(200);
},
);
const server = createBaseServer(router);
server.start(process.env.CANVA_BACKEND_PORT);
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";
import type { DesignEditorIntent } from "@canva/intents/design";
import { prepareDesignEditor } from "@canva/intents/design";
async function render() {
const root = createRoot(document.getElementById("root") as Element);
root.render(
<AppUiProvider>
<App />
</AppUiProvider>,
);
}
const designEditor: DesignEditorIntent = { render };
prepareDesignEditor(designEditor);
// Hot Module Replacement for development (automatically reloads the app when changes are made)
if (module.hot) {
module.hot.accept("./app", render);
}
TYPESCRIPT
# Brand templates
Demonstrates the brand template workflow: choose a brand template with `requestBrandTemplates`, then send its token to a backend, where `brandTemplate.verifyToken()` from `@canva/app-middleware` verifies the token and decodes it into the real brand template ID. That ID — not the raw token — is what's used to preview the template's dataset fields with `getBrandTemplateMetadata` and apply it to the current design with `applyTemplate`, both from `@canva/design`. The backend also uses the ID to track how many times the template has been applied.
For API reference docs and instructions on running this example, see: https://www.canva.dev/docs/apps/examples/brand-templates/.
This example requires a backend with a valid `CANVA_APP_ID` configured in the root `.env` file. See [Running an example's backend](../../../README.md#running-an-examples-backend) for setup instructions.
Related examples: See design_interaction/design_token for the equivalent pattern of verifying a token on the backend, or design_interaction/design_template_metadata for retrieving metadata about the templates used to create a design (a different, more mature API than the brand template APIs used here).
NOTE: This example differs from what is expected for public apps to pass a Canva review:
- **Preview/beta API**: `requestBrandTemplates`, `getBrandTemplateMetadata`, `applyTemplate`, and the brand template token verifier from `@canva/app-middleware` are `@beta` APIs — not yet guaranteed stable, and not suitable for production apps without checking the latest API status in the docs
- **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
- **Code structure**: Production apps using [intents](https://www.canva.dev/docs/apps/intents/) are recommended to call the prepareDesignEditor function from src/intents/design_editor/index.tsx
MARKDOWN

API reference

Need help?