Pagination
List endpoints (/v1/listings, /v1/orders) return cursor-paginated results. Cursors are opaque and stable. They survive concurrent inserts so you can page through a list at any rate without missing or duplicating rows.
Response shape
{
"object": "list",
"data": [ /* up to `limit` rows */ ],
"has_more": true,
"next_cursor": "eyJpZCI6IjI0Zjg…"
}- data: array of resource objects (
listing/order). - has_more: boolean.
truewhen more rows exist past this page. - next_cursor: opaque string. Pass it back as
afteron the next call.nullwhenhas_moreis false.
Walking a list
# First page
curl -s "https://watchtraderhub.com/api/v1/listings?limit=100" \
-H "Authorization: Bearer wth_YOUR_API_KEY"
# → { "data": [...], "has_more": true, "next_cursor": "eyJ..." }
# Subsequent pages, pass the previous next_cursor as `after`
curl -s "https://watchtraderhub.com/api/v1/listings?limit=100&after=eyJ..." \
-H "Authorization: Bearer wth_YOUR_API_KEY"Loop until has_more is false (or equivalently, next_cursor is null).
Incremental sync with updated_after
Once you've done an initial backfill, use updated_after to fetch only what changed:
curl -s "https://watchtraderhub.com/api/v1/listings?updated_after=2026-05-08T00:00:00Z" \
-H "Authorization: Bearer wth_YOUR_API_KEY"The server returns rows where updated_at > updated_after in the same descending-by-updated_at order as the unfiltered list, and after still works for paging through the filtered subset.
Recommended sync loop
updated_at of the most recent row you ingested. On the next sync, set updated_after to that value. Webhook events listing.updated / order.received are the realtime path; updated_after is the catch-up path for when your receiver was offline.Cursors are opaque, really
The string in next_cursor is a base64url-encoded JSON object containing (id, updated_at). That's an implementation detail. Do not parse it, do not modify it, and do not generate one yourself. The format is free to change between API versions.
Hand-crafted cursors return 400 invalid_cursor.
Tie-break behaviour
We order by (updated_at DESC, id DESC). The cursor carries both values so two rows with identical updated_at resolve deterministically. No skipped or duplicated rows at page boundaries, even when timestamps collide (e.g. bulk inserts).
Limits
limitdefaults to 50, max 250.- The endpoint always overshoots by 1 row internally to compute
has_more. That extra row is never returned indata. - Cursors don't expire. A cursor minted today still works tomorrow against the same dataset.