Webhooks
Receive a POST every time something interesting happens in your store.
A webhook is a URL you register that Baseplate POSTs to when an event fires. Use them to keep external systems (ERPs, AI agents, accounting, Slack) in sync without polling.
Setup
Register one webhook per (event, URL) pair from the admin UI under Webhooks, or programmatically:
curl -X POST "https://bp.mobicms.com.br/stores/$STORE_ID/webhooks" \
-H "Authorization: Bearer $BP_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"url": "https://api.example.com/baseplate/webhook",
"events": ["orders.created", "orders.updated"],
"secret": "shhh-this-stays-on-your-server"
}'The secret is what you'll use to verify signatures on incoming
deliveries (see below). Pick a long random string per webhook.
Common events
| Event | Fired when |
|---|---|
orders.created | A new order is committed (payment intent issued). |
orders.updated | Status change: paid, fulfilled, canceled, refunded. |
orders.refunded | Refund cleared the provider. |
products.created | New product, including the first save of a draft. |
products.updated | Anything mutates on a product or its media. |
product.image.created / .updated / .deleted | Gallery changes. |
customers.created / .updated | New customer or profile edit. |
user.created | New teammate accepted an invite. |
The full list lives in the backend's WebhookEvent enum and the
webhook_subscriptions.events column.
Delivery shape
Every delivery has the same envelope:
{
"event": "orders.updated",
"store_id": "00000000-0000-0000-0000-000000000000",
"delivered_at": "2026-05-13T03:24:11.452Z",
"data": {
"id": "...",
"status": "paid",
"total_cents": 18900,
"...": "event-specific payload"
}
}Baseplate expects a 2xx response. Non-2xx triggers the retry policy below.
Verifying signatures
Every delivery carries an X-Webhook-Signature header:
X-Webhook-Signature: sha256=<base64>The signature is HMAC-SHA256(secret, raw_request_body) encoded as
base64 (standard alphabet, with = padding). Compute the HMAC over
the raw bytes, not the parsed JSON — re-serializing would change
whitespace and break the comparison.
import crypto from 'node:crypto'
export function verify(req, secret) {
const header = req.headers['x-webhook-signature'] ?? ''
const sig = header.replace(/^sha256=/, '')
const expected = crypto
.createHmac('sha256', secret)
.update(req.rawBody) // raw bytes — see your framework's docs
.digest('base64')
// Constant-time compare; both buffers must be the same length first.
const a = Buffer.from(sig)
const b = Buffer.from(expected)
return a.length === b.length && crypto.timingSafeEqual(a, b)
}Always compare with a constant-time function. A naive === leaks
information about the secret to a timing attacker.
The delivery also includes X-Webhook-Event: webhook.delivery (a
constant tag for outbound deliveries) and standard Content-Type
and User-Agent headers — none of these affect signature verification.
Retries
If your endpoint returns a non-2xx (or the request errors out), Baseplate retries with short exponential backoff:
| Attempt | Wait before attempt |
|---|---|
| 1 | (immediate) |
| 2 | ~2 seconds |
| 3 | ~4 seconds |
After three failed attempts the delivery is recorded as failed and is
not retried automatically. Each attempt is persisted in the
webhook_deliveries table so you can review them in the Webhooks →
Deliveries tab.
Make your handlers fast
Because the backoff is short and the cap is low, a slow or flapping endpoint will burn through its retry budget in seconds. Acknowledge the delivery (return 2xx) as soon as you've validated the signature and persisted the payload, then process asynchronously.
Idempotency
Bursty events can deliver the same payload more than once (retries
after a timeout, for example). Dedupe on the natural primary key in
data — usually data.id plus the event name. The body is stable
across retries.
key = f"baseplate:{event}:{data['id']}"
if not redis.set(key, 1, nx=True, ex=86400):
return ack() # already processed in the last 24hTesting locally
Spin up a tunnel (cloudflared, ngrok) pointing at your dev server,
register the public URL as a webhook against a development store, and
trigger an event from the admin. The Webhooks → Deliveries tab
shows everything that was attempted, including failures, with response
bodies — useful when your handler is returning the wrong status.
Permission cheatsheet
| Action | Permission bit |
|---|---|
| List, get | webhooks.read |
| Create | webhooks.write |
| Edit, replay | webhooks.edit |
| Delete | webhooks.delete |