Signed notices when something happens in your workspace.
Each event reaches your server as a POST with JSON, signed with Standard Webhooks. All 13 catalog events are emitted today.
What your server receives.
A POST request with four things: the signature, the timestamp, the event name and a JSON body with id, event, createdAt and data.
POST /tu/endpoint HTTP/1.1
content-type: application/json
user-agent: BannerMotion-Webhooks/1
webhook-id: evt_p0q8w2e6r4t1
webhook-timestamp: 1759382964
webhook-signature: v1,K5oZfzN95Z9UVu1EsfQmfVNQhnkZ2pj9o9NDN/H/pI4=
x-bm-event: banner.generated
x-bm-delivery: dlv_k29x8m1q4a7z
x-bm-signature: sha256=3b1f... # de compatibilidad
{
"id": "evt_p0q8w2e6r4t1",
"event": "banner.generated",
"createdAt": "2025-10-02T05:29:24.100Z",
"data": {
"jobId": "job_x4k2m9q1a7bz",
"campaignId": "cmp_8f2a1c3d4e5f",
"creativeId": "crt_3a9c0d1e2f4b",
"versionNo": 1,
"source": "api"
}
}| Header | What it is |
|---|---|
| webhook-id | The event id (evt_…), the same as in the body. It does not change across retries. |
| webhook-timestamp | Unix seconds of the send. |
| webhook-signature | Standard Webhooks: v1,<base64 of the HMAC-SHA256>. Two signatures separated by a space during a rotation. |
| x-bm-event | The event name, for example banner.generated. |
| x-bm-delivery | The id of this delivery (dlv_…). |
| x-bm-signature | Previous signature, for compatibility: sha256=<hex>. |
In Integrations & API click Add endpoint, enter your server's address, check the events and click Create endpoint. Copy the secret (whsec_…): it is shown only once.
In the endpoint menu, Send test event: you receive a signed webhook.test. It does not count toward the automatic pause.
webhook-id is the event id (evt_…) and it is also in the body. It does not change across the retries of a delivery: use it to discard repeats.
The 13 events, with an example payload.
Choose an event to see when it is emitted and what data carries. All of them are emitted today; subscribe to whichever you want.
banner.generated
A concept finished generating, with all its sizes and locales. Generations made through the API and in the app both emit it.
Emitted by generations requested through the API and by those made in the app.
| Field | What it is |
|---|---|
| jobId | The job that generated the concept (job_…). |
| campaignId | The campaign (cmp_…). |
| creativeId | The generated concept (crt_…). Read it with GET /api/v1/banners/:id. |
| versionNo | Version number. It only comes when the concept was requested through the API. |
| source | "api" if you requested it through the API; "app" if it was generated in the app. |
{
"id": "evt_p0q8w2e6r4t1",
"event": "banner.generated",
"createdAt": "2026-01-01T12:00:00.000Z",
"data": {
"jobId": "job_x4k2m9q1a7bz",
"campaignId": "cmp_8f2a1c3d4e5f",
"creativeId": "crt_3a9c0d1e2f4b",
"versionNo": 1,
"source": "api"
}
}Check that the notice is from BannerMotion.
We sign with Standard Webhooks, so any of its libraries works. The SDKs ship verifyWebhook / verify_webhook ready to use.
- Use the exact bodyThe text as it arrived, before converting it to JSON.
- Build the signed content
webhook-id.webhook-timestamp.body - Compute the HMAC-SHA256The key is the bytes of the base64 that follows
whsec_in your secret. The result goes in base64, with thev1,prefix. - Compare in constant timeWith each
v1,…signature in the header. During a secret rotation two arrive, separated by a space. - Reject the old and the repeatedTimestamps more than
5minutes from your clock, and events you already processed (byid).
import { verifyWebhook } from "@bannermotion/sdk";
// rawBody: el cuerpo EXACTO recibido, como texto (antes de convertirlo a JSON).
// Durante una rotación del secreto pasa los dos: [nuevo, anterior].
const event = await verifyWebhook(process.env.BM_WEBHOOK_SECRET, req.headers, rawBody);
// lanza BannerMotionError ("invalid_signature") si la firma o la hora no valen
if (alreadyProcessed(event.id)) return; // las entregas pueden repetirseX-BM-Signature (sha256= plus the body's HMAC in hexadecimal, with the full secret) still arrives for now. In new integrations use webhook-signature.Respond fast; we keep trying.
Waits of 5 and 30 seconds between attempts. The same delivery, with the same webhook-id and the same payload.
Respond with a 2xx as soon as possible and process the event afterwards.
If you respond with a 3xx, we do not follow it: it counts as a failed attempt.
After 5 consecutive failures the webhook pauses itself and receives no more events until you reactivate it. Manual tests do not count.
Deliveries that exhausted their attempts are retried with Retry, under Recent deliveries.
That is the maximum per workspace. Each one chooses its events.
With Rotate secret, the previous one keeps signing for 24 hours as a second signature, so you can switch without losing events.
Public http and https addresses are accepted, without a username or password. Private, local and metadata networks are rejected.
Identify each event by its id and treat repeats as already processed.
Connect your notices in minutes.
Webhooks and the API are on the Team, Agency and Enterprise plans; the Starter plan does not include them.