Integrations
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
- Your server signs a short token for the user with the signing secret of the embed.
- Your page opens
https://embed.bannerify.co/<embed id>?token=<token>in an iframe. - Bannerify checks the token and the domain of the page, then opens the editor for that user.
- 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
- Open Embed in the dashboard sidebar.
- Click Set up the embed, then turn on Turn on the embed.
- Under Allowed domains, add the origin of each page that shows the editor, such as
https://app.example.com. Usehttps://*.example.comfor every subdomain, andhttp://localhost:3000while you develop. - 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.
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
<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.
npm install @bannerify/embed
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.
npm install @bannerify/embed-react
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. |
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”.
Each requestContent is one generation of your workspace, like a download in the editor or a render with the API. When the workspace used all its generations, a download shows the error in the editor and requestContent fails with 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.
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. Each download is one generation of your workspace.
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, then render them like any template.
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.
{
"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.
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. |
| This app used all its generations for this month | Downloads count as generations of your workspace. Wait for the reset on the first day of the month, or change your plan. |
| The page does not show the editor | Add the origin of the page under Allowed domains. |