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 continuously while your fleet is on duty.

Every list response 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.

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. 429 responses carry Retry-After — honor it.

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.

Every refused request returns one shape:

{ "detail": "Human-readable explanation.", "code": "machine_readable_code" }
HTTP status Codes
400 unknown_status, status_not_reportable, unknown_failure_code
404 task_not_found
403 fleet standing lost — certification, activation, or entitlement
409 already_claimed, robotics_paused, platform_frozen, arrival_dwell_pending, idempotency_key_reused, task_<current_status> ordering conflicts
410 offer_expired

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