Baseplate Docs

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

EventFired when
orders.createdA new order is committed (payment intent issued).
orders.updatedStatus change: paid, fulfilled, canceled, refunded.
orders.refundedRefund cleared the provider.
products.createdNew product, including the first save of a draft.
products.updatedAnything mutates on a product or its media.
product.image.created / .updated / .deletedGallery changes.
customers.created / .updatedNew customer or profile edit.
user.createdNew 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:

AttemptWait 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 24h

Testing 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

ActionPermission bit
List, getwebhooks.read
Createwebhooks.write
Edit, replaywebhooks.edit
Deletewebhooks.delete

On this page