Polling for orders
The Orders API is pull-based: DuckHub does not send webhooks. Your integration periodically asks for what changed — one cheap request per interval, no public endpoint on your side, and missed polls heal themselves on the next run.
The updatedSince cursor#
GET /v1/orders?updatedSince=... returns orders whose
updatedAt is at/after the given time, sorted by updatedAt
ascending. That makes updatedAt a resumable cursor:
- Keep a persisted cursor, starting from "now" (or how far back you want to backfill).
- Each cycle, request
updatedSince=<cursor>and page through the results (limitup to 100, thenpage=2, ... whilepage < totalPages). - Process each order: new ones (never seen
id) and status changes (comparestatuswith what you stored). - Advance the cursor to the largest
updatedAtyou received and persist it.
Because a status change bumps updatedAt, the same query surfaces both
brand-new orders and updates to ones you already imported — including
cancellations.
Cursor rules
Always send the cursor with an explicit timezone
(...Z) — naive timestamps are rejected with
400. The window is inclusive, so the order whose
updatedAt equals your cursor is returned again — dedupe by
id + updatedAt instead of bumping the cursor
by a second (an order updated within that same second would be lost).
Recommended cadence#
- Every 15–30 seconds gives kitchen-speed reactions and stays far below the rate limits (one venue polling at 20s ≈ 0.05 req/s).
- On
429or5xx, back off exponentially and keep the cursor unchanged — you'll catch up on the next successful poll. - Confirm accepted orders promptly via
PATCH /v1/orders/:id/statusso staff see progress in the dashboard.
Example polling loop#
const BASE = 'https://api.duck-hub.com/v1'
const HEADERS = { Authorization: 'Bearer dk_live_your_api_key' }
let cursor = await loadCursor() // e.g. from your DB; start at new Date().toISOString()
async function pollOnce() {
let page = 1
let totalPages = 1
let maxUpdatedAt = cursor
while (page <= totalPages) {
const params = new URLSearchParams({
updatedSince: cursor,
limit: '100',
page: String(page),
})
const response = await fetch(`${BASE}/orders?${params}`, {
headers: HEADERS,
})
if (!response.ok) throw new Error(`Poll failed: ${response.status}`)
const { items, totalPages: tp } = await response.json()
totalPages = tp
for (const order of items) {
await upsertOrderLocally(order) // dedupe by order.id + updatedAt
if (order.updatedAt > maxUpdatedAt) maxUpdatedAt = order.updatedAt
}
page += 1
}
cursor = maxUpdatedAt
await saveCursor(cursor)
}
setInterval(() => pollOnce().catch(console.error), 20_000)# One polling cycle: fetch everything updated since the stored cursor
CURSOR="2026-07-05T12:00:00.000Z"
curl "https://api.duck-hub.com/v1/orders?updatedSince=${CURSOR}&limit=100" \
-H "Authorization: Bearer dk_live_your_api_key"
# -> process items, then persist the max updatedAt as the next cursorWhy not poll by status?
Filtering by status=new alone misses later changes (e.g. a
guest-cancelled order you already imported). The
updatedSince cursor catches every change exactly because it
keys on updatedAt, not on a single status.