Idempotency
Overview
Every state-changing endpoint (POST, PATCH, DELETE) accepts an Idempotency-Key header. Voxy caches the first response for 24 hours and replays it for any subsequent request that presents the same key — so a network blip never causes a duplicate call, contact, or webhook delivery.
Combine idempotency with the 429 retry-after contract to build a worker loop that's safe under any transient failure.
Idempotency-Key header
- Format: any opaque string up to 255 characters. UUIDv4 is the recommended choice.
- Scope: per workspace + per endpoint. A key minted for
POST /callswon't collide with the same key onPOST /contacts. - TTL: 24 hours from first use. After expiry the key is free to reuse.
# Mint a fresh UUID per logical operation, not per HTTP attempt.
KEY=$(uuidgen)
curl -X POST https://voxyhq.com/v1/workspaces/{id}/calls/originate-manual \
-H "Authorization: Bearer voxy_sk_live_…" \
-H "Idempotency-Key: $KEY" \
-H "Content-Type: application/json" \
-d '{ "agentId": "agt_…", "to": "+15551234567", "from": "+15557654321" }'Retry policy
Retry safely on any transport error (connection reset, 5xx, 429 RATE_LIMITED). Replay the exact same request — same method, same path, same body, same key — Voxy returns the cached HTTP status and body without re-executing the side effect.
The recommended exponential backoff schedule for a worker loop:
| Attempt | Wait before retry | Backoff visual |
|---|---|---|
| 1 | immediate | |
| 2 | 1 s | |
| 3 | 2 s | |
| 4 | 5 s | |
| 5 | 15 s | |
| 6 | 60 s (give up) |
For non-cached failures (4xx other than 429), surface the error to the operator — retrying a 400 with the same payload returns the same 400. Mint a new key only when you intend a brand-new operation.
Examples
Replay the same request to confirm the cached response:
# Retry exactly the same request — same key, same body.
# Voxy returns the cached 202 instead of placing a second call.
curl -X POST https://voxyhq.com/v1/workspaces/{id}/calls/originate-manual \
-H "Authorization: Bearer voxy_sk_live_…" \
-H "Idempotency-Key: $KEY" \
-H "Content-Type: application/json" \
-d '{ "agentId": "agt_…", "to": "+15551234567", "from": "+15557654321" }'With the official SDK, pass idempotencyKey in options:
import { VoxyClient } from '@voxy/sdk';
import { randomUUID } from 'node:crypto';
const voxy = new VoxyClient({ apiKey: process.env.VOXY_API_KEY!, workspaceId: 'ws_…' });
const key = randomUUID();
const call = await voxy.calls.originateManual(
{ agentId: 'agt_…', to: '+15551234567', from: '+15557654321' },
{ idempotencyKey: key }, // ← header is auto-attached
);And from any HTTP client — the header is the only contract:
# Pseudocode — Python SDK ships in the next release.
import os, uuid, httpx
key = str(uuid.uuid4())
res = httpx.post(
f"https://voxyhq.com/v1/workspaces/{ws}/calls/originate-manual",
headers={
"Authorization": f"Bearer {os.environ['VOXY_API_KEY']}",
"Idempotency-Key": key,
},
json={"agentId": "agt_…", "to": "+15551234567", "from": "+15557654321"},
)422 on body mismatch
If you replay an Idempotency-Key with a different request body, Voxy returns 422 IDEMPOTENCY_KEY_REUSED_WITH_DIFFERENT_PAYLOAD. This guards against the two common bugs: a worker re-deriving the key but mutating the body in-flight, and two unrelated callers colliding on the same key.
The safe action is to mint a new key and re-send.
{
"success": false,
"code": "IDEMPOTENCY_KEY_REUSED_WITH_DIFFERENT_PAYLOAD",
"error": "The Idempotency-Key was already used with a different request body.",
"details": null
}