Orders
Polling for desktop POS
A POS on a Windows machine with no public address cannot take webhooks. It can ask for new events instead, and we hold the call open until something happens.
#When to poll
Poll when your POS runs on the restaurant's own computer. Send all your other calls from the same machine, from any internet connection. If you have a server with a public address, webhooks are simpler.
#The call
GET /v1/events returns the same events the webhooks carry, after your cursor. When there are none, we wait up to wait seconds and answer the moment one arrives, so a new order reaches the kitchen without delay.
| Query | Meaning |
|---|---|
after | next_cursor from your last call. Without it you get the events of the last 24 hours. |
wait | Seconds to hold the call open, 0 to 25. Default 25. |
limit | 1 to 500. Default 100. |
GET /v1/events?after=5520&wait=25
200
{
"data": {
"events": [
{ "event": "order.created",
"event_id": "5521",
"idempotency_key": "order.created:104812",
"occurred_at": "2026-11-01T12:41:11+05:45",
"data": { "laayo_order_id": "104812", "status": "pending", "...": "..." } }
],
"next_cursor": "5521"
}
}#A full loop
// Runs on one background thread. Never start a second poll while one is open. string cursor = Settings.Load("laayo_cursor"); // null on the very first run var backoff = TimeSpan.FromSeconds(1); while (!stop.IsCancellationRequested) { try { // the signed path includes this query exactly as sent var path = "/v1/events?wait=25" + (cursor != null ? "&after=" + Uri.EscapeDataString(cursor) : ""); var res = await Laayo.SignedGet(path, timeout: TimeSpan.FromSeconds(35)); if ((int)res.StatusCode == 429) { await Task.Delay(TimeSpan.FromMinutes(1)); continue; } res.EnsureSuccessStatusCode(); var page = res.Json<EventsPage>().Data; foreach (var ev in page.Events) { if (!Seen.Contains(ev.IdempotencyKey)) { Kot.Handle(ev); Seen.Add(ev.IdempotencyKey); } } cursor = page.NextCursor; Settings.Save("laayo_cursor", cursor); // only after the events are handled backoff = TimeSpan.FromSeconds(1); } catch (Exception) { await Task.Delay(backoff); // network down, our 5xx: wait and try again backoff = TimeSpan.FromSeconds(Math.Min(backoff.TotalSeconds * 2, 60)); } }
#Rules that keep it right
- Save
next_cursoronly after you have handled the events. If the POS crashes in between, you get the same events again instead of losing them. - Keep one poll open at a time. Start the next call when the last one answers.
- Sign the path with its query exactly as you send it, for example
/v1/events?after=5521&wait=25. Two identical calls in the same second are refused as 401 replayed; a loop that never overlaps never hits this. - Set your HTTP timeout above 25 seconds, about 35, so the held call is not cut short.
- On a network error or a 5xx, wait and try again with backoff up to a minute. On 429 rate_limited or 503 api_paused, wait the
Retry-Afterseconds. - Two quick changes to an order can arrive as one event with the latest status. Read
data.status; do not count events.
Polling does not replace the heartbeat. Send POST /v1/heartbeat every 60 seconds as well, see When the POS is offline.
Was this page clear?Updated 1 Nov 2026