Webhooks Overview
Webhooks tell you what changed, so you do not have to poll for it. When something happens to one of your orders, we send an HTTP request to a URL you have registered with us.
Each delivery is a small signed notification carrying identifiers and a link — never the business data itself. You verify the signature, then fetch the current state from the API. Because the payload holds no data, it cannot go stale between our sending it and your reading it: however long the notification sat in a queue, the fetch that follows returns what is true now.
The envelope
Section titled “The envelope”Every event has this shape, and every future event type will too. The fields below are the whole contract — nothing else is sent.
{ "eventId": "84a4a78bd45d732c5241ee8180896267", "eventType": "orders.order.placed", "occurredAt": "2026-08-06T09:30:23.1945315+02:00", "version": 1, "entity": { "id": "2004148349", "type": "order" }, "resource": { "href": "/orders/v1/2004148349", "rel": "order:read" }}| Field | Type | Description |
|---|---|---|
eventId |
string | 32 lowercase hex characters. Stable for a given logical event — use it as your idempotency key. |
eventType |
string | Lowercase dotted domain.entity.action. |
occurredAt |
string | ISO 8601 with an explicit offset, e.g. 2026-08-06T09:30:23.1945315+02:00. Carries the originating offset; it is not normalised to UTC. |
version |
integer | Envelope version. Currently always 1. |
entity.id |
string | Identifier of the thing that changed. |
entity.type |
string | Kind of thing that changed, e.g. order. |
resource.href |
string | Path to fetch the current state, relative to https://<store-url>/public. |
resource.rel |
string | What the link is, e.g. order:read. |
Parse the envelope tolerantly. Ignore event types you do not recognise and ignore fields you do not know, so that new event types can be added without breaking your receiver.
Delivery
Section titled “Delivery”| Method | POST |
| Scheme | HTTPS only |
| Content type | application/json |
| Timeout | 10 seconds |
| Success | Any 2xx |
| Redirects | Not followed — a 3xx counts as a failure |
Every delivery carries:
| Header | Value |
|---|---|
webhook-id |
Same value as eventId |
webhook-timestamp |
Unix time in seconds when the delivery was signed |
webhook-signature |
One or more space-separated v1,<base64> signatures |
Anything other than a 2xx inside the 10-second budget is a failed delivery, and we retry
it on a backoff schedule spanning roughly 21 hours. See
Retries and failures for the schedule, what
happens when attempts run out, and how a subscription can be suspended.
What we guarantee
Section titled “What we guarantee”Delivery is at-least-once. The same eventId can arrive more than once — a retry, or a
redelivery of an event you have already handled. You must deduplicate on eventId and
treat a repeat as a no-op. There is no deduplication on our side.
There is no ordering. Do not infer sequence from arrival order. Each event retries
independently, so a later event can land before an earlier one. Do not sort by occurredAt
either — event types take it from different clocks, so it can disagree with the order in
which things actually happened. The envelope is thin precisely so that this does not matter:
whatever order notifications arrive in, fetching the current state converges on the same
answer.
One change can raise several events. Cancelling an order raises
orders.order.cancelled and an orders.order.lines.changed for the cancelled lines; the
final invoice on an order raises orders.order.invoiced and orders.order.completed. They
arrive as separate deliveries with different eventIds, so deduplication does not collapse
them into one. Treat any of them as a cue to re-fetch the order, and let the fetch settle
what actually changed. See
Follow an order lifecycle.
Only the SoldTo customer is notified. If you are ShipTo, BillTo, ContactTo or PlacedBy on an order but not SoldTo, you receive nothing for that order.
Event reference
Section titled “Event reference”Ten event types are published, all of them about orders. Every one carries the same identifiers, so a receiver that can handle one can handle all ten:
| Field | Value |
|---|---|
entity.type |
order |
entity.id |
The order number |
resource.rel |
order:read |
resource.href |
/orders/v1/{orderId} |
Fetch the order by resolving resource.href against https://<store-url>/public, with a
token carrying the order:read scope. See Orders overview for the
response shape. What differs between the event types is only why you were told, and
therefore what is worth re-reading.
orders.order.placed
Section titled “orders.order.placed”The order has been created and confirmed in our order system. Raised once per order.
Re-read the whole order.
This fires after the order is confirmed in our order system, which happens later than your checkout call returning. It is not a synchronous confirmation of your checkout submission — treat it as an asynchronous notification, and never block a checkout response waiting for it.
orders.order.lines.changed
Section titled “orders.order.lines.changed”A line was added, removed, substituted, repriced, quantity-changed or cancelled. Raised any number of times, and a single change can raise it more than once.
Re-read orderLines[].
orders.order.delivery.estimated
Section titled “orders.order.delivery.estimated”Expected delivery dates have been set or revised. Raised any number of times, and normally once shortly after the order is placed.
Re-read expectedDeliveries[].
orders.order.delivery.created
Section titled “orders.order.delivery.created”A delivery has been created for some or all of the lines. Raised once per delivery, so an order that ships in parts raises it more than once.
Re-read deliveries[].
orders.order.delivery.picked
Section titled “orders.order.delivery.picked”Picking has started on a delivery in the warehouse. Raised once per delivery, so an order that ships in parts raises it more than once.
Re-read deliveries[], in particular events.Picked.
Picking the first delivery closes the cancel-request path for the whole order, not just for that delivery’s lines — a request made after this has fired will not succeed. That does not preclude
orders.order.cancelledfiring later for other reasons. See Follow an order lifecycle.
orders.order.delivery.sent
Section titled “orders.order.delivery.sent”The delivery has left the warehouse. Raised once per delivery.
Re-read deliveries[], in particular events.Sent and trackingNumber.
orders.order.invoiced
Section titled “orders.order.invoiced”An invoice has been issued. Raised once per invoice, so an order invoiced in parts raises it more than once.
Re-read billings[] where type is Invoice. This is also what moves
orderLines[].quantity.fulfilled.
orders.order.credited
Section titled “orders.order.credited”A credit note has been issued. Raised any number of times, and it can arrive long after the order is finished.
Re-read billings[] where type is CreditNote.
orders.order.completed
Section titled “orders.order.completed”Every line has been invoiced or cancelled. Raised at most once per order.
Re-read the events map for Completed.
orders.order.cancelled
Section titled “orders.order.cancelled”The whole order has been cancelled. Raised at most once per order.
Re-read the events map for Cancelled.
For a full walkthrough of a single event — verify, deduplicate, acknowledge, then fetch the order — see Handle an order placed event. For how the types follow one another over an order’s life, see Follow an order lifecycle.
Signing secret
Section titled “Signing secret”Each subscription has its own signing secret, which you receive from Komplett when the subscription is set up. There is no API that returns it.
Treat it as you would any other credential: store it somewhere secret, keep it out of source control, and keep it out of your logs.
We can rotate the secret on request — ask if one is ever lost or exposed. During a rotation
the webhook-signature header carries both the new and the previous signature,
space-separated, so you can move to the new secret with no downtime and no dropped
deliveries.
See Verify a signature for working C# and Node verifiers, and for the mistakes that make verification fail.
Getting set up
Section titled “Getting set up”Webhook subscriptions are not self-service. They are configured by Komplett — not something you register through the API.
Contact your Komplett Account Manager with:
- the HTTPS URL deliveries should be sent to, and
- the event types you want to receive.
Event types can be listed individually, or selected with a trailing wildcard —
orders.order.* for every order event, or * for everything we publish. A wildcard keeps
your subscription current as new event types are added, provided your receiver ignores the
ones it does not recognise.
You get back a subscription identifier and a signing secret.
Webhooks need no scope of their own. The notification carries no business data, so the only
authorization that matters is the scope on the token you use to fetch resource.href —
order:read for an order.
Webhooks are delivered in the sandbox environment too, so you can prove your receiver works against a real order before going live. See Test your receiver.