Webhooks
Instead of asking for an order's status again and again, let CompuDime tell you. When an order you sent with POST /v1/orders reaches the store's POS, or a step in the store, CompuDime sends a POST to your receiver.
Register your own receiver
Register a receiver with your order key, a key with the order-intake capability (POST /v1/webhooks). Name the events you want. The answer is 201 with the receiver and its signing secret. The secret is shown in that answer only, never again, not by the API and not in the store owner's console: store it at once and keep it like a password. Managing receivers is free.
curl -s -X POST "https://api.compudime.com/v1/webhooks" \
-H "Authorization: Bearer $PUBLIC_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url": "https://hooks.example.com/compudime/orders", "events": ["order.dispatched", "order.parked"]}'
{
"requestId": "register-receiver-1",
"webhook": {
"id": "7b0c9a1e-3f52-4c1d-9a7e-2d4f6b8c0e13",
"url": "https://hooks.example.com/compudime/orders",
"events": [
"order.dispatched",
"order.parked"
],
"active": true,
"mode": "live",
"secretPrefix": "whsec_3f9a1c",
"createdAt": "2026-10-13T15:00:00Z",
"updatedAt": "2026-10-13T15:00:00Z"
},
"secret": "whsec_3f9a1c...",
"secretNote": "This signing secret is shown once, in this answer, and never again: store it now, like a password. If you lose it, rotate it (POST /v1/webhooks/{webhookId}/rotate-secret)."
}
- The URL must start with
https://, reach a public address, be at most 2048 characters long, and carry no user name or password. Private, loopback and link-local addresses are refused when you register, and again each time CompuDime connects. A URL that breaks a rule is400 bad_request, and so is an event that does not exist. - Your integration may keep 5 receivers per key mode, counting any CompuDime registered for you. One more is
409 webhook_limit_reached. - Registering a URL you already have is
409 webhook_exists, and the detail names that receiver. So a retry after a lost answer never registers twice. If the lost answer took the secret with it, rotate the secret (below). - A receiver belongs to your key's integration. Another key's receivers, even of the same store, are not yours: their ids answer
404 webhook_not_found.
Send a test event
Before the first order, check that your receiver verifies the signature (POST /v1/webhooks/{webhookId}/test):
curl -s -X POST "https://api.compudime.com/v1/webhooks/$WEBHOOK_ID/test" \
-H "Authorization: Bearer $PUBLIC_API_KEY"
The answer is 202 with the delivery. Within about a minute CompuDime posts a webhook.test event to the receiver, with the same headers and signature as an order event and the same retries. It is about no order: check the signature, answer 2xx, and do nothing else with it.
{
"id": "c2e8f4a0-6b1d-4e3f-8a9c-0d2e4f6a8b1c",
"type": "webhook.test",
"createdAt": "2026-10-13T15:01:00Z",
"data": {
"webhookId": "7b0c9a1e-3f52-4c1d-9a7e-2d4f6b8c0e13",
"mode": "live",
"requestedAt": "2026-10-13T15:01:00Z",
"requestId": "test-receiver-1",
"note": "A test event you asked for with POST /v1/webhooks/{webhookId}/test. It is not about an order: check the signature, answer 2xx, and do nothing else with it."
}
}
See how it went with GET /v1/webhooks/{webhookId}/deliveries: each delivery's state (pending, failed, delivered or dead), its attempts, and the status and error of the last one. You may send 10 test events to a receiver in an hour; more is 429 rate_limited with Retry-After. A receiver the store owner turned off is 409 webhook_inactive.
List, rotate and remove
GET /v1/webhookslists your receivers, each with its last delivery. It never shows a secret:secretPrefixis its first characters, to tell secrets apart.POST /v1/webhooks/{webhookId}/rotate-secretanswers a new secret, once. The old one stops working at the next delivery attempt, retries included, so switch your receiver to the new one at once.DELETE /v1/webhooks/{webhookId}removes a receiver. Nothing is sent to it again, not even a retry that was waiting.- A revoke removes receivers the same way. When the store owner withdraws your approval, every receiver registered under it goes. When you revoke a key that reads through no approval, the receivers it registered go. Replacing or revoking one key of an approval removes none: its receivers stay with the approval for its other and next keys.
Test keys: a receiver you register with a test key gets only the test events you send it, because a test key's orders never send a webhook. A test key never sees a live key's receivers, and a live key never sees a test key's. Receivers CompuDime staff registered for you are live receivers.
CompuDime staff can still register a receiver for you: write to support@compudime.com with the store, the URL and the events.
Events
| Event | When |
|---|---|
order.dispatched | The order landed in the store's POS: data.invoice links the POS invoice (by feed identity), and data.posOrderRef is the store's own reference, not an invoice number. Treat it as final for dispatch; the fulfillment events (order.picked and the ones after it) may follow. |
order.parked | Dispatch stopped (a permanent POS rejection, the retry bound, or the store's operator parked the order): data.dispatchError says why. The owner can retry it from the console, after which order.dispatched may follow. |
order.picked | The store finished picking the order: data.fulfillment.pickedAt. Sent once per order and receiver, when the order status sync first sees the step (it reads the store every few minutes, so a step can be announced minutes after it happened, and two steps can arrive together or out of order: go by data.fulfillment.state). data.fulfillment is the whole fulfillment object as the order reads show it; data.invoice links the POS invoice. Only for orders at stores whose agent reads order status for the platform. |
order.out_for_delivery | A driver took the order: data.fulfillment.outForDeliveryAt, and data.fulfillment.eta when the store recorded one. Sent once per order and receiver, when the order status sync first sees the step (it reads the store every few minutes, so a step can be announced minutes after it happened, and two steps can arrive together or out of order: go by data.fulfillment.state). data.fulfillment is the whole fulfillment object as the order reads show it; data.invoice links the POS invoice. Only for orders at stores whose agent reads order status for the platform. |
order.delivered | The store recorded the delivery as done: data.fulfillment.deliveredAt. Final. Sent once per order and receiver, when the order status sync first sees the step (it reads the store every few minutes, so a step can be announced minutes after it happened, and two steps can arrive together or out of order: go by data.fulfillment.state). data.fulfillment is the whole fulfillment object as the order reads show it; data.invoice links the POS invoice. Only for orders at stores whose agent reads order status for the platform. |
order.completed | An order that is not a delivery was rung up at the store's register (the customer checked out): data.fulfillment.completedAt. Final. Sent once per order and receiver, when the order status sync first sees the step (it reads the store every few minutes, so a step can be announced minutes after it happened, and two steps can arrive together or out of order: go by data.fulfillment.state). data.fulfillment is the whole fulfillment object as the order reads show it; data.invoice links the POS invoice. Only for orders at stores whose agent reads order status for the platform. |
order.canceled | The store deleted the order from its POS, which is how the POS cancels an order: data.fulfillment.canceledAt. Final. Sent once per order and receiver, when the order status sync first sees the step (it reads the store every few minutes, so a step can be announced minutes after it happened, and two steps can arrive together or out of order: go by data.fulfillment.state). data.fulfillment is the whole fulfillment object as the order reads show it; data.invoice links the POS invoice. Only for orders at stores whose agent reads order status for the platform. |
webhook.test | Sent only when you ask: POST /v1/webhooks/{webhookId}/test. It is about no order; use it to check that your receiver verifies the signature and answers 2xx. |
Each event is sent at most once per order and receiver. Steps in the store are seen every few minutes, so two can arrive together or out of order: go by data.fulfillment.state. order.parked is not always the last word: the store can retry a parked order, and you then get order.dispatched.
An order sent with a test key is simulated and never reaches the store, so it sends no webhooks. See Test orders.
The body
Every delivery has the same shape: id, type, createdAt and data. The full shape is OrderWebhookEvent.
{
"id": "5d2f0f0c-8a1c-4d5e-9b52-1c4a6d0f7e21",
"type": "order.dispatched",
"createdAt": "2026-09-16T12:00:07Z",
"data": {
"orderId": "1a2b3c4d-0000-4000-8000-000000000001",
"externalOrderId": "web-1001",
"status": "accepted",
"dispatchState": "dispatched",
"posOrderRef": "OrderExchange CDAPI:int_123#13697638",
"dispatchError": "",
"attempts": 0,
"invoice": {
"externalId": "web-1001",
"processorCode": "CDAPI:int_123",
"url": "https://api.compudime.com/v1/invoices/external/web-1001?processorCode=CDAPI%3Aint_123"
}
}
}
data.invoicelinks the POS invoice the order became. Read it with a sales-data key of the store, not your order key.data.posOrderRefis the store's own reference. It is not a dependable invoice number: followdata.invoiceinstead.- The step events add
data.fulfillment, the same object the order reads show.
Headers
| Name | Type | Description |
|---|---|---|
X-CompuDime-Event required | string | The event name, the same as type in the body. |
X-CompuDime-Delivery required | string | The delivery id, the same as id in the body. |
X-CompuDime-Timestamp required | string | When this attempt was sent (RFC 3339, UTC). |
X-CompuDime-Signature required | string | sha256= followed by the hex HMAC-SHA256 of the raw request body under the receiver's signing secret. |
Check the signature
CompuDime POSTs this body to each active live receiver registered for your order-intake integration (by you through POST /v1/webhooks, or by CompuDime) and subscribed to order.dispatched. Verify X-CompuDime-Signature (sha256=<hex HMAC-SHA256 of the raw body under the receiver's secret>) before trusting it, and deduplicate on id: retries and redeliveries carry the same id. Deliveries are not billed. A test key's orders are simulated and never send a webhook.
Compute HMAC-SHA256 over the raw bytes you received (not over a parsed and re-written object), with your signing secret as the key. Write it in hex, put sha256= in front, and compare it with X-CompuDime-Signature in constant time.
// Node, Express with a raw body
import crypto from "node:crypto";
app.post("/compudime/webhooks", express.raw({ type: "application/json" }), (req, res) => {
const expected = "sha256=" + crypto.createHmac("sha256", process.env.COMPUDIME_WEBHOOK_SECRET).update(req.body).digest("hex");
const given = req.get("X-CompuDime-Signature") ?? "";
if (given.length !== expected.length || !crypto.timingSafeEqual(Buffer.from(given), Buffer.from(expected))) {
return res.status(401).end();
}
const event = JSON.parse(req.body);
// skip event.id if you handled it before, then act on event.type
res.status(204).end();
});
# Python
import hashlib, hmac
def verify(secret: str, raw_body: bytes, header: str) -> bool:
expected = "sha256=" + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(header.strip(), expected)
Answer, and retries
Any 2xx within 10 seconds acknowledges the delivery; the body is ignored. Anything else, a timeout or a redirect is a failed attempt and is retried after 1 minute, 5 minutes, 30 minutes, 2 hours and 12 hours.
Redirects are never followed. Retries carry the same id, so record the ids you handled and answer 2xx again without doing the work twice. Deliveries are not billed.