Webhooks
Where Mekari POS sends event notifications for your application. Each
application manages its own endpoints, one per key (orders, products,
collections, inventory); another application of the same merchant
never sees them. These calls change your own settings, so they need no
X-User-ID and no Idempotency-Key.
Event body
Every event is one JSON object with the same keys (schema WebhookEvent):
id, type, schema_version, occurred_at, company_id, outlet_id
and data.
ididentifies the change. It is the same on every retry and for every endpoint that receives the change. It is also sent asX-Mekari-Event-ID.company_idis the merchant's company id, the one you know from Mekari SSO.outlet_idis the outlet of the change, ornullfor company-wide events.occurred_atis business time in UTC (Z), whole seconds.schema_versionis1. New fields may be added todatawithout a new version, so ignore fields you do not know.
Thin data
With payload set to thin (the default), data only names the
resource. Call the matching GET to read it.
| Events | data |
|---|---|
order.paid, order.payment_updated, order.returned, order.refunded, order.exchanged | WebhookOrderData |
order.accepted, order.rejected, order.preparing, order.ready, order.served, order.completed, order.cancelled | WebhookKitchenData |
product.created, product.updated, product.deleted | WebhookProductData |
sku.changed | WebhookSkuData |
collection.created, collection.updated, collection.deleted, collection.reordered | WebhookCollectionData |
inventory.availability_changed | WebhookAvailabilityData |
Notes on the order and kitchen events:
- Order record events carry the order fields;
order.returned,order.refundedandorder.exchangedaddreturn_id,refund_idorexchange_id. Theirorder_idis the original order, and the points are the ones stored on it when the event was written. order.paidmeans the sale is completed at the till. For a Terms sale (pay later) checkpayment_status: it can still beunpaid.order.payment_updatedis sent on every Terms payment, partial or full. Both carrypayment_statusandamount_owed.- Kitchen events carry
kitchen_order_id,kitchen_status,order_id,sales_typeandupdated_at.kitchen_statusis the event name afterorder.:order.readyhaskitchen_statusready.order_idisnulluntil a POS order exists for the ticket. A setorder_iddoes not mean the order is paid:order.paidsays the sale is completed at the till; for a Terms sale checkpayment_status. - On
sku.changed,product_idisnullwhen the variant no longer exists. inventory.availability_changednever carries a quantity.
Full data
With payload set to full (not for the inventory endpoint), data is
the same object that the matching GET returns in its own data:
GET /orders/{id}, GET /products/{id} or GET /collections/{id}. It is
read when the event is sent, so it shows the resource as it is then, not
as it was at the time of the change. For order.returned,
order.refunded and order.exchanged, the order also has return_id,
refund_id or exchange_id, so you still know which one happened.
- Image URLs are empty strings, never
null. CallGETto read images. - A product carries no stock, because stock is per outlet. Read it with
GET /inventory/stock/{item_type}/{item_id}. - An order carries the same customer (member) data as
GET /orders/{id}. - These are always thin, even on a
fullendpoint:*.deletedevents, any event whose resource no longer exists,sku.changed,inventory.availability_changed, and a kitchen event while itsorder_idis stillnull. Onceorder_idis set, a kitchen event on afullendpoint carries the full order. - Rarely a
fullendpoint receives a thin body, when the resource could not be read at that moment. Check which fields are present, and callGETwhen you need the rest.
Headers and signature
| Header | Value |
|---|---|
X-Mekari-Signature | v1= and the hex HMAC-SHA256 of the signed string |
X-Mekari-Timestamp | Unix time in seconds, when this attempt was signed |
X-Mekari-Key-Version | The version of the secret that signed it |
X-Mekari-Delivery-ID | Identifies the delivery of this event to this endpoint |
X-Mekari-Event-ID | The event id |
User-Agent | mekaripos-webhooks/1.0 |
The signed string is, in this order, joined with a dot:
timestamp "." delivery_id "." event_id "." key_version "." raw_body.
The key is the signing secret of that key version. Use the raw request
body bytes, before any JSON parsing.
- Read the headers and the raw body.
- Reject the request when the timestamp is older than 5 minutes.
- Pick the secret that matches
X-Mekari-Key-Version, build the string, compute the HMAC-SHA256 and compare it with thev1value in constant time.
After rotate-secret (or a PUT that changes the url), the new
version signs every request from that moment, retries included. Keep the
previous secret for 24 hours only to verify requests that were already on
their way, then delete it. Deploy the new secret quickly: until then your
receiver cannot verify new requests.
Delivery, retries and pause
Answer with any 2xx status within 10 seconds. Mekari POS never follows
redirects: a 3xx answer is a final failure, and the Location is not
requested.
- A transport error, a timeout,
408,429and any5xxare retried. Any other4xx, and any3xx, fail at once and are not retried. - There are at most 8 attempts: the first at once, then after about
30 seconds, 2 minutes, 10 minutes, 30 minutes, 1 hour, 3 hours and
6 hours (about 10 hours 42 minutes in all). A
Retry-Afterheader with a number of seconds is honoured when it is longer than that delay, up to 1 hour. - A delivery that fails for good counts as one failure. After 10 failures
in a row, with no success between, the endpoint is
pausedand sends nothing. APUTon the endpoint makes itactiveagain; deliveries still waiting from before the pause are cancelled, not sent. Any2xxresets the count. - Retries are made about once a minute, so the real delay can be up to a minute longer than the times above.
What to expect
- Delivery is at least once. The same event can arrive more than once:
dedupe on
id. - There is no ordering. Order events by
occurred_at, and kitchen events by theirkitchen_status. - Events are not replayed. After an outage on your side, or after the
endpoint was paused, reconcile with
GET /orders,GET /productsandGET /collections. - You do not receive the change that your own application made through
this API. Other applications of the same merchant do. This does not
apply to
inventory.availability_changed: it carries no origin, so you may receive an availability change caused by your own stock write. - Nothing is sent when the merchant's Public API add-on is off or expired.
📄️ List webhook endpoints
Your webhook endpoints, one per key you have set. An endpoint you deleted is still listed, with status `disabled`. The signing secret is never part of this list.
📄️ Set the orders webhook endpoint
Creates or updates your `orders` endpoint, which receives order and kitchen-status events. Answers `201` when the endpoint is new and `200` when it existed.
📄️ Set the products webhook endpoint
Creates or updates your `products` endpoint, which receives product and SKU events. Answers `201` when the endpoint is new and `200` when it existed.
📄️ Set the collections webhook endpoint
Creates or updates your `collections` endpoint, which receives collection events. Answers `201` when the endpoint is new and `200` when it existed.
📄️ Set the inventory webhook endpoint
Creates or updates your `inventory` endpoint, which receives product availability events. Answers `201` when the endpoint is new and `200` when it existed.
📄️ Delete a webhook endpoint
Stops the endpoint: it sends nothing more, its pending deliveries are cancelled and its signing secrets stop working. An unknown `key`, and an endpoint you have not set or have deleted, answer `404 NOT_FOUND`. Setting the endpoint again later issues a new secret.
📄️ Rotate a webhook signing secret
Issues a new signing secret for the endpoint.
📄️ Send a test webhook
Sends one signed `webhook.test` event to the endpoint now and waits up to 10 seconds for the answer. It signs the event the same way as real events.