Polling and idempotency
Polling is the source of truth
Section titled “Polling is the source of truth”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.
Sitora sets your polling cadence
Section titled “Sitora sets your polling cadence”Every task-list response — 200 and 304 alike — tells you how many seconds
to wait before asking again:
X-Sitora-Poll-After: 2Retry-After: 2Both 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.
Rate limits
Section titled “Rate limits”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.
Idempotency
Section titled “Idempotency”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.
The error envelope
Section titled “The error envelope”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.
Refusals that carry no code
Section titled “Refusals that carry no code”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."]}. 401and403return{"detail": "..."}. A401means the key is invalid or revoked. A403means 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.429returns{"detail": "..."}with aRetry-Afterheader.