Skip to content

Polling and idempotency

GET /serving-tasks/?status=offered is how you discover work. Webhooks, if registered, are a latency optimization — never truth. Poll for as long as your fleet is on duty, at the cadence Sitora hands you.

One page answers at most limit tasks — 50 by default, 100 at most. A busy counter can hold more offers than one page shows, so raise limit rather than reading a short list as a quiet kitchen. The webhook delivery listing takes the same parameter, defaulting to 100 and capped at 200.

Every task-list response — 200 and 304 alike — tells you how many seconds to wait before asking again:

X-Sitora-Poll-After: 2
Retry-After: 2

Both headers carry the same number. Wait that long, then poll again.

Do not hard-code the interval. Sitora can raise it fleet-wide with no release of yours, which is how a capacity problem gets handled by slowing fleets down instead of refusing their traffic. Two seconds is what the task list asks for today.

Retry-After on a successful response is pacing, not an error. A 200 or a 304 carrying it means exactly what it says: here is the answer, and here is when to come back.

The heartbeat and the fleet snapshot are not paced this way. Keep heartbeating on your own timer, about every 30 seconds, as heartbeat and fleet state describes.

The task list carries an ETag. Send it back as If-None-Match; an unchanged list returns 304 Not Modified with no body. This makes tight polling loops nearly free for both sides — always implement it.

The webhook delivery listing carries no ETag. Page through it with since_id instead.

Rates are strict and keyed per credential:

Surface Limit
Task polling 60/minute (~1/s)
Task claim/report 120/minute
Heartbeat 30/minute

A controller that needs more is misbehaving, not busy. A 429 is Sitora refusing you, not pacing you — its Retry-After must be honored, and reaching one at all means your loop ignored the cadence header above.

Every mutation accepts an Idempotency-Key header (or a client_idempotency_key body field). A duplicate request with the same key returns the original outcome and records nothing new.

Always send one. Networks fail mid-request; with a key you can retry freely and never double-apply a claim or a report.

Use one key per action, not one per task: reuse the same key for retries of the same logical action, and a fresh key for each new one. A key already spent on a different transition of the same task is refused with 409 (idempotency_key_reused). Keys are capped at 128 characters.

Request bodies are bounded too — the optional telemetry and heartbeat summary objects must be JSON objects of at most 4 KB, nested no more than 5 levels, with no more than 50 entries in any one object or array.

A refusal the robotics surface raises itself returns one shape:

{ "detail": "Human-readable explanation.", "code": "machine_readable_code" }
HTTP status Codes
400 unknown_status (an unknown value in the status filter), invalid_limit, invalid_since_id, unknown_failure_code
404 task_not_found, delivery_not_found
409 already_claimed, robotics_paused, platform_frozen, arrival_dwell_pending, idempotency_key_reused, task_<current_status> ordering conflicts
410 offer_expired
503 content_key_missing — this Sitora deployment is misconfigured, so retrying will not help

Branch on code, never on the detail text — detail wording may change; codes are contractual.

Three refusals are answered before your request reaches the robotics surface, so they have no code field. A client that reads body["code"] unconditionally crashes on all three — read it defensively.

  • A malformed body is refused by field validation, and the body is keyed by the field at fault: {"status": ["\"loaded\" is not a valid choice."]}.
  • 401 and 403 return {"detail": "..."}. A 401 means the key is invalid or revoked. A 403 means the key is valid but may not act: either your fleet has lost standing — certification, activation, or plan entitlement — or the key is not scoped for that endpoint. The reason is named in the text.
  • 429 returns {"detail": "..."} with a Retry-After header.