When an integration needs to know about a new order there are two routes: ask the API over and over, or have a message sent when something happens. A CMS supports both, but only one of them is sustainable.
This one is for developers. If you are after a finished product, start at Notify instead.
Why not polling
Fetching the order list every minute spends your request budget on a question whose answer is almost always “nothing new”. With one shop it passes. With two hundred shops it is two hundred wasted calls a minute — and when an order really does arrive you are up to a minute behind.
A webhook reverses the direction: the platform calls you.
Registration
POST /api/webhooks with a data array where each item carries event and
url. At most 50 webhooks per call, and a URL may be up to 2000 characters.
An important constraint: one URL per event. Registering the same event twice
fails with 422 Webhook already exists for this event, so your registration has
to be idempotent on your side — GET /api/webhooks first, then add only what is
missing.
Some events exclude each other. order:update cannot be registered alongside
order:cancel, nor customer:update alongside customer:disableOrders or
customer:enableOrders; the combination returns 409.
What arrives
The notification is a short JSON:
{
"eshopId": 222651,
"event": "order:create",
"eventCreated": "2019-01-08T15:13:39+0100",
"eventInstance": "2018000057"
}
eventInstance identifies the entity — an order code, a product GUID, a list for
mass events. The entity body is not in the notification. The sendPayload: "full" parameter exists, but only for events marked payload supported, and the
current OpenAPI marks none. So the order detail is always a separate call.
The signature
Each notification can carry a signature header —
hash_hmac('sha1', rawBody, signatureKey) in hex. You obtain the key with
POST /api/webhooks/renew-signature-key; it is per addon × e-shop and every
call generates a new one.
The catch: until you call that endpoint, notifications carry no signature. Call it right after installation and store the key encrypted. And verify the signature against the raw request body, not against whatever comes out of the JSON parser — any re-serialisation of the object breaks it.
Reject an unsigned notification. Fail closed, not open.
Four seconds
A notification must be answered with 200 within 4 seconds. Miss that and
The CMS retries every 15 minutes, three times at most, then marks the
notification inactive — meaning it is gone for good. There is a seven-day
history at GET /api/webhooks/notifications.
Which leaves exactly one possible handler shape:
- verify the signature,
- persist the raw event,
- enqueue a job,
- return 200.
Nothing else. No CMS API call, no message composition, no posting to Slack — all of that belongs in a worker. Aim under 500 ms, not “we fit inside four seconds”.
Idempotency
The CMS sends no delivery ID. So when a notification arrives a second time — and on retry it will — there is nothing telling you it is the same one. You have to build the key yourself out of what the message carries:
(addon, eshopId, event, eventCreated, eventInstance)
That tuple is stable across retries. Store it behind a unique index and drop the second write silently.
Rate limit
The API uses a leaky bucket of 200 drops draining at 10 per second, so 600 requests a minute, counted per addon × e-shop. Another addon on the same shop has its own bucket, so you are not competing with it.
X-RateLimit-Bucket-Filling: 130/200 is always present; Retry-After only when
the bucket is full — and it is an HTTP-date in GMT, not a number of seconds.
Parse it as a number and you get NaN and start spinning requests for nothing.
In short
CMS webhooks are straightforward, but there are four places where an
integration breaks: a missing signature key, a slow handler, missing idempotency
and a misparsed Retry-After. Get those four right and the rest is just
composing the message.
That is what the shared CMS kit in our monorepo handles, and what Notify is built on.