Errors & retries
Every error shares one shape, so you can branch on error.code rather than parsing prose.
{
"error": {
"code": "<machine-readable>",
"message": "<human-readable>",
"details": {}
}
}
Status table
| HTTP | code | When |
|---|---|---|
| 400 | invalid_or_expired | Pair code doesn't exist, expired, or already consumed. |
| 400 | missing_upload | finalize with no prior upload (or the file is absent). |
| 400 | idempotency_key_required | Full-snapshot finalize without an Idempotency-Key. |
| 401 | missing_hmac_headers | A required HMAC header is absent. |
| 401 | bad_tenant_id | X-VoiceBot-Tenant-Id is not a valid UUID. |
| 401 | bad_timestamp | X-VoiceBot-Timestamp is not an integer epoch. |
| 401 | stale_timestamp | Timestamp drift > 300s. |
| 401 | nonce_replay | Nonce already seen within its 600s window. |
| 401 | tenant_not_found | No connected provider connection for this tenant. |
| 401 | missing_secret | Shared secret is missing or undecryptable. |
| 401 | bad_signature | HMAC mismatch. |
| 404 | not_found | sync_id doesn't exist or belongs to another tenant. |
| 409 | conflict_full_sync_in_progress | A snapshot is already uploading / dispatching. |
| 413 | (see note) | Upload exceeds max_size_bytes (200 MB). Returns the FastAPI {"detail": …} body, not the ingest envelope. |
| 426 | protocol_upgrade_required | X-VoiceBot-Protocol-Version ≠ 2. Flat shape — see note below. |
| 429 | rate_limited | Over a rate limit. Retry-After reports the window. |
| 500 | internal | Unexpected backend error (no internal detail returned). |
426 breaks the error envelopeEvery row above uses the { "error": { "code", "message", "details" } } shape except 426.
The protocol-version mismatch is rejected by a separate handler, before the body is read, and returns
a flat object where error is the string code itself:
{
"error": "protocol_upgrade_required",
"expected_version": 2,
"received_version": 1,
"message": "Expected sync protocol version 2; received 1"
}
A producer branching on error.code gets undefined on a 426. Branch on the HTTP status for this
one case (or handle error being a string). The 413 upload-too-large is also not the ingest
envelope — it's the framework's {"detail": …} body.
The retry rule
This is the single most important operational detail of the protocol:
The backend records the nonce before running the handler, so a 429 or 5xx has already
consumed that nonce. Re-sending the identical signed request is rejected as nonce_replay. To
retry a signed request you must build a fresh one — new timestamp, new nonce, new signature.
What is and isn't safe:
| Request | Retry on connection error | Retry on 429 / 5xx |
|---|---|---|
POST /pair (unsigned) | Yes | Yes, honour Retry-After |
PUT /upload/{id} (unsigned URL) | Yes (within expires_at) | Yes, honour Retry-After |
POST /init (signed) | Fresh request only | Fresh request only |
POST /finalize (signed) | Use Idempotency-Key | Use Idempotency-Key |
POST /events (signed) | Use batch_id / Idempotency-Key | Use batch_id / Idempotency-Key |
GET /status (signed) | Fresh request only | Fresh request only |
The unsigned legs (the pair handshake and the snapshot upload, which has no HMAC) are the only ones
safe to blind-retry on 429/5xx with backoff.
Idempotency for exactly-once
For the two mutating signed endpoints, idempotency keys give you exactly-once even across a lost connection:
Idempotency-Key(UUID) on/finalize(required for full snapshots) and optionally on/events. The full response is cached for 24h and a repeat with the same key returns it without re-running.batch_id(UUID) on/eventsis deduplicated server-side; a replay returnsidempotent_replay: truewithprocessed: 0.
Use a stable key per logical operation (e.g. derive it from the sync_id or batch_id) so a retry
reuses the same key rather than minting a new one.
Per-event errors
/events is partial-success: the batch returns 200 with a per-index errors array for the
operations that failed. Failed ops are dead-lettered for review and are not applied — fix the
payload and re-send those events; don't replay the whole batch under the same batch_id (it would be
treated as a dedup replay).
Back to the reference
- Overview · Auth & HMAC · Endpoints · Entities