Skip to main content

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": {}
}
FieldMeaning
idThe event id. The same on every retry. Use it to drop duplicates.
typeThe event name, for example order.paid.
schema_versionThe body version, 1 today.
occurred_atWhen it happened in the merchant's business (see Event Time), in UTC with Z and whole seconds.
company_idThe merchant's company id, as known from Mekari SSO.
outlet_idThe outlet of the change, or null when the change is not tied to one outlet. Product and collection events can carry either.
dataWhat changed. Its fields depend on the event and on the payload type (below).
note

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​

KeyEventSent whenThin dataWith payload: full
ordersorder.paidThe sale is completed at the till; for a Terms sale check payment_statusOrder record fieldsThe order, as GET /orders/{id}
ordersorder.payment_updatedA payment is made on a Terms (pay later) order, partial or fullOrder record fieldsThe order, as GET /orders/{id}
ordersorder.returned, order.refunded, order.exchangedA return, refund or exchange is made on an orderOrder record fields of the original order, and the record idThe original order, as GET /orders/{id}, and the record id
ordersorder.accepted, order.rejected, order.preparing, order.ready, order.served, order.completed, order.cancelledThe kitchen ticket moves to accepted, rejected, preparing, ready, served, completed or cancelledKitchen fieldsThe order when order_id is set, otherwise thin
productsproduct.created, product.updated, product.deletedA product is created, changed or deletedproduct_idThe product, as GET /products/{id} (thin for deleted)
productssku.changedThe SKU of a variant changesproduct_id, variant_id, skuAlways thin
collectionscollection.created, collection.updated, collection.deleted, collection.reorderedA collection is created, changed, deleted or reorderedcollection_idThe collection, as GET /collections/{id} (thin for deleted)
inventoryinventory.availability_changedA product variant at an outlet goes from in stock to out of stock, or backproduct_id, variant_id, availableAlways 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:

FieldMeaning
order_idThe order id. For a return, refund or exchange, the id of the original order
return_idOnly on order.returned: the id of the return
refund_idOnly on order.refunded: the id of the refund
exchange_idOnly on order.exchanged: the id of the exchange
statusThe order status, for example paid
sales_typeHow the order was sold, for example dine_in (same values as GET /orders/{id})
sourceWhere the order came from: pos, grabfood, gofood or the source_code of an application that creates orders
points_earnedLoyalty points stored on the order when the event was written
points_redeemedLoyalty points stored on the order when the event was written
payment_statusOnly on order.paid and order.payment_updated: unpaid, partially_paid, overdue or settled, as on GET /orders/{id}
amount_owedOnly on order.paid and order.payment_updated: what the buyer still owes, in whole units of currency; 0 when settled
updated_atWhen 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:

FieldMeaning
order_idThe order id, or null while no order exists yet (for example an unpaid QR order)
kitchen_order_idThe kitchen ticket id
kitchen_statusThe ticket status. It is the event name after order.: order.ready has kitchen_status ready
sales_typeHow the order was sold, for example dine_in
updated_atWhen the ticket changed

A kitchen event has no status, source or points. Call GET /orders/{id} with order_id to read them.

Rules​

  • order.paid means the sale is completed at the till. For a Terms sale, check payment_status: it can still be unpaid, and order.payment_updated follows each payment. A kitchen event with an order_id does not mean the order is paid: tickets can link an unpaid order. Wait for order.paid.
  • Kitchen events report the ticket, not the order. Call GET /orders/{id} with order_id for the order status, source and points.
  • points_earned and points_redeemed can be 0 before 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_id is null when the variant cannot be found, for example because it was deleted.
  • inventory.availability_changed reports a status, never a quantity. available: true means 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} or GET /collections/{id}, or use payload: full.

Event Time​

Eventoccurred_at
order.paidWhen the payment was saved. For a sale made offline in the cashier app, the time of the sale on the device.
order.payment_updatedWhen the Terms payment was saved
order.returned, order.refunded, order.exchangedWhen the return, refund or exchange was made
Kitchen eventsWhen the ticket changed status
Other eventsWhen 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): data names 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: data is the object the matching GET returns in its data, 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 (""). Call GET when you need images.
  • Products carry no stock per outlet.
  • For order.returned, order.refunded and order.exchanged, the order also has return_id, refund_id or exchange_id, so you know which one happened.
  • For a kitchen event (order.accepted to order.cancelled) with an order_id, the full body is the order. Its kitchen_status is 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, and kitchen_status for where the ticket is now.
  • When the resource no longer exists, or cannot be read at that moment, the thin data is sent instead. Check the fields before you use them.
  • Delete events, sku.changed, inventory.availability_changed and kitchen events without order_id are always thin.
warning

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 {}.