Set one up
Create an endpoint with your API key and tell us which events you care about:https. Use ["*"] for enabled_events if you want everything. The response
includes a signing secret — copy it now, it’s shown only once. (Lost it? Rotate it from the
dashboard or POST /webhooks/{id}/secret/rotate.)
Managing webhooks needs a key with the webhook permission (or a full-access key).
What a delivery looks like
Every event we send has the same shape:livemode is true for events from your real workspace and false for events from a
sandbox — handy for keeping test traffic out of your real processing.
Along with three headers:
Webhook-Id— the event id.Webhook-Timestamp— unix seconds when we signed it.Webhook-Signature—v1,<hex>.
Check the signature
Before you trust a delivery, verify it came from us. Recompute the signature with your endpoint’s secret and compare:expected to the Webhook-Signature header using a constant-time comparison. Reject the
request if they don’t match, or if the timestamp is more than a few minutes old.
Reply quickly, and we’ll retry
Return any2xx within 20 seconds and we consider it delivered. Return anything else (or time out)
and we retry with backoff — right away, then 1m, 5m, 30m, 2h and 6h later, six attempts over about
8½ hours. So:
- Do the slow work after you respond, not before. Acknowledge fast.
- Expect the same event more than once. Use the event
idto de-duplicate.
GET /webhooks/{id}/deliveries, inspect one with its request and
response bodies, and re-send any of them.
Test it
POST /webhooks/{id}/ping sends a webhook.ping event to your endpoint so you can confirm your
handler and signature check work before real events flow.
Events you can subscribe to
GET /webhooks/event-types returns the live list. Put any of these names in enabled_events, or
use ["*"] for all of them.
Products
Collections
Products joining or leaving a collection — including automatic changes to a rule-driven
collection — do not emit a collection event; only the collection entity itself does. To track
membership, read
GET /collections/{handle}/products. A collection
event’s data:
Bundles
A bundle’s members are its definition, so member changes are a
bundle.updated — its data
always carries the full member list. Prices and stock move without a bundle event (they belong to
the member products); refetch GET /bundles/{bundle} for those.
price is a per-currency map in minor units ({"NGN": 34000000}); null means the bundle charges
the plain sum of its members. product_variant is set only when the merchant pinned one.
Orders
order.created fires at the cart stage. If you only care about real orders, watch status
on order.updated rather than acting on every order.created.Fulfillments
Customers
Materials and supplies
These, and every section below, fire from the dashboard, the Bee and the API alike. A bulk import reports once, throughimport.completed, instead of an event per row.
component.* event carries id, name, category, status, unit_of_measure, track_stock
and available_stock.
Suppliers
data carries id, name, email, telephone and roles.
Product categories and tags
A category’s
data is { id, name, slug, parent, position }; a tag’s is { id, name, slug }.
Tagging or untagging a product is a product.updated.
Expenses
Invoices
invoice.paid fires for each issued invoice
when the order becomes fully paid — alongside order.paid. Its data is { id, number, order, currency, total }.
Payment milestone templates
data carries id, name, splits (each { "label": "Deposit", "pct": 70 }) and is_default.
Stock
Counting a line changes no stock, so it fires nothing;
stock_count.closed carries what the count
changed:
stock_pool.adjusted event:
data is { id, from_pool, to_pool, lines, note, moved_at }, where each pool is
{ id, code } and each line { variant_id, sku, label, qty }.
Bee tasks
data carries the task’s id, source, type, category, status, title, value_line,
count_total, count_remaining, amount_minor, currency, proposal_status, assignee,
snoozed_until, resolved_by and resolved_at.

