Linkit

Order changes (polling)

Follow every order change from every channel with a cursor — once a second, or long-polled, without missing one

Order changes

GET /api/v1/orders/changes is how an integration keeps up with orders. It returns every change to every order of your organization, from every source — delivery apps (HungerStation, Jahez, Keeta…), marketplaces (Trendyol, Amazon…), web stores (Salla, Zid, Shopify, WooCommerce…), external POS systems, Linkit's own till, payment orders and orders created through the API — in the order they were committed, each with a cursor you pass back to get the next ones.

It is built to be polled once a second: when nothing has changed, the answer is a 304 Not Modified served from memory, without a database read. If you would rather be told, use webhooks — both are fed by the same change log.

Every change is recorded by the database itself, so no source can bypass the feed — including connectors added after you integrate.

How to poll

  1. Start: call without a cursor. You get an empty page and the current cursor.
  2. Loop: call with cursor=<the last cursor> and send the same value as If-None-Match.
    • 304 — nothing changed. Wait poll_after_ms (1 s) and ask again.
    • 200 — process changes in order, keep the new cursor. If has_more is true, ask again immediately.
  3. Persist the cursor after you have processed a page. Restarting from a saved cursor resumes exactly where you stopped.
GET /api/v1/orders/changes?cursor=djEuODc2MDEuMg
If-None-Match: "djEuODc2MDEuMg"

Query parameters

ParameterTypeDefaultDescription
cursorstring—The cursor a previous response returned. Opaque: store it, do not parse it.
sinceRFC 3339—Start at the first change at or after this time instead of at a cursor (retention: 30 days). May repeat a few earlier changes; never skips a later one.
limitinteger1001–500 changes per page.
includestringorderorder: each change carries the order's current canonical state. raw: also the source's own payload. none: ids only.
waitinteger0Long-poll: hold the request up to this many seconds (0–25) until a change arrives.

Response

200 with headers ETag: "<cursor>" and X-Linkit-Cursor: <cursor>:

{
  "spec_version": "2026-09-29",
  "organization_id": "3xrde8yfgeqeaas",
  "cursor": "djEuODc2MDIuMw",
  "has_more": false,
  "poll_after_ms": 1000,
  "changes": [
    {
      "spec_version": "2026-09-29",
      "id": "evt_87602_3",
      "type": "order.status_changed",
      "occurred_at": "2026-09-29T05:12:44.120331Z",
      "organization_id": "3xrde8yfgeqeaas",
      "cursor": "djEuODc2MDIuMw",
      "order_id": "r1234567890abcd",
      "change": "updated",
      "changed_fields": ["order_status"],
      "status_from": "received",
      "status_to": "delivered",
      "payment_status_from": "paid",
      "payment_status_to": "paid",
      "order": { "id": "r1234567890abcd", "status": "delivered", "channel": { "family": "hungerstation", "kind": "delivery" }, "...": "see the canonical order" }
    }
  ]
}
StatusMeaning
200A page (possibly empty).
304Nothing after your cursor (If-None-Match matched). No body.
400Malformed cursor, since, limit, include or wait.
410Your cursor is older than retention (30 days). Resynchronise with GET /api/v1/orders and start again without a cursor.

Event types

One change, one type — the most specific that applies:

typeWhen
order.createdA new order arrived (any channel, or POST /api/v1/orders).
order.status_changedThe order status changed.
order.cancelledThe order became cancelled.
order.refundedThe payment became refunded.
order.updatedAnything else changed (changed_fields says what). A rewrite that changes nothing is not a change.
order.invoicedA platform order was invoiced at the till.
order.deletedThe order was deleted (order is null).

order is the order's state when the page is read, not at the moment of the change — two changes to one order on the same page carry the same current order. Use status_from / status_to for the transition.

Guarantees

  • Nothing is skipped. Changes are ordered by commit, and a change whose writer has not finished yet is held back until it has, so a later cursor can never pass an earlier change.
  • At-least-once. If you crash after processing a page and before saving the cursor, you will see those changes again. Deduplicate on id.
  • Tenant-scoped. A cursor only ever reads the calling organization's changes; a branch-restricted user sees only their branch's.

Examples

# Start
curl -s "https://linkit.works/api/v1/orders/changes" -H "Authorization: Bearer $LINKIT_TOKEN"
# Poll (304 when nothing changed)
curl -s -i "https://linkit.works/api/v1/orders/changes?cursor=$CURSOR" \
  -H "Authorization: Bearer $LINKIT_TOKEN" -H "If-None-Match: \"$CURSOR\""
# Long-poll up to 25 s
curl -s "https://linkit.works/api/v1/orders/changes?cursor=$CURSOR&wait=25" \
  -H "Authorization: Bearer $LINKIT_TOKEN"
let cursor = await loadCursor(); // from your storage; null the first time
for (;;) {
  const url = new URL('https://linkit.works/api/v1/orders/changes');
  if (cursor) url.searchParams.set('cursor', cursor);
  url.searchParams.set('wait', '25'); // long-poll: returns as soon as something changes
  const res = await fetch(url, {
    headers: { Authorization: `Bearer ${token}`, ...(cursor && { 'If-None-Match': `"${cursor}"` }) },
  });
  if (res.status === 304) continue;
  if (res.status === 410) { cursor = null; await resync(); continue; }
  const page = await res.json();
  for (const change of page.changes) await handle(change); // idempotent on change.id
  cursor = page.cursor;
  await saveCursor(cursor);
}
import requests, time

cursor = load_cursor()  # None the first time
while True:
    params = {"wait": 25}
    headers = {"Authorization": f"Bearer {token}"}
    if cursor:
        params["cursor"] = cursor
        headers["If-None-Match"] = f'"{cursor}"'
    r = requests.get("https://linkit.works/api/v1/orders/changes", params=params, headers=headers, timeout=35)
    if r.status_code == 304:
        continue
    if r.status_code == 410:
        cursor = None; resync(); continue
    page = r.json()
    for change in page["changes"]:
        handle(change)  # idempotent on change["id"]
    cursor = page["cursor"]
    save_cursor(cursor)
    if not page["has_more"] and not page["changes"]:
        time.sleep(page["poll_after_ms"] / 1000)

Every Linkit SDK wraps this loop; each sends the cursor as If-None-Match and treats a 304 as "nothing new", not as an error:

SDKOne pageFollow for ever
Goclient.Orders.Changes(ctx, opts)OrderChangesPoller
Rustclient.orders().changes()…send()client.orders().poller(cursor)
Pythonawait client.orders.changes(cursor)async for e in client.orders.iter_changes(cursor)
TypeScriptclient.orders.changesAsync({ cursor })for await (const e of client.orders.streamChanges({ cursor }))
.NETclient.Orders.GetChangesAsync(new() { Cursor = … })await foreach (var e in client.Orders.StreamChangesAsync(cursor))
Kotlinclient.orders.changes(cursor = …)client.orders.changesFlow(cursor)
Swifttry await client.orders.changes(cursor: …)for try await e in client.orders.changesStream(cursor: …)
Dartawait client.orders.changes(cursor: …)client.orders.changesStream(cursor: …)
C++client.orders().changes(request)linkit::OrderChangesPoller
Zigclient.orders().changes(.{ .cursor = … })linkit.OrderChangesPoller

The canonical order

Every order in the feed (and in webhooks, and in GET /api/v1/orders?format=canonical) has one shape whatever its source:

FieldDescription
channel{app_slug, app_name, family, kind, install_id, account, source} — kind is delivery, marketplace, ecommerce, pos, payments, api or unknown. A connector added later still resolves (kind: unknown, family from its slug).
external{order_id, reference, code, placed_at} — how the source knows the order.
status / status_rawCanonical received, ready_for_pickup, dispatched, delivered, cancelled, pending_payment, unknown — and the stored value verbatim.
payment{status, status_raw, method} — pending, paid, refunded, failed, cancelled, unknown.
fulfillment{method, method_raw, status, status_raw, branch_id, branch_reference, store_name, promised_for, is_preorder, receiver, shipping_address, dispatch_routing, shipping} — method delivery, pickup, shipping, dine_in, unknown.
customer{name, phone, email}
amounts{currency, subtotal, discounts, vat, delivery_fee, service_fee, total}
lines[{product_id, sku_id, name, quantity, unit_price, total}] — product_id is your product number as the source sent it.
rawThe source's own payload, with include=raw only.