Docs
Orders: Polling for desktop POS
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.

QueryMeaning
afternext_cursor from your last call. Without it you get the events of the last 24 hours.
waitSeconds to hold the call open, 0 to 25. Default 25.
limit1 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

Poll for events
// 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_cursor only 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-After seconds.
  • 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