> ## Documentation Index
> Fetch the complete documentation index at: https://documentation.bannerify.co/llms.txt
> Use this file to discover all available pages before exploring further.

# Embed the editor in your app

> Learn how to put the Bannerify editor in your product, so your users design with your templates.

Use the embed when your users need to make banners, posts, labels or documents inside your product. Your page shows the Bannerify editor in an iframe. Each of your users gets their own designs, and can start from the templates of your workspace.

## How it works

1. Your server signs a short token for the user with the signing secret of the embed.
2. Your page opens `https://embed.bannerify.co/<embed id>?token=<token>` in an iframe.
3. Bannerify checks the token and the domain of the page, then opens the editor for that user.
4. The designs of the user are templates of your workspace. You render them with the API, like any other template.

## Prerequisites

* A workspace on the Scale or Enterprise plan.
* A server that can sign a JWT. The signing secret must stay on your server.

| | Scale | Enterprise |
| - | - | - |
| Allowed domains | 3 | 8 |
| Editor users | 100 | 500 |
| Downloads, permissions, name, logo and accent color | Yes | Yes |
| SDK events and commands | Yes | Yes |
| Webhooks | Yes | Yes |
| Hide the Bannerify branding | No | Yes |
| Asset picker from your app | No | Yes |
| Analytics | No | Yes |

## Set up the embed

1. Open **Embed** in the dashboard sidebar.
2. Click **Set up the embed**, then turn on **Turn on the embed**.
3. Under **Allowed domains**, add the origin of each page that shows the editor, such as `https://app.example.com`. Use `https://*.example.com` for every subdomain, and `http://localhost:3000` while you develop.
4. Copy the **Embed id** and the **Signing secret**.

## Sign a token on your server

The token is an HS256 JWT. Set `sub` to the id of the user in your app. The same `sub` always opens the same designs.

```js theme={null}
import { SignJWT } from "jose"

const token = await new SignJWT({ sub: user.id, name: user.name, email: user.email })
  .setProtectedHeader({ alg: "HS256" })
  .setExpirationTime("1h")
  .sign(new TextEncoder().encode(process.env.BANNERIFY_EMBED_SECRET))
```

| Claim | Required | Description |
| - | - | - |
| `sub` | No | The user id in your app. Letters, numbers, `_`, `-`, `@` and `.`, up to 256 characters. Without it, the editor edits the templates of your workspace. |
| `name` | No | The name of the user. |
| `email` | No | The email of the user. |
| `exp` | Yes | When the token expires. At most 24 hours from now. |

## Show the editor

```html theme={null}
<iframe
  src="https://embed.bannerify.co/emb_xxx?token=TOKEN"
  style="width: 100%; height: 800px; border: 0"
  allow="clipboard-write"
></iframe>
```

Add `&templateId=tpl_xxx` to the URL to open one design directly.

## Use the SDK

The SDK adds the iframe for you, tells your page what the user does, and lets your page control the editor.

```bash theme={null}
npm install @bannerify/embed
```

```ts theme={null}
import { createBannerifyEmbed } from "@bannerify/embed"

const embed = createBannerifyEmbed({
  container: document.getElementById("editor"),
  embedId: "emb_xxx",
  token, // signed by your server
})

embed.on("template:update", ({ template }) => {
  console.log("Saved", template.id)
})
```

In React, install `@bannerify/embed-react` and use the component. The ref controls the editor.

```bash theme={null}
npm install @bannerify/embed-react
```

```tsx theme={null}
import { BannerifyEmbed, type BannerifyEmbedHandle } from "@bannerify/embed-react"

const editor = useRef<BannerifyEmbedHandle>(null)

<BannerifyEmbed
  ref={editor}
  embedId="emb_xxx"
  token={token}
  style={{ height: 800 }}
  onDownload={({ fileName, content }) => upload(fileName, content)}
/>
```

### Events

| Event | React prop | Data |
| - | - | - |
| `ready` | `onReady` | `{ user }`. The editor accepts commands from now on. |
| `template:open` | `onTemplateOpen` | `{ template, canEdit }`. `canEdit` is false for a template of your workspace. |
| `template:close` | `onTemplateClose` | `{ templateId }` |
| `template:create` | `onTemplateCreate` | `{ template }`, for a new design or a copy. |
| `template:update` | `onTemplateUpdate` | `{ template }`, after a save or a rename. |
| `template:delete` | `onTemplateDelete` | `{ templateId }` |
| `download` | `onDownload` | `{ templateId, format, fileName, content }`. `content` is a data URL of the file. |
| `error` | `onError` | `{ message }`, when the editor could not open. |
| `asset:request` | `onAssetRequest` | `{ requestId, type }`. See [Asset picker](#asset-picker). |

A `template` has `id`, `name`, `width`, `height` and `updatedAt`.

### Commands

Each command waits for `ready` and returns a promise.

| Command | Result |
| - | - |
| `openTemplate(templateId)` | `{ template }`. Opens a design of the user or a template of your workspace. |
| `closeTemplate()` | Goes back to the list of designs. |
| `setModifications([{ name, text, color, src, qrcode, barcode, visible }])` | `{ changed }`. Changes the layers of the open design by their names, like the modifications of the API. |
| `save()` | `{ template }`. Saves the open design. |
| `requestContent(format)` | `{ templateId, content }`. Renders the open design as `png`, `jpeg`, `webp` or `pdf`. |

A template of your workspace is read-only, so `setModifications` and `save` fail with "Make a copy of this template to change it".

### Asset picker

On the Enterprise plan, your page can show its own library when a user adds an image or picks a color. Turn on **Images from your app** or **Colors from your app** under **Asset picker**. The Media panel then shows **Choose from** with the name of your app, and the color picker gets a swatch button. Users can still upload their own files.

```ts theme={null}
const embed = createBannerifyEmbed({
  container,
  embedId: "emb_xxx",
  token,
  onAssetRequest: async ({ type }) => {
    if (type === "image") {
      const photo = await openMyMediaLibrary() // your own UI
      return photo ? { url: photo.url, fileName: photo.name } : null
    }
    const hex = await openMyBrandColors()
    return hex ? { value: hex } : null
  },
})
```

Return `null` when the user closes your picker. Bannerify downloads the image from the `https` URL and copies it into the uploads of the user, so the URL can expire after that. An image is PNG, JPEG, WebP or SVG, up to 5 MB. In React, pass the same function as `onAssetRequest`.

### Without the SDK

The editor talks to your page with `postMessage`. It sends `{ source: "bannerify-embed", type, data }` to the page that frames it, and accepts `{ source: "bannerify-host", type, requestId, data }` only from that page. It answers each command with `{ source: "bannerify-embed", type: "response", requestId, data }`, or with `error` in place of `data`. The command types are `open-template`, `close-template`, `set-modifications`, `save`, `request-content` and `asset-response`. Answer each `asset:request` event with `asset-response` and the data `{ requestId, asset }`, where `asset` is `{ url, fileName }`, `{ value }` or `null`.

## What your users can do

* Create designs in common sizes, or copy a template of your workspace.
* Edit text, images, shapes, QR codes and the other layers of the editor.
* Upload their own images.
* Download a design as PNG, JPEG, WebP or PDF.

Turn each of these on or off under **Permissions**. Under **Appearance**, set the name and logo at the top of the editor, and the accent color of its main buttons.

## Render the designs of a user

List the designs of one user with the `embedUserId` parameter of [List templates](/docs/api-reference/endpoint/template-list), then render them like any template.

```bash theme={null}
curl "https://api.bannerify.co/v1/templates?apiKey=bnfy_xxx&embedUserId=user_123"
```

The templates of your users do not show in the template list of the dashboard.

## Webhooks

Add a URL under **Webhook**, and your server gets a `POST` when a user creates, changes or deletes a design. Use them to keep your own records, because the events of the SDK only reach the open page.

```json theme={null}
{
  "id": "eev_01J9X...",
  "type": "template.create",
  "createdAt": "2026-10-19T08:00:00.000Z",
  "embedId": "emb_xxx",
  "user": { "id": "user_123" },
  "template": { "id": "tpl_xxx", "name": "Summer sale", "width": 1080, "height": 1080 }
}
```

| `type` | When |
| - | - |
| `template.create` | A user starts a design or copies a template. |
| `template.update` | A user saves or renames a design. |
| `template.delete` | A user deletes a design. |
| `test` | You click **Send test event**. `user` and `template` are `null`. |

`user` is `null` for a token without `sub`. Answer with a 2xx status within 10 seconds. A delivery that fails is tried again after 1, 2, 4, 8, 16 and 32 minutes. The same event always has the same `id`, so skip an `id` that you already handled.

### Check the signature

Each request has a `Bannerify-Signature` header: `t=<unix seconds>,v1=<signature>`. The signature is the hex HMAC-SHA256 of `<t>.<raw body>` with the webhook secret.

```ts theme={null}
import { createHmac, timingSafeEqual } from "node:crypto"

const verify = (rawBody: string, header: string, secret: string) => {
  const { t, v1 } = Object.fromEntries(header.split(",").map((part) => part.split("=")))
  if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false
  const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex")
  return v1.length === expected.length && timingSafeEqual(Buffer.from(v1), Buffer.from(expected))
}
```

Read the raw body before you parse it: a parsed and serialized body can have other bytes.

## Analytics

On the Enterprise plan, the **Analytics** section of the Embed page shows how your users use the editor in the last 7, 30 or 90 days: users, sessions, designs created and downloads, the sessions per day, the sites that opened the editor, and the most active users. A session is one load of the editor. Days are in UTC.

## Rotate the signing secret

Click **Rotate secret** when the secret leaks. Tokens signed with the old secret stop working at once, so update your server first.

## Troubleshooting

| Message | What to do |
| - | - |
| The embed token is not valid | Sign the token with the current signing secret, with `alg` HS256 and an `exp` claim. |
| This embed is turned off | Turn on the embed in the dashboard. |
| This app reached its limit of editor users | Each `sub` counts as one user. Upgrade to Enterprise for more users. |
| The page does not show the editor | Add the origin of the page under **Allowed domains**. |


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.