Adding one
In the dashboard, open Developers, then Webhooks, and press Add Webhook. Enter the address, tick the events, and save. Or add one with the API. You are shown the endpoint’s signing secret once. It startsshoply_whsec_. Keep it: your server needs it to check that a delivery is Shoply’s. A store holds up to 20 endpoints.
The address has to be https, on port 443, 80, 8080 or 8443, and reachable from the internet. An address inside a private network is refused.
What is sent
X-Shoply-Delivery is this delivery’s ID. It is the same on every retry and every resend of it, which makes it the thing to remember in order to take a delivery once.
Checking the signature
Anybody can post to your address. Only Shoply knows your endpoint’s secret, so check every delivery before you believe it. The signature isv1= followed by the HMAC-SHA256, in lowercase hex, of the timestamp, a dot, and the body exactly as it arrived, under your secret:
- Use the raw body, the bytes as they arrived. Parsing the JSON and writing it out again changes them, and the signature will never match.
- Compare in constant time, with the function your language has for it.
- Refuse an old timestamp. The timestamp is signed, so a delivery somebody captured cannot be sent to you again later with a new one. Five minutes is a good allowance.
webhooks.verify(body, headers, secret) does all three and answers the event, with shoplygg/shoply-php, Shoply\Webhooks::verify($body, $headers, $secret), and with shoply for Python, shoply.webhooks.verify(body, headers, secret). Without them:
Dynamic delivery is signed the same way, under the same two headers, with the store’s delivery signing secret. One function checks both.
Answering
Answer with any2xx status and the delivery is taken. What you write in the body does not matter, though its first 2,000 characters are kept in the delivery log for you to read.
Answer within five seconds. Take the delivery, answer, and do the work afterwards. A server that takes longer is counted as not having answered.
A redirect is not followed. If your address moves, save the new one on the endpoint.
When your server does not take it
A delivery that is not taken is tried again, eight times in all, at gaps that widen:
That is a little over 22 hours from first to last, so a server that was down overnight still gets what it missed. After the eighth try the delivery is marked
failed and left. One that your server has still not taken 24 hours after its event is marked failed too, tried eight times or not.
Every try is written down. The delivery log, in the dashboard and in the API, shows the status your server answered with and the start of what it said, or why it could not be reached: it did not answer in time, it refused the connection, its certificate is wrong. Resend on any delivery sends the same body again, under the same delivery ID, signed afresh.
An endpoint that keeps failing
One delivery that is not taken is treated as a bad moment, and nothing else changes. From the second in a row the endpoint is held as a whole: nothing waiting for it is sent, and an event that happens meanwhile waits with the rest. When the hold is over, one delivery is tried. If your server takes it, everything that was waiting untried is sent at once. If it does not, the endpoint is held again:
What counts as one more in the row is a delivery refused for the first time, or a server that gives no answer at all. A delivery your server has refused before and answers with an error again does not make the hold longer, so one event that your handler cannot take does not hold up the others for an hour. The count starts again when your server takes a delivery, or after a day with nothing refused.
So a server that is down is tried once an hour at most, however many events it has missed, and is sent them within the hour of coming back. To have them sooner, press Send a test on the endpoint: a test that your server takes ends the hold. Saving the endpoint with a new address has one delivery tried at once.
Waiting spends none of a delivery’s eight tries, and a delivery your server has already refused keeps to its own times in the table above.
If the trouble is on Shoply’s side, nothing is held against your endpoint and no try is spent: the delivery waits a minute and is sent then. Its row in the log says so.
When 20 deliveries in a row have been given up on, the endpoint is paused: nothing more is sent to it, and you are shown so in the dashboard. Fix your server and switch the endpoint back on. Deliveries that were already waiting for it are sent then. What happened while it was paused was not kept for it, so read it back from the API.
How fast they are sent
A delivery is sent as its event happens. An endpoint is sent its deliveries one at a time: the next is sent when the one before it has been answered. So answer quickly, and do the work afterwards. A server that takes two seconds or more to answer is sent its deliveries from a queue for the next ten minutes, and not as each event happens. The queue runs every minute, so they can arrive up to a minute late. A store sends one server at most 1,200 deliveries a minute, whichever of the store’s endpoints they are for. The server is counted by the addresses its name points at, so several names for one machine are one server. Past it no try is spent: a delivery waits a few seconds and is sent then.How much can wait
Up to 5,000 deliveries wait for one endpoint. While that many are waiting, a new event is not queued for it, and events are queued again as soon as fewer are. An endpoint that takes what it is sent has nothing waiting, so this is only ever reached by one that has refused for hours in a busy store. Read what it missed back from the API. The delivery log keeps 30 days, and a store’s newest 50,000 deliveries that were taken or given up on. One that is still waiting is never removed from it.Getting it right
Expect a delivery more than once
Expect a delivery more than once
A retry after your server took a delivery but answered too slowly, or a resend, brings the same delivery again. Remember
X-Shoply-Delivery and do nothing the second time.Do not rely on the order they arrive in
Do not rely on the order they arrive in
Deliveries are sent as events happen, and a retry can land after a later event. Go by
createdAt, or read the thing back from the API.A webhook tells a server, not a page
A webhook tells a server, not a page
The Checkout Kit and the embed tell the page they are on that an order was paid, so that the page can say thank you. That is not proof of anything. Hand over goods of your own when your server receives
order.paid.Trust the signature, not the address
Trust the signature, not the address
Deliveries do not come from one fixed IP address, and the addresses they do come from are shared with other senders. Do not allowlist an address in place of checking the signature: the signature is what says a delivery is Shoply’s.
Check up now and then
Check up now and then
Servers go down for longer than a day. Read orders back now and then to catch what was missed.
Trying it
Send a test on an endpoint posts awebhook.test event to it at once, signed like any other, and shows you what your server answered. It is sent whatever the endpoint subscribes to, and whether or not the endpoint is being held.
