Webhooks
A webhook tells another system the moment something happens in your store. When an order is paid, for example, your store posts a short JSON message about it to an address you give, such as your accounting system's or your warehouse's. That system doesn't have to keep asking your store what's new.
Use webhooks to:
- send paid orders to a warehouse or a fulfillment service;
- record orders and refunds in your accounting system;
- keep your stock count in step on a marketplace;
- email or message your team when stock runs low;
- add new customers to your mailing or CRM system.
| Menu | Commerce > Webhooks |
| Permission | Manage commerce webhooks |
| Feature | Commerce Webhooks |
What you see depends on your role. If the menu, a button or a setting on this page is missing, ask your administrator to give you the permission listed above or to turn on the feature. Only administrators can turn features on and change site settings.
The system that receives the messages is called the receiver. Whoever builds it needs the address it listens on, and the webhook's signing secret, to check that each message really came from your store.
Add a webhook
- Open Commerce > Webhooks and click Add webhook.
- Fill in the fields (below).
- Click Save.
The webhook was added. Copy its signing secret into the system that receives it. confirms it, and the webhook's page opens with its Signing secret. Copy it into the receiver.
| Field | What it does |
|---|---|
| Name | What you call it, such as the system it feeds: Warehouse or Accounting. Required. |
| URL | The receiver's full address, starting with https://, such as https://hooks.contoso.com/store. An address on your own network, such as https://10.0.0.5/ or https://192.168.1.20/, isn't accepted. A plain http:// address works only for a receiver on the same machine while the site runs in development. |
| On | Turn it off to stop sending without losing the webhook. Messages waiting to be tried again aren't sent either: they're marked Failed, with The webhook was turned off. |
| Events | The events to send (below). Tick Every event, those added later too to send them all, including events added to the store later. Choose at least one. |
Commerce > Webhooks lists your webhooks, each with whether it's On or Off and how many events it sends. Click a webhook's name, or Edit, to change it. Delete, on its page, removes it and its deliveries log: nothing more is sent to it.
The events
Each event has a name, which the receiver reads, and a label you see when choosing events. The events you can choose depend on the commerce features your store has turned on.
| Event | Label | Sent when | data holds |
|---|---|---|---|
order.created | Order placed | An order is made, such as when a buyer checks out their cart, before it's paid. It's sent once the order is saved and its stock is held: an order refused because something ran out sends nothing. Checking out the same cart again makes a new order and quietly cancels the earlier one, which sends no event. | order |
order.paid | Order paid | The order's checkout is completed. An order placed to pay offline, such as by bank transfer, is sent when it's placed, before the money arrives. | order |
order.canceled | Order canceled | The order is canceled, by your store or because its checkout didn't finish. | order, and reason |
order.refunded | Order refunded | Money is given back on the order, once for each refund, in full or in part. | order, and refund |
customer.created | Customer created | A customer record is made: for a guest who buys, the first time a signed-in buyer needs one (their first checkout, or opening My account), when you add one under Commerce > Customers, or when a recipe adds one. | customer |
shipment.shipped | Shipment shipped | A shipment is marked shipped, with its carrier and tracking. | shipment, orderNumber and customerEmail |
shipment.delivered | Shipment delivered | A shipment is marked delivered. | shipment, orderNumber and customerEmail |
stock.changed | Stock changed | What's available of a product or variant changes: a sale, units held while a buyer pays and let go again, units received, an adjustment or a count. Several changes saved together send one event. A move between locations doesn't change what's available, so it sends nothing. | stock, and previousAvailable |
stock.low | Stock running low | What's available drops to the item's low-stock level. | stock, and previousAvailable |
stock.sold-out | Stock sold out | Nothing is left, where there was before. It's sent instead of stock.low. | stock, and previousAvailable |
What's in data:
orderis the order as the commerce API gives it: itsid,number,status, the customer's name, email, phone and addresses, its lines, discounts, shipping, what paid toward it, its refunds and totals.reasonsays why an order was canceled:canceledByStore,checkoutCanceled,checkoutExpired,paymentFailed,abandoned, orunavailable(something it needed couldn't be held).refundis the refund just made: itsid,amount,reasonandcreatedUtc.customeris the customer as the API gives them: theirid,kind, name, email, phone and addresses.shipmentis the shipment as the API gives it: its number,status, carrier, tracking number and link, its lines and tracking events.orderNumberandcustomerEmailsay whose it is.stockis the stock item as the API gives it: itssku,title, what'sonHand,reservedandavailablein all and at each location.previousAvailableis what was available before.
What the receiver gets
Each event is a POST with a short JSON message. It says which event happened (event, such as order.paid), gives the event an id and the time it happened (createdUtc), and puts what the event is about in data, such as the whole order. Names and amounts are written the same way as in the commerce API, so a developer who knows the API reads them the same way. The Technical Manual shows a whole message.
Every message has these headers:
| Header | What it holds |
|---|---|
X-Webhook-Id | The event's id, the same as id in the body. It's the same on every try, so the receiver can drop a message it has already handled. order.created and customer.created always have the same id for the same order or customer, such as evt_order-created_4d1k8s0p2w, so even if one is raised twice, the receiver can drop the repeat. |
X-Webhook-Event | The event's name, such as order.paid. |
X-Webhook-Timestamp | When this try was signed, in seconds since 1 January 1970 (UTC). |
X-Webhook-Signature | v1= and the signature (below). |
Check the signature
Anybody can post to the receiver's address, so the receiver checks each message's signature before trusting it. The signature is an HMAC-SHA256, keyed with the webhook's signing secret, of the timestamp, a dot and the body, written in lowercase hexadecimal.
- Read the body exactly as it arrived, before parsing the JSON.
- Join the
X-Webhook-Timestampvalue, a dot (.) and the body. - Work out its HMAC-SHA256 with the whole signing secret (
whsec_included) as the key, in lowercase hex, and putv1=in front. - Compare it with
X-Webhook-Signature. Refuse the message if they differ, or if the timestamp is more than a few minutes old: an old message may be a recording someone sends again.
Each try is signed again when it's sent, so a retry has a fresh timestamp.
Your developers find this check written out in C# and Node.js in Checking the signature, in the Technical Manual.
Keep the signing secret in the receiver's settings or secret store, never in a web page or an app. If it may have leaked, click New secret on the webhook's page (below).
Deliveries and retries
A message is sent right after the change that raised it is saved, so a slow or broken receiver never slows down your buyers.
- The receiver has 15 seconds to answer. A
2xxstatus, such as200, means it took the message: it's Delivered. A redirect isn't followed: it counts as not taken. - Any other answer, no answer in time, or no connection, and the message is tried again: after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours, 12 hours and 24 hours. That's 8 tries over nearly two days. If the last one fails too, the message is Failed, and isn't tried again unless you send it again yourself.
- Every try sends the same body, with the same
id.
So the receiver should:
- answer quickly, with
200, and do slow work, such as calling other systems, afterwards; - drop repeats: a message can arrive more than once, for example when its answer was lost on the way back. Keep the ids it has handled, and skip one it has seen;
- not depend on order: a retried message can arrive after a later one. Read the
statusand times indata, or get the latest from the commerce API, rather than assuming events come in order.
The deliveries log
Click Deliveries, on the list or on a webhook's page, to see every message sent to the webhook, newest first. Each shows:
| Shown | What it means |
|---|---|
| Event | The event's name, such as order.paid. Point at it to see the event's id. |
| Pending, Delivered or Failed | Waiting to be sent or sent again, taken by the receiver, or given up on: after the last try, or because the webhook was turned off or its secret can't be read. |
| The answer | The status code of the receiver's last answer, such as 200 or 500. |
| Raised | When the event happened. |
| Attempts | How many times it has been tried. |
| Tried next | For a pending message, when it's tried again. |
| The error | For a message that wasn't taken, the receiver's answer (cut short) or why it couldn't be reached. |
The status filter in the list header (Filter by status) shows Any status, Pending, Delivered or Failed messages.
Click Send again to send a message now, for example once you've fixed the receiver. It starts over: if it fails, it's tried again on the same schedule. Sent again; the receiver answered 200. confirms it was taken.
If a message failed with The webhook's signing secret can't be read. Give it a new secret, then send it again., the site can no longer read the secret it stored, for example after its encryption keys were lost. Click New secret, put it in the receiver, and send the failed messages again.
Delivered and failed messages are kept for 30 days, then deleted.
Send a test event
On the deliveries log, click Send a test event to check the receiver is set up. It sends a webhook.ping event to this webhook only, signed like any other, whatever events the webhook takes. Its data holds the webhook's id and its Name, such as Warehouse.
The receiver answered 200. means it was taken. Otherwise the message says why, such as The receiver didn't take it: 401 Unauthorized, which usually means the receiver has a different secret. The test is listed in the log like any other message.
Change the signing secret
On the webhook's page, click New secret next to Signing secret. The webhook has a new signing secret. Deliveries are signed with it from now on, so put it in the receiver. From that moment, every message, including retries of earlier ones, is signed with the new secret. Put it in the receiver straight away; messages it refuses in between are tried again later.