Skip to content

Integration

Placing orders

Pricing a basket, sending the order to the kitchen, retrying safely and following the order.

The order calls price a basket, send an order straight to the venue's kitchen and let you follow it until the venue closes the bill. Every call in this guide needs a key with the Can send orders permission.

Warning: There is no separate test environment. Every order placed with a real key is a real order: its ticket prints in the kitchen and the venue's staff are told. The API cannot cancel an order; a mistaken one is cancelled at the venue's till. While you develop, use a venue you run yourself, tell your staff, and try everything that need not be an order by pricing the basket.

The flow#

  1. Your site shows the menu and the guest builds a basket.
  2. Your server prices the basket with POST /venues/{venueId}/orders/preview and shows the guest the total.
  3. When the guest confirms, your server sends the same body to POST /venues/{venueId}/orders.
  4. The order reaches the kitchen without waiting for approval. Your site follows it with webhooks, or with GET /orders/{orderId}.
  5. The guest pays at the venue.

Order types#

typeWhat it needsWhat happens
DineIntableId, one of the venue's tables. No customer, no paymentMethod.The round is added to the table's open bill, or opens one.
Takeawaycustomer.name and customer.phone. No tableId.The customer collects it at the venue.
Deliverycustomer.name, customer.phone and customer.address. No tableId.The venue brings it to the customer.

The basket#

An order has 1 to 30 lines. Each line names a product from the menu in one of its portions, and can carry the chosen extras and the ingredients to leave out:

JSON
{
  "productId": "5d2c8a41-7b0e-4c36-8f1d-9e4a6b3c2d10",
  "portionId": "a8e1f6c3-2d94-4b70-b5a6-1c7d9e0f3a24",
  "quantity": 2,
  "note": "Köfteler ayrı paketlensin.",
  "extraIds": ["f1a2b3c4-d5e6-4f70-8192-a3b4c5d6e7f8", "b4c5d6e7-f8a9-4b1a-8c2d-3e4f5a6b7c8d"],
  "removedIngredientIds": ["b7c8d9e0-f1a2-4b3c-8d4e-5f6a7b8c9d0e"]
}
LimitValue
Lines1 to 30
Quantity of a line (quantity)1 to 20
Extras on a lineAt most 20
Ingredients left out of a lineAt most 20
A line's noteAt most 200 characters
The order's noteAt most 300 characters
The customer's name, phone, addressAt most 100, 30 and 300 characters

Extras and left-out ingredients apply to each unit of the line. When units of the same product are wanted with different choices, send each as a line of its own.

A request that breaks these rules is answered 400 PUBLIC_API_ORDER_INVALID. validationErrors lists every problem found by its path in the request: lines[1].extraIds, say.

Price it first#

A request carries no prices. Cibusy prices each line at the menu price with its extras, applies the venue's automatic campaigns and its service fee, and says what it came to.

POST /venues/{venueId}/orders/preview takes the same body as an order and prices the basket without ordering it:

curl -X POST "https://api.cibusy.com/public/v1/venues/3fa85f64-5717-4562-b3fc-2c963f66afa6/orders/preview" \
  -H "X-Api-Key: $CIBUSY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "DineIn",
    "tableId": "2f4e6a8c-0b1d-4c3e-9f5a-7b9d1e3c5a70",
    "note": "Bir misafirin fıstık alerjisi var.",
    "lines": [
      {
        "productId": "5d2c8a41-7b0e-4c36-8f1d-9e4a6b3c2d10",
        "portionId": "a8e1f6c3-2d94-4b70-b5a6-1c7d9e0f3a24",
        "quantity": 2,
        "note": "Köfteler ayrı paketlensin.",
        "extraIds": [
          "f1a2b3c4-d5e6-4f70-8192-a3b4c5d6e7f8",
          "b4c5d6e7-f8a9-4b1a-8c2d-3e4f5a6b7c8d"
        ],
        "removedIngredientIds": [
          "b7c8d9e0-f1a2-4b3c-8d4e-5f6a7b8c9d0e"
        ]
      }
    ]
  }'

The answer's data:

JSON
{
  "lines": [
    {
      "productId": "5d2c8a41-7b0e-4c36-8f1d-9e4a6b3c2d10",
      "portionId": "a8e1f6c3-2d94-4b70-b5a6-1c7d9e0f3a24",
      "quantity": 2,
      "unitPrice": 360,
      "lineTotal": 720
    }
  ],
  "subtotal": 720,
  "discount": 72,
  "serviceFee": 32.4,
  "total": 680.4,
  "currency": "TRY"
}
  • subtotal is the menu price of the lines with their extras. discount is what the campaigns take off, and serviceFee the venue's service fee. total = subtotal - discount + serviceFee.
  • Pricing checks the basket the way an order does: products, portions, choices and stock. It does not check whether the venue is open.
  • When a dine-in round joins a bill that is already open, a campaign that looks at the whole bill can price the round differently once it is on the bill. The totals in the answer to placing the order are the figures that count.

Do not work out prices on your site; always show the guest the amount the server gave.

Sending the order#

An order is placed with POST /venues/{venueId}/orders, and every request needs an Idempotency-Key header:

curl -X POST "https://api.cibusy.com/public/v1/venues/3fa85f64-5717-4562-b3fc-2c963f66afa6/orders" \
  -H "X-Api-Key: $CIBUSY_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "Takeaway",
    "customer": {
      "name": "Ayşe Yılmaz",
      "phone": "+905321112233"
    },
    "paymentMethod": "Card",
    "lines": [
      {
        "productId": "5d2c8a41-7b0e-4c36-8f1d-9e4a6b3c2d10",
        "portionId": "a8e1f6c3-2d94-4b70-b5a6-1c7d9e0f3a24",
        "quantity": 2,
        "note": "Köfteler ayrı paketlensin.",
        "extraIds": [
          "f1a2b3c4-d5e6-4f70-8192-a3b4c5d6e7f8",
          "b4c5d6e7-f8a9-4b1a-8c2d-3e4f5a6b7c8d"
        ],
        "removedIngredientIds": [
          "b7c8d9e0-f1a2-4b3c-8d4e-5f6a7b8c9d0e"
        ]
      }
    ]
  }'

The first successful answer is 201 Created. Its data holds the order (order) and the ids of the lines this call wrote (addedLineIds).

  • It goes straight to the kitchen. There is no approval step. The kitchen tickets print as the order is placed, and the venue's staff are told as they are for an order from the QR menu. The key's name appears on the order.
  • It is paid at the venue. Nothing is charged through the API. paymentMethod (Cash, Card or MealCard) is a hint, on a takeaway or a delivery, about how the customer means to pay, for whoever hands the order over. paymentStatus follows what the venue records at the till.
  • The venue must be taking orders. It needs an active subscription, no rush declared and to be inside its opening hours. Otherwise the answer is a 409: PUBLIC_API_ORDERING_UNAVAILABLE, PLACE_IS_BUSY or OUTSIDE_WORKING_HOURS. You can check the venue's ordering state before you show an order button.

Retrying safely#

Send an Idempotency-Key with every order: a value you make up once per order, 8 to 64 characters of letters, digits, - and _ only. A UUID works.

If a request times out or the connection drops, send the same request again with the same key. You get the order the first call placed, as the venue has it now, with status 200 where the first answer was 201. Nothing is ordered twice. Make a new key for a new order.

JavaScript
import { randomUUID } from 'node:crypto';

const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

async function placeOrder(venueId, basket) {
  // Made once per order, and the same on every attempt.
  const idempotencyKey = randomUUID();

  for (let attempt = 1; ; attempt++) {
    let response;
    try {
      response = await fetch(`https://api.cibusy.com/public/v1/venues/${venueId}/orders`, {
        method: 'POST',
        headers: {
          'X-Api-Key': process.env.CIBUSY_API_KEY,
          'Content-Type': 'application/json',
          'Idempotency-Key': idempotencyKey,
        },
        body: JSON.stringify(basket),
        signal: AbortSignal.timeout(15_000),
      });
    } catch (error) {
      // A timeout or a dropped connection: the order may exist, so ask again with the same key.
      if (attempt === 3) throw error;
      await wait(attempt * 2_000);
      continue;
    }

    const body = await response.json();
    const stillWorking = body.errorCode === 'ORDER_CREATION_IN_PROGRESS';
    if ((response.status >= 500 || stillWorking) && attempt < 3) {
      await wait(attempt * 2_000);
      continue;
    }

    // 201: a new order. 200: the order this key placed before.
    return body;
  }
}
  • A retry is answered before the venue's hours and subscription are looked at again, since the order already exists.
  • An Idempotency-Key is remembered for the API key that sent it and the venue it was sent to. Sent again with a different basket, it places nothing new: the original order comes back.
  • 409 ORDER_CREATION_IN_PROGRESS says a request with the same value is still being processed. Send yours again with the same value and you receive the order.
  • Keep the addedLineIds of the first (201) answer. In that answer they are exactly the lines the call wrote. In a retry's answer (200) they are a best-effort reconstruction: the lines put on the order within 15 seconds before the first call was recorded. On a dine-in order that joined a table's bill, they can include a line somebody else added to the same bill in that window. If you never received the 201, treat a retry's ids as likely rather than certain.

Following an order#

GET /orders/{orderId} reads an order as the venue has it now. GET /orders lists the orders placed through the API, newest first, a page at a time; status chooses open (open, the default), closed (closed) or all (all) orders.

Each of a venue's keys can read the orders placed with the venue's other keys. Rather than asking about orders over and over, use a webhook: your server is told whenever an order changes.

Order status#

status is worked out from the order's lines and its bill; the first rule that fits applies:

statusWhen
CancelledThe order was voided, or every line on it was struck off.
CompletedThe venue closed the bill.
OnTheWayA delivery has left the venue.
ServedEvery line on it is served.
ReadyEvery line is ready or already served.
PreparingThe kitchen has started on at least one line.
ReceivedThe venue has the order and the kitchen has not started.

Each line has a status of its own too: Pending, Preparing, Ready, Served or Cancelled. The lines of a cancelled order read Cancelled as well.

Payment status#

paymentStatus is Unpaid when nothing has been paid at the venue, Paid when the bill is settled, and PartiallyPaid in between. totals.paid is what has been paid, and totals.remaining what the customer still owes.

Table bills#

A dine-in order that joined a bill that was already open comes back as the whole bill: the lines other people ordered at that table are in lines too. addedLineIds in the answer to placing it says which of them your call wrote.