The Bill&Go shop API.

Connect your own website, your accounting or a delivery partner to your shop. Read the menu, orders, sales and stock, send orders to the till, and mark orders paid that were paid somewhere else.

Your first request
curl https://api.billandgo.app/api/v1/stores \
  -H "Authorization: Bearer bg_live_q3Vf0yJr7mXo9w2Kd8sLhA1uZtBnEcGp"

Get started

  1. 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.
  2. 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.
  3. Send it with every requestIn the Authorization header, as below. Every address on this page starts with https://api.billandgo.app/api/v1.
Every request
Authorization: Bearer bg_live_q3Vf0yJr7mXo9w2Kd8sLhA1uZtBnEcGp

Keep 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 dashboardThe key may
ReadEvery GET on this page.
Read and place ordersAlso 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’s timezone; days are counted on that clock.
  • Money is a whole number in the smallest unit, beside a currency: 20900 with KHR is 20 900 riel, 450 with USD is $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.

Answer
{
  "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.

FieldWhat it is
storeIdOne store (one this key reaches).
statusplaced, confirmed, preparing, ready, out_for_delivery, delivered, completed, rejected or cancelled.
updatedSinceA UTC time: orders changed at or after it.
cursorThe nextCursor of the page before.
limit1 to 100. 50 when left out.
Answer
{
  "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.

Answer
{
  "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.

Answer
{
  "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. net is sales less refunded.
  • 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.

Answer
{
  "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).

Request
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"
  }'
FieldWhat it is
storeIdThe store to send it to.
orderTypetakeaway 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, notesOptional.

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.

Request
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" }'
FieldWhat it is
methodThe 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.
referenceOptional. Your own id for the payment, up to 100 characters, kept with it.
Idempotency-KeyOptional 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.

Answer (parts)
{
  "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 and Idempotent-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

Every error
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "This shop does not take that payment method",
    "details": { "method": "bitcoin" }
  }
}
StatuscodeWhen
400VALIDATION_ERRORA 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.
401UNAUTHORIZEDNo key, a wrong key, or one the owner revoked.
403FORBIDDENA read-only key sending an order or a payment, or a store this key is not allowed to use.
404NOT_FOUNDAnother business's store, order or product, or an order at a store this key cannot use.
409CONFLICTMarking an order paid that is already paid another way, refunded, cancelled or turned down.
422INVALID_MODIFIER_SELECTIONOptions that break a group’s rules: a required choice left out, or too many picked.
429TOO_MANY_REQUESTSToo 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 a Retry-After header 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.

Connecting something we have not thought of?

Tell us what you want to connect to your shop. We'll help you set up the key and the first requests.

Already with us? Sign in

Get started

Takes a minute. We reply within a working day.

How many shops?

We only use these to get back to you.