Webhook event types
Distribu fires twelve events today, covering the order lifecycle, customer and product changes, returns and refunds, low-stock alerts, and subscription billing:
| Event | Fires when… |
|---|---|
order.created | A new order is placed (storefront or API). |
order.status_changed | An order transitions between statuses. |
order.shipped | An order transitions specifically to SHIPPED. |
order.cancelled | An order transitions specifically to CANCELLED. |
customer.created | A customer account is created (API or self-register). |
customer.updated | A customer's details change. |
product.updated | A product is edited. |
product.low_stock | A product's stock crosses below its low-stock threshold. |
invoice.paid | A Stripe invoice succeeds (subscription billing). |
return.created | A return is opened. |
return.approved | A return transitions to APPROVED. |
refund.processed | A refund is issued. |
Register for any subset of these when you create a webhook — see Webhooks endpoints.
Dashboard-created orders do NOT fire
order.created. Only storefront and API order placement do. They can still fire every other order event once status moves pastSUBMITTED.
Envelope recap
Every event wraps its payload in the same envelope:
{
"event": "order.created",
"timestamp": "2026-04-16T14:22:00.000Z",
"data": { /* event-specific */ }
}
The rest of this page covers the data object for each event type.
Order events
order.created
Fires the moment an order is successfully placed from the storefront
or POST /api/v1/orders.
Heads-up: the data shape varies by source
There are two slightly different payloads for order.created depending
on which code path created the order. We're working on unifying them;
until then, write your receiver to handle both.
From the storefront:
{
"event": "order.created",
"timestamp": "2026-04-16T14:22:00.000Z",
"data": {
"id": "clxxorder123...",
"status": "SUBMITTED",
"total": 131.25,
"customerId": "clxxcust1...",
"poNumber": "PO-12345",
"itemCount": 2
}
}
itemCount— count of line items, not total quantity.- No
items[]array. Fetch the full order viaGET /api/v1/orders/{id}if you need line details.
From POST /api/v1/orders:
{
"event": "order.created",
"timestamp": "2026-04-16T14:22:00.000Z",
"data": {
"id": "clxxorder123...",
"status": "SUBMITTED",
"poNumber": "PO-12345",
"notes": null,
"total": 131.25,
"createdAt": "2026-04-16T14:22:00.000Z",
"updatedAt": "2026-04-16T14:22:00.000Z",
"customerId": "clxxcust1...",
"items": [
{
"id": "clxxitem1...",
"quantity": 10,
"unitPrice": 8.5,
"productId": "clxxproduct1...",
"productName": "Widget Blue",
"productSku": "WDG-001"
}
]
}
}
Fields present in both sources:
| Field | Type | Notes |
|---|---|---|
id | string | The order's ID. |
status | string | Always "SUBMITTED" for order.created. |
total | number | Order total, 2 decimals. |
customerId | string | Never null — only fires for customer-linked orders. |
poNumber | string | null | Whatever was supplied, or null. |
order.status_changed
Fires on every status transition (dashboard, API, or programmatic).
{
"event": "order.status_changed",
"timestamp": "2026-04-17T09:10:00.000Z",
"data": {
"id": "clxxorder123...",
"status": "CONFIRMED",
"previousStatus": "SUBMITTED",
"total": 131.25,
"customerId": "clxxcust1...",
"tracking": null
}
}
| Field | Type | Notes |
|---|---|---|
id | string | The order's ID. |
status | string | New status — SUBMITTED, CONFIRMED, SHIPPED, DELIVERED, or CANCELLED. |
previousStatus | string | The status before this change. |
total | number | Order total. |
customerId | string | null | null for dashboard-created orders that never had a customer. |
tracking | object | null | Populated once the order has been marked SHIPPED. |
From PATCH /api/v1/orders/{id} the same event includes the full
order object (items[], poNumber, notes, createdAt,
updatedAt). The six fields above are always present regardless of
source.
Shipment tracking
When an order transitions to SHIPPED, a tracking sub-object appears
(and persists on later transitions for the same order):
{
"tracking": {
"carrier": "UPS",
"number": "1Z999AA10123456784",
"url": "https://www.ups.com/track?tracknum=1Z999AA10123456784"
}
}
carrier— one ofUPS,USPS,FEDEX,DHL,CANADA_POST,OTHER.number— the raw tracking number, whitespace stripped.url— tracking URL (auto-generated for named carriers, supplied by the distributor forOTHER). May benullon legacy rows.
order.shipped
Fires in addition to order.status_changed when an order
specifically transitions to SHIPPED. Lets you subscribe to "shipped"
events without filtering every status change.
{
"event": "order.shipped",
"timestamp": "2026-04-17T09:10:00.000Z",
"data": {
"id": "clxxorder123...",
"previousStatus": "CONFIRMED",
"total": 131.25,
"customerId": "clxxcust1...",
"tracking": {
"carrier": "UPS",
"number": "1Z999AA10123456784",
"url": "https://www.ups.com/track?tracknum=1Z999AA10123456784"
}
}
}
tracking is always populated on order.shipped — the transition
requires it.
order.cancelled
Fires in addition to order.status_changed when an order
specifically transitions to CANCELLED.
{
"event": "order.cancelled",
"timestamp": "2026-04-17T11:42:00.000Z",
"data": {
"id": "clxxorder123...",
"previousStatus": "CONFIRMED",
"total": 131.25,
"customerId": "clxxcust1..."
}
}
Cancelling an order does not automatically open a return or adjust stock — see Order statuses → Stock and status.
Customer events
customer.created
Fires when a customer record is created — either via
POST /api/v1/customers or a self-registration on the storefront.
From POST /api/v1/customers:
{
"event": "customer.created",
"timestamp": "2026-04-17T10:00:00.000Z",
"data": {
"id": "clxxcust1...",
"email": "buyer@acme.com",
"name": "Acme Restaurant Group",
"status": "ACTIVE",
"source": "api"
}
}
From storefront self-register:
{
"event": "customer.created",
"timestamp": "2026-04-17T10:00:00.000Z",
"data": {
"id": "clxxcust1...",
"email": "buyer@acme.com",
"name": "Acme Restaurant Group",
"storeSlug": "acme-foods",
"source": "storefront"
}
}
The source field distinguishes the two. Staff creating a customer
from the dashboard does not fire this event.
customer.updated
Fires when a customer record changes (name, email, status, credit limit, tax rate override, notes, etc.).
{
"event": "customer.updated",
"timestamp": "2026-04-17T10:30:00.000Z",
"data": {
"id": "clxxcust1...",
"email": "newbuyer@acme.com",
"name": "Acme Restaurant Group",
"status": "ACTIVE",
"previousStatus": "PENDING"
}
}
previousStatus is only present when the status itself changed.
Product events
product.updated
Fires when a product is edited — price, stock, metadata, category, or
active-state toggle. Does not fire on creation (no
product.created event today).
{
"event": "product.updated",
"timestamp": "2026-04-17T12:05:00.000Z",
"data": {
"id": "clxxproduct1...",
"sku": "WDG-001",
"name": "Widget Blue",
"price": 8.50,
"stock": 42,
"isActive": true,
"category": "Hardware"
}
}
For active-state toggles (Activate / Deactivate buttons), a lighter
payload is sent with just id, sku, name, and isActive.
product.low_stock
Fires when a product's stock crosses at or below its configured low-stock threshold, typically as a side effect of an order decrement. It won't re-fire on further decrements until the stock climbs back above the threshold.
{
"event": "product.low_stock",
"timestamp": "2026-04-17T14:22:00.000Z",
"data": {
"id": "clxxproduct1...",
"sku": "WDG-001",
"name": "Widget Blue",
"currentStock": 3,
"threshold": 5,
"triggeringOrderId": "clxxorder123..."
}
}
currentStock— the new stock level after decrement.threshold— the configured low-stock cutoff on the product.triggeringOrderId— the order that pushed stock below the threshold. May benullfor non-order decrements (e.g. manual stock adjustments).
See Stock tracking → Low stock alerts.
Return & refund events
return.created
Fires when a return is opened — from the storefront or from the dashboard.
{
"event": "return.created",
"timestamp": "2026-04-17T15:00:00.000Z",
"data": {
"id": "clxxreturn1...",
"orderId": "clxxorder123...",
"customerId": "clxxcust1...",
"itemCount": 2,
"reason": "Two units arrived damaged in transit.",
"status": "REQUESTED",
"actor": "staff"
}
}
actor—"staff"when opened from the dashboard, omitted when opened by a customer from the storefront.itemCount— distinct line items on the return, not total quantity.reason— the free-form reason text, 3–2000 chars.
See Returns & refunds for the full lifecycle.
return.approved
Fires on the REQUESTED → APPROVED transition.
{
"event": "return.approved",
"timestamp": "2026-04-17T15:45:00.000Z",
"data": {
"id": "clxxreturn1...",
"orderId": "clxxorder123...",
"customerId": "clxxcust1...",
"status": "APPROVED"
}
}
There is no separate return.rejected or return.received event
today. The moment that matters for external systems (e.g. "start
expecting the package") is approval.
refund.processed
Fires on the RECEIVED → REFUNDED transition, i.e. when a refund row
is created against the return.
{
"event": "refund.processed",
"timestamp": "2026-04-17T17:10:00.000Z",
"data": {
"id": "clxxrefund1...",
"returnId": "clxxreturn1...",
"orderId": "clxxorder123...",
"customerId": "clxxcust1...",
"amount": 42.50,
"method": "CREDIT",
"breakdown": {
"goodUnits": 2,
"damagedUnits": 1,
"subtotal": 42.50
}
}
}
method—CREDIT(credit note created) orMANUAL(refunded out of band).amount— the actual refunded amount, including any override.breakdown— unit breakdown from the received condition (GOOD vs DAMAGED).
Billing events
invoice.paid
Fires when a Stripe invoice on your Distribu subscription succeeds — initial sign-up charge, a monthly renewal, or any subscription-related invoice settling. Use this to reconcile billing in your own books.
{
"event": "invoice.paid",
"timestamp": "2026-04-17T00:00:00.000Z",
"data": {
"stripeInvoiceId": "in_1PqA2bB...",
"amountPaid": 49.00,
"currency": "usd",
"hostedInvoiceUrl": "https://invoice.stripe.com/i/abc...",
"number": "DISTRIBU-000042",
"periodStart": "2026-04-01T00:00:00.000Z",
"periodEnd": "2026-05-01T00:00:00.000Z"
}
}
This event is about your subscription to Distribu — not your customers' orders.
Out-of-order delivery
Webhooks are best-effort — there's no guaranteed ordering. In rare cases (especially if your endpoint is slow) you might receive:
- A
CONFIRMEDorder.status_changedbefore aSUBMITTEDorder.createdyou haven't processed yet. - A
return.approvedbefore thereturn.createdfor the same return.
Mitigations:
- Include
previousStatus→statusin your state machine — it gives you a sanity check that you're not applying a stale update. - On receiving any event, a follow-up
GETagainst the related resource gives you the authoritative current state. - Use the
timestampfield on the envelope to drop obviously old deliveries.
Events not currently available
None of these fire webhooks today:
product.created,product.deletedcustomer.deleted,customer.blocked(the blocked-state change flows throughcustomer.updated)scheduled_report.failed— in-app notification + email onlysubscription.*beyondinvoice.paid— in-app / email only
On the roadmap — email support@distribu.app if one is critical.
Next: Signature verification.
