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.
| Step | Call |
|---|---|
| 1 | PUT /order-source, once per merchant |
| 2 | POST /orders, for each order |
| 3 | GET /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_codehas 2 to 32 characters: lowercase letters, digits and hyphens. It is unique within the merchant (409 SOURCE_CODE_TAKEN).pos,grabfood,gofood,qrandopen-apiare 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_namehas 1 to 40 characters. You can change it at any time.- The call answers
201the first time and200after that. It needs noX-Outlet-ID, noX-User-IDand noIdempotency-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 answers404 NOT_FOUNDuntil 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
}
| Field | Meaning |
|---|---|
external_order_id | Your id of the order. 1 to 64 characters, without spaces at the start or the end |
external_ordered_at | When your customer placed the order (optional) |
sales_type | delivery, takeaway or pickup. When the outlet does not sell this type the call answers 409 SALES_TYPE_DISABLED |
accepted_type | auto or manual (default). See Accept and Reject |
member_id | A customer from GET /customers. The customer earns loyalty points when the ticket is accepted |
voucher_code | A voucher of the merchant. An unknown code answers 400 VALIDATION_FAILED on voucher_code |
lines | 1 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_discount | A discount on the whole order. Needs X-User-ID |
tenders | How the customer paid. See Payments |
expected_total | The 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. Itsdataholds the POS amounts:total,subtotal,line_discount,tax,service_fee,payment_feeandrounding. Show them to your customer, or send the order again with the POStotalasexpected_totalwhen 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
tendersout, or send it empty, and Mekari POS records the whole total on thePaid 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
tenderslist is refused for it. Send one explicit tender withamount0on 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_type | What happens |
|---|---|
manual | The ticket waits on the kitchen board. The cashier accepts or rejects it |
auto | The 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-Keyis. - 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:
| Field | Meaning |
|---|---|
source | Your source_code. Orders made in Mekari POS have pos; sales channels have grabfood or gofood |
external_order_id | Your id of the order, or null when the order has none |
kitchen_order_id | The ticket on the outlet's kitchen board, or null |
kitchen_status | new, 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. Theirkitchen_statussays where it is. - Use
order.rejectedto refund your customer. - You do not receive the events your own call causes. The answer of
POST /ordersis your confirmation, and theorder.paidof that order is not sent to your application. - After an event, read the order with
GET /orders/{id}, or use afullpayload.
Errors
| Code | When |
|---|---|
400 VALIDATION_FAILED | A field is wrong. data.fields names it. A payment problem names tenders; an unknown voucher names voucher_code |
409 DUPLICATE_EXTERNAL_ORDER_ID | Your source already sent this external_order_id. data.order_id is the first order |
409 ORDER_SOURCE_NOT_REGISTERED | Register with PUT /order-source first |
409 SOURCE_CODE_TAKEN | Another application of the merchant uses this source_code |
409 SOURCE_CODE_LOCKED | The source has an order, so its source_code cannot change |
409 CART_REPRICED | expected_total is not the POS total. data has the POS amounts |
409 SHIFT_NOT_OPEN | The acting user has no open shift at the outlet |
409 SALES_TYPE_DISABLED | The outlet does not sell this sales_type |
422 KITCHEN_ACCEPT_STOCK_INSUFFICIENT | accepted_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_REQUIRED | The merchant's plan has no kitchen order management |
All error codes are in Errors.