Skip to main content
A webhook is just a URL of yours that we call when something happens in your workspace — an import finishes, an order comes in, and so on. You register the URL once; we POST each event to it. No polling.

Set one up

Create an endpoint with your API key and tell us which events you care about:
The URL has to be 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:
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 any 2xx 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 id to de-duplicate.
You can see every attempt under 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, through import.completed, instead of an event per row.
A 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

An invoice’s balance comes from its order’s payments, so 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:
A stock_pool.adjusted event:
A transfer’s 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.

Imports