Events
Event Body
Every event uses the same JSON body:
{
"id": "6f1c2a9e-0000-4000-8000-000000000001",
"type": "order.ready",
"schema_version": 1,
"occurred_at": "2026-10-14T03:15:00Z",
"company_id": "<COMPANY_ID>",
"outlet_id": "6f1c2a9e-0000-4000-8000-0000000000a1",
"data": {}
}
| Field | Meaning |
|---|---|
id | The event id. The same on every retry. Use it to drop duplicates. |
type | The event name, for example order.paid. |
schema_version | The body version, 1 today. |
occurred_at | When it happened in the merchant's business (see Event Time), in UTC with Z and whole seconds. |
company_id | The merchant's company id, as known from Mekari SSO. |
outlet_id | The outlet of the change, or null when the change is not tied to one outlet. Product and collection events can carry either. |
data | What changed. Its fields depend on the event and on the payload type (below). |
Every time in a webhook body is RFC 3339 in UTC with Z and whole seconds, for example 2026-10-14T03:15:00Z, the same as in API answers. Parse it with a date library, not by hand.
New fields can be added to the body and to data at any time. Ignore fields you do not know.
Event List
| Key | Event | Sent when | Thin data | With payload: full |
|---|---|---|---|---|
orders | order.paid | The sale is completed at the till; for a Terms sale check payment_status | Order record fields | The order, as GET /orders/{id} |
orders | order.payment_updated | A payment is made on a Terms (pay later) order, partial or full | Order record fields | The order, as GET /orders/{id} |
orders | order.returned, order.refunded, order.exchanged | A return, refund or exchange is made on an order | Order record fields of the original order, and the record id | The original order, as GET /orders/{id}, and the record id |
orders | order.accepted, order.rejected, order.preparing, order.ready, order.served, order.completed, order.cancelled | The kitchen ticket moves to accepted, rejected, preparing, ready, served, completed or cancelled | Kitchen fields | The order when order_id is set, otherwise thin |
products | product.created, product.updated, product.deleted | A product is created, changed or deleted | product_id | The product, as GET /products/{id} (thin for deleted) |
products | sku.changed | The SKU of a variant changes | product_id, variant_id, sku | Always thin |
collections | collection.created, collection.updated, collection.deleted, collection.reordered | A collection is created, changed, deleted or reordered | collection_id | The collection, as GET /collections/{id} (thin for deleted) |
inventory | inventory.availability_changed | A product variant at an outlet goes from in stock to out of stock, or back | product_id, variant_id, available | Always thin |
Events are sent for changes from every source: the cashier app, the back office, sales channels and other applications. Your own application's API writes are not sent back to it. The one exception is inventory.availability_changed: it does not record who caused it, so a stock write by your application can send you an availability change.
Order Fields
The thin data of an orders event has one of two shapes, by event group.
Order Record Events
order.paid, order.payment_updated, order.returned, order.refunded and order.exchanged report the order record:
| Field | Meaning |
|---|---|
order_id | The order id. For a return, refund or exchange, the id of the original order |
return_id | Only on order.returned: the id of the return |
refund_id | Only on order.refunded: the id of the refund |
exchange_id | Only on order.exchanged: the id of the exchange |
status | The order status, for example paid |
sales_type | How the order was sold, for example dine_in (same values as GET /orders/{id}) |
source | Where the order came from: pos, grabfood, gofood or the source_code of an application that creates orders |
points_earned | Loyalty points stored on the order when the event was written |
points_redeemed | Loyalty points stored on the order when the event was written |
payment_status | Only on order.paid and order.payment_updated: unpaid, partially_paid, overdue or settled, as on GET /orders/{id} |
amount_owed | Only on order.paid and order.payment_updated: what the buyer still owes, in whole units of currency; 0 when settled |
updated_at | When the order was last changed |
Read the record with GET /returns/{return_id}, GET /refunds/{refund_id} or GET /exchanges/{exchange_id}. With payload: full, data is the original order as GET /orders/{id} returns it, plus the post-sale record id (return_id, refund_id or exchange_id).
payment_status and amount_owed are null only on an event written before these fields existed.
Terms Payments
A Terms (pay later) sale is completed at the till before the buyer pays. Its order.paid has payment_status unpaid (or partially_paid when part was paid at the till) and the amount_owed. Every order that is not a Terms order is settled with amount_owed 0.
order.payment_updated is sent on every payment on a Terms order, partial or full. It carries the order record fields with the new payment_status and amount_owed: partially_paid while money is still owed, settled with 0 after the last payment. A Terms order that passes its due date becomes overdue on GET /orders/{id} without an event. Subscribe to order.payment_updated on the orders endpoint. With payload: full, data is the order as GET /orders/{id} returns it.
Kitchen Events
order.accepted, order.rejected, order.preparing, order.ready, order.served, order.completed and order.cancelled report the kitchen ticket:
| Field | Meaning |
|---|---|
order_id | The order id, or null while no order exists yet (for example an unpaid QR order) |
kitchen_order_id | The kitchen ticket id |
kitchen_status | The ticket status. It is the event name after order.: order.ready has kitchen_status ready |
sales_type | How the order was sold, for example dine_in |
updated_at | When the ticket changed |
A kitchen event has no status, source or points. Call GET /orders/{id} with order_id to read them.
Rules
order.paidmeans the sale is completed at the till. For a Terms sale, checkpayment_status: it can still beunpaid, andorder.payment_updatedfollows each payment. A kitchen event with anorder_iddoes not mean the order is paid: tickets can link an unpaid order. Wait fororder.paid.- Kitchen events report the ticket, not the order. Call
GET /orders/{id}withorder_idfor the order status, source and points. points_earnedandpoints_redeemedcan be0before the kitchen accepts the order. Returns, refunds and exchanges carry the points stored on the original order.- A new ticket sends nothing. A status change that is not allowed sends nothing.
sku.changed:product_idisnullwhen the variant cannot be found, for example because it was deleted.inventory.availability_changedreports a status, never a quantity.available: truemeans the variant can be sold at the outlet now. Call the inventory endpoints for quantities.- Product and collection events name only the id. Call
GET /products/{id}orGET /collections/{id}, or usepayload: full.
Event Time
| Event | occurred_at |
|---|---|
order.paid | When the payment was saved. For a sale made offline in the cashier app, the time of the sale on the device. |
order.payment_updated | When the Terms payment was saved |
order.returned, order.refunded, order.exchanged | When the return, refund or exchange was made |
| Kitchen events | When the ticket changed status |
| Other events | When the change was saved |
Because sales made offline arrive later, events do not always arrive in occurred_at order. See Order and Duplicates.
Thin and Full Payloads
Choose the payload type per endpoint with payload when you set it up.
thin(default):datanames what changed with ids and statuses, as in the tables above. Fetch the details with the API when you need them. This is the best choice for most integrations.full:datais the object the matchingGETreturns in itsdata, read when the event is sent. It shows the current state, not a copy of the moment of the change: if the order changed again before sending, you get the newer state.
full bodies differ from GET:
- Image URLs are empty strings (
""). CallGETwhen you need images. - Products carry no stock per outlet.
- For
order.returned,order.refundedandorder.exchanged, the order also hasreturn_id,refund_idorexchange_id, so you know which one happened. - For a kitchen event (
order.acceptedtoorder.cancelled) with anorder_id, thefullbody is the order. Itskitchen_statusis the current one, read when the event is sent. It can differ from the event name (order.ready) when the ticket has moved on since. Use the event name for what happened, andkitchen_statusfor where the ticket is now. - When the resource no longer exists, or cannot be read at that moment, the thin
datais sent instead. Check the fields before you use them. - Delete events,
sku.changed,inventory.availability_changedand kitchen events withoutorder_idare always thin.
A full order body contains the customer data that GET /orders/{id} returns. Store and protect it under the same rules as your API responses. Use thin if you do not need that data.
Examples
order.paid, thin:
{
"id": "6f1c2a9e-0000-4000-8000-000000000002",
"type": "order.paid",
"schema_version": 1,
"occurred_at": "2026-10-14T05:30:05Z",
"company_id": "<COMPANY_ID>",
"outlet_id": "6f1c2a9e-0000-4000-8000-0000000000a1",
"data": {
"order_id": "6f1c2a9e-0000-4000-8000-0000000000b1",
"status": "paid",
"sales_type": "dine_in",
"source": "pos",
"points_earned": 12,
"points_redeemed": 0,
"payment_status": "settled",
"amount_owed": 0,
"updated_at": "2026-10-14T05:30:05Z"
}
}
order.payment_updated, thin (a partial payment on a Terms order):
{
"id": "6f1c2a9e-0000-4000-8000-000000000006",
"type": "order.payment_updated",
"schema_version": 1,
"occurred_at": "2026-10-20T02:00:00Z",
"company_id": "<COMPANY_ID>",
"outlet_id": "6f1c2a9e-0000-4000-8000-0000000000a1",
"data": {
"order_id": "6f1c2a9e-0000-4000-8000-0000000000b2",
"status": "paid",
"sales_type": "retail",
"source": "pos",
"points_earned": 0,
"points_redeemed": 0,
"payment_status": "partially_paid",
"amount_owed": 20000,
"updated_at": "2026-10-20T02:00:00Z"
}
}
order.refunded, thin (the order record fields and the id of the refund):
{
"id": "6f1c2a9e-0000-4000-8000-000000000004",
"type": "order.refunded",
"schema_version": 1,
"occurred_at": "2026-10-14T07:10:00Z",
"company_id": "<COMPANY_ID>",
"outlet_id": "6f1c2a9e-0000-4000-8000-0000000000a1",
"data": {
"order_id": "6f1c2a9e-0000-4000-8000-0000000000b1",
"refund_id": "6f1c2a9e-0000-4000-8000-0000000000f1",
"status": "paid",
"sales_type": "dine_in",
"source": "pos",
"points_earned": 12,
"points_redeemed": 0,
"updated_at": "2026-10-14T07:10:00Z"
}
}
order.ready, thin (a kitchen event; order_id is null while no order exists for the ticket):
{
"id": "6f1c2a9e-0000-4000-8000-000000000005",
"type": "order.ready",
"schema_version": 1,
"occurred_at": "2026-10-14T06:45:30Z",
"company_id": "<COMPANY_ID>",
"outlet_id": "6f1c2a9e-0000-4000-8000-0000000000a1",
"data": {
"order_id": "6f1c2a9e-0000-4000-8000-0000000000b1",
"kitchen_order_id": "6f1c2a9e-0000-4000-8000-0000000000f2",
"kitchen_status": "ready",
"sales_type": "dine_in",
"updated_at": "2026-10-14T06:45:30Z"
}
}
inventory.availability_changed:
{
"id": "6f1c2a9e-0000-4000-8000-000000000003",
"type": "inventory.availability_changed",
"schema_version": 1,
"occurred_at": "2026-10-14T06:02:11Z",
"company_id": "<COMPANY_ID>",
"outlet_id": "6f1c2a9e-0000-4000-8000-0000000000a1",
"data": {
"product_id": "6f1c2a9e-0000-4000-8000-0000000000c1",
"variant_id": "6f1c2a9e-0000-4000-8000-0000000000d1",
"available": false
}
}
The webhook.test event sent by POST /webhooks/endpoints/{key}/test has the same body with type webhook.test, outlet_id null and data {}.