Get started
- Be on Starter or aboveThe shop API is a connection, like Telegram and BasilBook. If your shop moves to Free later, keys you already made keep working.
- Make a keyThe owner opens Settings, then API keys, in the dashboard. Give the key a name (“Website”), choose what it may do, and which stores. The whole key is shown once: copy it then.
- Send it with every requestIn the
Authorizationheader, as below. Every address on this page starts withhttps://api.billandgo.app/api/v1.
Authorization: Bearer bg_live_q3Vf0yJr7mXo9w2Kd8sLhA1uZtBnEcGpKeep the key on your server, never in a web page or a phone app: anyone who has it can read your shop. If it leaks, revoke it in the dashboard and make a new one.
Keys and access
| In the dashboard | The key may |
|---|---|
| Read | Every GET on this page. |
| Read and place orders | Also POST /orders and POST /orders/:id/payment. |
- Stores. A key with no stores picked reaches every store, including ones you open later. Pick stores and the key sees, and orders at, only those.
- Revoke. Revoking stops the key at its next request. A revoked key never works again.
- Up to 10 working keys per business. Make one per system, so you can revoke one without stopping the others.
- Only for this API. A key works only on addresses under
/api/v1, and a dashboard sign-in does not work there.
How answers look
- JSON in and out. A field the API does not know is refused, so a typo never goes unnoticed.
- Times are in UTC, like
2026-10-23T03:04:05.123Z. Answers with times also carry the shop’stimezone; days are counted on that clock. - Money is a whole number in the smallest unit, beside a
currency:20900withKHRis 20 900 riel,450withUSDis $4.50. - Lists come as
{ "data": [ … ] }. - Errors come as
{ "error": { "code", "message", "details" } }. See Errors.
Endpoints
GET/stores
Any key.
The stores this key reaches.
{
"data": [
{
"id": "0b6c…", "name": "Riverside", "address": "St. 108, Phnom Penh",
"lat": 11.57, "lng": 104.92, "phone": "+855 12 345 678",
"openingHours": { "mon": [{ "open": "07:00", "close": "21:00" }] },
"active": true, "taxRate": 10, "deliveryFee": 2000, "deliveryRadiusM": 5000,
"currency": "KHR", "timezone": "Asia/Phnom_Penh"
}
],
"timezone": "Asia/Phnom_Penh"
}GET/orders
Any key.
Orders, the one changed longest ago first, a page at a time: use it to keep another system up to date.
| Field | What it is |
|---|---|
storeId | One store (one this key reaches). |
status | placed, confirmed, preparing, ready, out_for_delivery, delivered, completed, rejected or cancelled. |
updatedSince | A UTC time: orders changed at or after it. |
cursor | The nextCursor of the page before. |
limit | 1 to 100. 50 when left out. |
{
"data": [ { "id": "9a1e…", "…": "an order, as below" } ],
"nextCursor": "WyIyMDI2LTEwLTIzVDAzOjA0OjA1LjEyMzQ1NloiLCI3ZjFjLi4uIl0",
"hasMore": false,
"timezone": "Asia/Phnom_Penh"
}Staying up to date. Follow nextCursor while hasMore is true. Keep the last nextCursor even when hasMore is false, and ask with it again later: an order that changed since comes back, once for each change. Your own clock never comes into it.
GET /orders/:id
One order. Lines and prices are what it was sold at, not what the menu says today. source is app, api, counter or phone.
{
"id": "9a1e…", "storeId": "0b6c…", "reference": "20261023-0042", "callNumber": 42,
"source": "api", "orderType": "takeaway", "status": "placed",
"paymentStatus": "unpaid", "paymentMethod": null,
"items": [
{
"productId": "p1…", "name": "Iced Latte", "nameKm": "ឡាតេទឹកកក", "qty": 2, "pack": null,
"options": [{ "group": "Sugar", "name": "Extra sweet", "nameKm": null, "priceDelta": 500 }],
"basePrice": 9000, "unitPrice": 9500, "lineTotal": 19000, "note": null
}
],
"subtotal": 19000, "discount": 0, "discounts": [], "deliveryFee": 0, "tax": 1900, "total": 20900,
"currency": "KHR",
"customerName": "Dara", "customerPhone": null, "deliveryAddress": null,
"deliveryLat": null, "deliveryLng": null, "notes": null, "cancelReason": null,
"placedAt": "2026-10-23T03:04:05.123Z", "paidAt": null, "cancelledAt": null, "refundedAt": null,
"createdAt": "2026-10-23T03:04:05.123Z", "updatedAt": "2026-10-23T03:04:05.123Z",
"timezone": "Asia/Phnom_Penh"
}GET/sales/daily?from=YYYY-MM-DD&to=YYYY-MM-DD
Any key.
Takings by shop day, per store and currency, for at most 92 days. Add storeId for one store.
{
"from": "2026-10-23", "to": "2026-10-23", "timezone": "Asia/Phnom_Penh",
"data": [
{
"day": "2026-10-23", "storeId": "0b6c…", "currency": "KHR",
"orders": 2, "sales": 15000, "discount": 0, "tax": 909, "deliveryFees": 0,
"refunds": 1, "refunded": 5000, "net": 10000
}
]
}orders,sales,discount,tax,deliveryFees: orders paid that day, by when the money came in, including any refunded later.refunds,refunded: money given back that day, whenever it was paid.netissaleslessrefunded.- Days with nothing are left out. Never add amounts in different currencies together.
GET/stock?storeId=…
Any key.
What is on the shelf, in single units, for every product the shop counts. Every store the key reaches when storeId is left out. It can be below zero: a sale is never refused for stock.
{
"data": [
{
"storeId": "0b6c…", "productId": "p9…", "name": "Coca-Cola", "nameKm": null, "unitName": "Can",
"onHand": 77, "lowStockAt": 24, "packs": [{ "id": "u1…", "name": "Box", "contains": 24 }]
}
],
"timezone": "Asia/Phnom_Penh"
}POST/orders
Needs a key that can place orders.
Send an order to the till. The Idempotency-Key header is required (see Sending again safely).
curl -X POST https://api.billandgo.app/api/v1/orders \
-H "Authorization: Bearer bg_live_…" \
-H "Idempotency-Key: web-order-58213" \
-H "Content-Type: application/json" \
-d '{
"storeId": "0b6c…",
"orderType": "takeaway",
"items": [
{ "productId": "p1…", "qty": 2, "modifierIds": ["o2…"], "note": "less ice" },
{ "productId": "p9…", "qty": 1, "unitId": "u1…" }
],
"customerName": "Dara",
"customerPhone": "+855 12 345 678",
"notes": "Pick up at 7"
}'| Field | What it is |
|---|---|
storeId | The store to send it to. |
orderType | takeaway or delivery. A delivery needs customerPhone and deliveryAddress, and may carry a pin (deliveryLat and deliveryLng), refused past the store’s delivery distance. |
items[] | productId, qty (1 to 99), modifierIds (option ids from the menu, all groups together), unitId (a pack), note (for the kitchen). |
customerName, customerPhone, notes | Optional. |
There is no price to send: Bill&Go prices every order the way the till does, with the option rules, the store’s menu, its running deals, tax and the delivery fee. The answer is 201 with the order.
It arrives as an online order: status placed, unpaid. Staff are told, and the counter accepts it or turns it down. Its kitchen ticket prints when it is accepted, or at once if the shop’s printing rules say so. Follow it with GET /orders.
POST/orders/:id/payment
Needs a key that can place orders.
Mark an order paid that was paid somewhere else: a delivery partner or your website already charged the customer. The till then hands the order over without asking for money.
curl -X POST https://api.billandgo.app/api/v1/orders/9a1e…/payment \
-H "Authorization: Bearer bg_live_…" \
-H "Idempotency-Key: grab-GF-58213" \
-H "Content-Type: application/json" \
-d '{ "method": "grab", "reference": "GF-58213" }'| Field | What it is |
|---|---|
method | The code of one of the shop’s payment methods, taken at this store (Settings, Payment methods; for example card, khqr, or one the shop added such as grab). Not a cash method: cash is counted in the drawer at the till. |
reference | Optional. Your own id for the payment, up to 100 characters, kept with it. |
Idempotency-Key | Optional header. The same key again answers as a repeat. |
The answer is 200 with the order, now "paymentStatus": "paid". Its status does not change: an order still waiting is still waiting for the counter to accept it. The receipt prints at the shop.
{
"id": "9a1e…", "status": "placed",
"paymentStatus": "paid", "paymentMethod": "grab",
"total": 20900, "currency": "KHR",
"paidAt": "2026-10-23T03:09:41.002Z",
"…": "the rest of the order"
}- Sent again with the same
Idempotency-Key, or with the same method and reference: 200 with the order andIdempotent-Replayed: true. Nothing is paid twice. - Already paid another way (at the till, or with another reference), refunded, cancelled or turned down: 409
CONFLICT. - An order at a store this key cannot use: 404.
Sending again safely
Networks drop. To send a request again without making a second order or a second payment, give it an Idempotency-Key header: any text you make up, 1 to 255 printable characters with no spaces, one for each order or payment you mean, and the same one every time you retry it.
- The same key again answers 200 with what the first request made, and
Idempotent-Replayed: true, even when the two arrive at once. - A key belongs to your API key: two systems using the same text never meet.
- A repeat is answered from the first request, whatever it carries: never use one key for two different orders.
- A request that failed (400, 422…) did nothing, so sending it again really tries again.
Errors
{
"error": {
"code": "VALIDATION_ERROR",
"message": "This shop does not take that payment method",
"details": { "method": "bitcoin" }
}
}| Status | code | When |
|---|---|---|
| 400 | VALIDATION_ERROR | A field or query that does not fit, a product this store does not sell, a payment method the shop does not take, or cash sent as a payment. details.issues names the fields. |
| 401 | UNAUTHORIZED | No key, a wrong key, or one the owner revoked. |
| 403 | FORBIDDEN | A read-only key sending an order or a payment, or a store this key is not allowed to use. |
| 404 | NOT_FOUND | Another business's store, order or product, or an order at a store this key cannot use. |
| 409 | CONFLICT | Marking an order paid that is already paid another way, refunded, cancelled or turned down. |
| 422 | INVALID_MODIFIER_SELECTION | Options that break a group’s rules: a required choice left out, or too many picked. |
| 429 | TOO_MANY_REQUESTS | Too many requests in a minute. Wait the seconds in Retry-After. |
Limits
- 60 requests a minute for each key. One more and the answer is 429
TOO_MANY_REQUESTS, with aRetry-Afterheader saying how many seconds to wait. - 20 failed tries a minute from one address (no key, a wrong one or a revoked one), then 429 too.
- The minute starts with the first request and is counted across all of Bill&Go’s servers, so it holds while we update.
Syncing orders? Ask GET /orders with your last cursor every 30 seconds or so; that stays well inside the limit.