Skip to main content

Creating Orders

Your application can send paid delivery, takeaway and pickup orders to a merchant's outlet. Mekari POS prices the order, records it with its payments, and puts a ticket on the outlet's kitchen board. Your customer has already paid in your application, so these orders are never open or unpaid.

StepCall
1PUT /order-source, once per merchant
2POST /orders, for each order
3GET /orders/{id} and webhooks, to follow the order

Register Your Source​

Before the first order, tell Mekari POS who you are. Send a source_code and a display_name:

{ "source_code": "harvest", "display_name": "Harvest" }
  • source_code has 2 to 32 characters: lowercase letters, digits and hyphens. It is unique within the merchant (409 SOURCE_CODE_TAKEN). pos, grabfood, gofood, qr and open-api are reserved. It cannot change after the source has an order (409 SOURCE_CODE_LOCKED), because the merchant's reports and your webhooks use it.
  • display_name has 1 to 40 characters. You can change it at any time.
  • The call answers 201 the first time and 200 after that. It needs no X-Outlet-ID, no X-User-ID and no Idempotency-Key: sending it again changes nothing.
  • The first call also creates a payment method named Paid on <display_name>. The merchant does not see it in the payment method list.
  • Read your source with GET /order-source. It answers 404 NOT_FOUND until you register.

A POST /orders before you register answers 409 ORDER_SOURCE_NOT_REGISTERED.

Every order you create shows your source_code in its source field, and in the source of its webhooks.

Send an Order​

POST /orders needs X-Outlet-ID and Idempotency-Key. X-User-ID is optional.

{
"external_order_id": "HV-10021",
"external_ordered_at": "2026-10-15T12:30:00Z",
"sales_type": "delivery",
"accepted_type": "manual",
"member_id": "6f1c2a9e-0000-4000-8000-0000000000c1",
"voucher_code": "WELCOME10",
"lines": [
{
"variant_id": "6f1c2a9e-0000-4000-8000-0000000000d1",
"quantity": 2,
"modifiers": [
{
"group_id": "6f1c2a9e-0000-4000-8000-0000000000e1",
"option_id": "6f1c2a9e-0000-4000-8000-0000000000e2"
}
],
"note": "No ice"
},
{ "variant_id": "<variant id>", "quantity": 1 }
],
"tenders": [
{ "payment_method_id": "<PAYMENT_METHOD_ID>", "amount": 98000, "reference": "HV-10021-PAY" }
],
"expected_total": 100000
}
FieldMeaning
external_order_idYour id of the order. 1 to 64 characters, without spaces at the start or the end
external_ordered_atWhen your customer placed the order (optional)
sales_typedelivery, takeaway or pickup. When the outlet does not sell this type the call answers 409 SALES_TYPE_DISABLED
accepted_typeauto or manual (default). See Accept and Reject
member_idA customer from GET /customers. The customer earns loyalty points when the ticket is accepted
voucher_codeA voucher of the merchant. An unknown code answers 400 VALIDATION_FAILED on voucher_code
lines1 to 100 lines. A catalog line names variant_id. A manual line names a manual_* sku with product_name and manual_price (needs X-User-ID). Options go in modifiers and preferences
order_discountA discount on the whole order. Needs X-User-ID
tendersHow the customer paid. See Payments
expected_totalThe total you expect. See Prices and expected_total

A line can also carry a note for the kitchen (up to 200 characters). manual_price and a line discount need X-User-ID. A line with a manual_price cannot carry modifiers or preferences.

Manual SKU Lines​

A line whose sku starts with manual_ has no catalog item behind it. It needs three things: X-User-ID, manual_price and product_name. Without one of them the call answers 400 VALIDATION_FAILED. A line names variant_id or a manual_* sku, never both. Any other sku answers 400 VALIDATION_FAILED on lines[i].sku.

Prices and expected_total​

You never send a price. Mekari POS prices every line with the merchant's own prices, promotions, tax and service fees, then compares the result with your expected_total.

  • When they are the same, the order is recorded.
  • When they are not, nothing is recorded and the answer is 409 CART_REPRICED. Its data holds the POS amounts: total, subtotal, line_discount, tax, service_fee, payment_fee and rounding. Show them to your customer, or send the order again with the POS total as expected_total when your customer agrees.

expected_total is the POS total including any payment fee that POS adds for a custom payment method with a fee. The tender amount plus that fee must add up to the total exactly. All amounts are in rupiah, as everywhere in this API.

Example: the lines, tax and service fees come to Rp98.000, and the payment method has a fee of Rp2.000. The total is Rp100.000.

{
"tenders": [{ "payment_method_id": "<PAYMENT_METHOD_ID>", "amount": 98000 }],
"expected_total": 100000
}

Tenders that do not add up to the total answer 400 VALIDATION_FAILED on the field tenders (code AMOUNT_MISMATCH), and nothing is recorded.

Payments​

tenders says how the customer paid. Each tender has payment_method_id (from GET /payment-methods), amount and an optional reference. The cash method needs X-User-ID with an open shift.

GET /payment-methods lists the methods a tender can name at the outlet in X-Outlet-ID: the cash method and the active custom methods of the merchant. A method of another type, such as QRIS or points, is not listed and cannot pay an order.

A problem with a tender answers 400 VALIDATION_FAILED, with the field in data.fields: tenders[i].payment_method_id for an unknown method, a method that GET /payment-methods does not list at the outlet (code NOT_ALLOWED: inactive, hidden, not offered at this outlet, or another type), or a cash tender without X-User-ID; tenders when the amounts do not add up.

  • No tenders. Leave tenders out, or send it empty, and Mekari POS records the whole total on the Paid on <display_name> method of your source. This is the usual case when you collected the money yourself.
  • A total of 0. An order can total Rp0, for example with a 100% voucher. An empty tenders list is refused for it. Send one explicit tender with amount 0 on a custom payment method.

The Acting User​

X-User-ID is optional on POST /orders.

  • Without it, the order runs as your source. No shift is needed.
  • With it, the user needs the permission to create sales, and an open shift at the outlet (409 SHIFT_NOT_OPEN). See Outlets and Acting User.

Send X-User-ID when the order uses manual_price, a line discount, order_discount, a manual SKU line or a cash tender. Without it these answer 400 VALIDATION_FAILED.

Accept and Reject​

Every order puts a ticket on the outlet's kitchen board.

accepted_typeWhat happens
manualThe ticket waits on the kitchen board. The cashier accepts or rejects it
autoThe ticket is accepted at once. When stock is short the call answers 422 KITCHEN_ACCEPT_STOCK_INSUFFICIENT and nothing is recorded

At accept (at once with auto, or when the cashier accepts), Mekari POS deducts stock, awards loyalty points to member_id, and redeems the voucher. If the voucher was used up in the meantime, the order is still accepted: the discount stays on the order, and no voucher use is recorded.

At reject, Mekari POS records a refund to the original payment. Refunding your customer is up to you: Mekari POS does not move money in your application. Use the order.rejected webhook (see Webhooks) to start your refund. A cashier cannot refund a partner order in cash when closing its ticket on the kitchen board.

Repeating an Order​

An order is identified by your source and its external_order_id. An external_order_id this order source already sent answers 409 DUPLICATE_EXTERNAL_ORDER_ID with data.order_id, the id of the first order. Read it with GET /orders/{id}. The answer is the same:

  • Whatever the Idempotency-Key is.
  • Whatever the body is. The new body is ignored, so a changed line is not applied.
  • Even when you sent the first order to another outlet of the same merchant.

A new order answers 201. Retry a request with the same Idempotency-Key and the same body: after a timeout or a network error you get the stored answer, and never a second order.

An error answer is stored under its Idempotency-Key, as in Idempotency. After you fix the cause of a 4xx, send the order again with a new Idempotency-Key.

Read the Order​

The answer of POST /orders has the same shape as GET /orders/{id}. Besides the usual fields it has:

FieldMeaning
sourceYour source_code. Orders made in Mekari POS have pos; sales channels have grabfood or gofood
external_order_idYour id of the order, or null when the order has none
kitchen_order_idThe ticket on the outlet's kitchen board, or null
kitchen_statusnew, accepted, preparing, ready, served, completed, rejected or cancelled, or null

GET /orders has the same four fields. Read kitchen_status to show your customer how the kitchen is doing.

Webhooks​

Set up an orders webhook endpoint (see Webhooks) to follow your orders without polling.

  • The kitchen events (order.accepted, order.rejected, order.preparing, order.ready, order.served, order.completed, order.cancelled) follow the ticket. Their kitchen_status says where it is.
  • Use order.rejected to refund your customer.
  • You do not receive the events your own call causes. The answer of POST /orders is your confirmation, and the order.paid of that order is not sent to your application.
  • After an event, read the order with GET /orders/{id}, or use a full payload.

Errors​

CodeWhen
400 VALIDATION_FAILEDA field is wrong. data.fields names it. A payment problem names tenders; an unknown voucher names voucher_code
409 DUPLICATE_EXTERNAL_ORDER_IDYour source already sent this external_order_id. data.order_id is the first order
409 ORDER_SOURCE_NOT_REGISTEREDRegister with PUT /order-source first
409 SOURCE_CODE_TAKENAnother application of the merchant uses this source_code
409 SOURCE_CODE_LOCKEDThe source has an order, so its source_code cannot change
409 CART_REPRICEDexpected_total is not the POS total. data has the POS amounts
409 SHIFT_NOT_OPENThe acting user has no open shift at the outlet
409 SALES_TYPE_DISABLEDThe outlet does not sell this sales_type
422 KITCHEN_ACCEPT_STOCK_INSUFFICIENTaccepted_type is auto and stock is short. data.shortages names the items (kind, name, required, on_hand, and used_by with product_name and quantity)
403 PLAN_FEATURE_REQUIREDThe merchant's plan has no kitchen order management

All error codes are in Errors.