Webhooks
A webhook sends your store's Business Events to an endpoint on your own server as they happen: an order placed, confirmed, handed to a carrier, delivered. Each event is POSTed as JSON, signed per the Standard Webhooks specification, and tried again until your endpoint accepts it.
Carriers reporting deliveries to your store is the other direction; read Carrier webhooks for that.
Add a webhook
In the dashboard, open Integrations in the sidebar and choose Add webhook; give it a name, the URL of your endpoint and the events it receives. Through the API, POST /api/event-consumers with the integrations:manage scope (integrations:read lists webhooks and their deliveries):
Code
The answer holds the webhook and its Signing Secret, whsec_…. It is shown this once: store it in your server's secrets. Every later answer shows it masked.
- The URL must be
https, on the public internet (no private, local or reserved address), with no user name or password in it. - A store has at most 10 webhooks.
- A webhook receives the events recorded after it was added. To send it older ones, use a Replay (see below).
A subscription names Event Types exactly (order.confirmed), by subject (order.*, shipment.*), or all of them (*). A subject or * also takes the types added to the catalogue later.
What your endpoint receives
Each Delivery is one POST to your URL:
| Header | Value |
|---|---|
content-type | application/json |
user-agent | CodFlow-Webhooks/1 (Wocart's former name, kept for compatibility) |
webhook-id | The event's id. The same on every attempt and every Replay. |
webhook-timestamp | When this attempt was sent, in Unix seconds. |
webhook-signature | v1,<signature>, one per Signing Secret, separated by spaces. |
The body is the Business Event, exactly as the Event Log API returns it:
Code
| Field | Meaning |
|---|---|
id | The event's unique id. Recognise an event you already handled by it. |
sequence | Its position in the store's Event Log: a later event has a higher number. |
type, version | The Event Type and the version of its data. |
subject | What the event is about: an order or a shipment. |
orderId | The order it concerns. |
occurredAt | When it happened: a carrier may report a delivery hours later. |
recordedAt | When the store recorded it. |
actor, origin | Who caused it (staff, AI agent, shopper, carrier, system) and through what (dashboard, API, storefront, carrier webhook…). |
data | The order, and the shipment for a delivery event, as they were when it happened. |
Money is a decimal string with its currency: { "amount": "9000.00", "currency": "DZD" }. An event never exceeds 64 KB.
Verify the signature
Check every request before you act on it. The signature is the base64 HMAC-SHA256 of <webhook-id>.<webhook-timestamp>.<raw body>, keyed by the bytes the Signing Secret encodes: the part after whsec_, base64-decoded.
- Read the raw body as it arrived. Parsing and re-serialising the JSON changes the bytes, and the signature no longer matches.
- Refuse a
webhook-timestampmore than five minutes from your clock, so a captured request cannot be replayed later. - Accept the request when one of the space-separated
v1,signatures matches, compared in constant time.
Node.js
Code
Pass the raw body as a string, for example with express.text({ type: "application/json" }).
PHP
Code
The raw body is file_get_contents('php://input') (in Laravel, $request->getContent()); give the headers with lower-case names.
Python
Code
The raw body is request.get_data() in Flask, or await request.body() in FastAPI.
Any Standard Webhooks library verifies these requests too, for example new Webhook(secret).verify(rawBody, headers) with the standardwebhooks package.
Rotating the Signing Secret
Change signing secret in the dashboard, or POST /api/event-consumers/{id}/rotate-secret, answers a new secret once. For 24 hours every request carries a signature by each secret, so switch your endpoint to the new one within that time and nothing is refused. After that, only the new secret signs.
Answer quickly
Your answer decides what happens next:
| Your endpoint answers | What the store does |
|---|---|
Any 2xx, within 15 seconds | Delivered. |
410 Gone | Stops this webhook and sends it nothing more until you start it again. |
| Anything else: another status, a redirect, no answer in time, no connection | Tries again later, on the schedule below. |
Answer 2xx as soon as you have stored the event, then do the work. Redirects are not followed: give the final URL. A Retry-After header on a refusal is respected, up to a day.
Retries, duplicates and order
A Delivery is tried 8 times. The first attempt goes out seconds after the event; each failure waits before the next:
| Attempt | Waits after the one before |
|---|---|
| 1 | none: seconds after the event |
| 2 | 5 minutes |
| 3 | 30 minutes |
| 4 | 2 hours |
| 5 | 6 hours |
| 6 | 12 hours |
| 7 | 24 hours |
| 8 | 24 hours |
After the last failure the Delivery is dead: it stays in the dashboard, where you can try it again. Retries run on a five-minute schedule, so a wait can be up to five minutes longer. A webhook whose every attempt fails for three days is stopped; start it again from the dashboard once your endpoint is fixed.
Deliveries are at least once and in no promised order:
- You may receive an event twice. Keep the
webhook-idof what you handled and ignore it the second time. - Two events can arrive out of order. Order them by
sequence, or byoccurredAtfor when they happened.
Delivering again
From Integrations → Deliveries in the dashboard, or through the API:
- Try again one Delivery:
POST /api/event-deliveries/{id}/retry. - Deliver again a range of events:
POST /api/event-consumers/{id}/replaywithfromSequenceandtoSequence, at most 1,000 events at a time. Events recorded while the webhook was stopped are included.
An event delivered again keeps its id, so your endpoint recognises it.
Test events
Send test event in the dashboard, or POST /api/event-consumers/{id}/test with an optional type, sends an example of an Event Type to your endpoint at once, signed like any other, and shows what your endpoint answered. A test event has sequence 0 and an id starting with test_; it is not in your Event Log.
Event types
| Event Type | When |
|---|---|
order.created | An order was placed: on the store, in the dashboard, through the API or by an AI agent. |
order.updated | An order's customer, destination or notes were edited. |
order.confirmed | The customer confirmed the order, usually on the phone. |
order.unreachable | The customer could not be reached to confirm the order. |
order.preparing | The order is being prepared. |
order.ready | The order is packed and ready to hand to delivery. |
order.cancelled | The order was cancelled. |
order.line_returned | Units of an order line were refused at the door and came back. |
order.deleted | The order was deleted; the event carries it as it was. |
shipment.handed_off | The order was handed to a carrier or to one of the store's drivers. |
shipment.out_for_delivery | The parcel left for the customer. |
shipment.delivery_failed | An attempt to deliver the parcel failed. |
shipment.delivered | The parcel was delivered and paid for on delivery. |
shipment.return_started | The parcel is on its way back to the store. |
shipment.returned | The parcel is back with the store. |
shipment.cancelled | The shipment was cancelled before it was delivered. |
shipment.voided | The handoff was undone: the order is no longer with delivery. |
Each type's full schema is in the API reference under Webhooks.
Related
- Authentication for API keys and the
integrations:readandintegrations:managescopes - Errors for the error envelope
- Carrier webhooks for deliveries your carriers report