Errors, retries and idempotency
Read an error, decide whether to retry, and retry sends without ever sending twice.
What you'll build
Error handling that tells "fix the request" apart from "try again", and a retry loop for sends that is safe even when a request times out.
Prerequisites
- A secret API key. See Authentication and API keys.
- Node.js 18 or later with
@voicelayer/api, or Python 3.9 or later withvoicelayer, or any HTTP client.
Step 1: Read the error body
A refused request answers an HTTP status and a JSON body:
{
"error": "window_closed",
"message": "The WhatsApp reply window for this conversation has closed.",
"requestId": "req-8f2c1d4b",
"retryable": false,
"docs": "https://docs.vlayers.ai/errors/window_closed"
}| Field | Use it to |
|---|---|
error | Branch in code. It is a stable code; every code has a page. |
message | Log it, or show it to a developer. It may change; don't match on it. |
requestId | Quote it to support, or find the request in GET /v1/request-logs. |
retryable | Decide whether the same request can succeed later. |
details | Extra facts some codes carry, such as the missing scope. |
docs | The code's page: what happened, why, and how to fix it. |
Authentication errors (401 unauthorized) and the event and webhook routes answer a smaller body, { "error", "message" }.
In the TypeScript SDK, pass throwOnError: true and a refusal throws a VoiceLayerApiError with status, code, requestId, retryable, details and docs. In Python, a refusal raises a voicelayer.ApiException with status and body.
Step 2: Decide whether to retry
- 4xx with
retryable: false: the request is wrong for now. Fix it; retrying the same request fails the same way. 429(rate limit or test cap) and5xx: retry after a wait. Honor aRetry-Afterheader when there is one.- A network error or timeout: you don't know whether the request arrived. For a send, retry with the same idempotency key (Step 3).
Back off between retries: wait about 1 s, then 2 s, 4 s and so on, with some randomness, up to a cap.
Step 3: Make sends idempotent
Send an Idempotency-Key header (1–255 printable characters; a UUID works) with every POST /v1/messages and POST /v1/conversations/{id}/messages. Make one key per message you mean to send, and reuse it only for retries of that message.
curl -X POST https://api.vlayers.ai/v1/messages \
-H "Authorization: Bearer $VOICELAYER_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"channel": "sms",
"to": "+15550123",
"content": {
"type": "text",
"text": "Your table for two is booked for 7pm."
}
}'What happens when you retry with the same key:
| Situation | Answer |
|---|---|
| The first request finished | The first result again, with the header idempotency-replayed: true. Nothing is sent twice. |
| The first request is still running | 409 send_in_progress. Wait a few seconds and retry. |
| Same key, different body | 409 idempotency_conflict. Use a new key for a new message. |
Without a key, a retried send can go out twice.
Test it
With a test key, send the same request twice with the same Idempotency-Key value (set it yourself instead of $(uuidgen)). The second answer has idempotency-replayed: true and the same message id.
Common errors
| Error | Status | How to fix |
|---|---|---|
invalid_request | 400 | Read message and details.issues (the validation issues, by path), fix the request and send it again. |
invalid_idempotency_key | 400 | Send 1–255 printable ASCII characters (a UUID works), or leave the header out. |
idempotency_conflict | 409 | Use a new key for a new message; repeat the original body exactly to replay the original request. |
send_in_progress | 409 | Retry with the same key after retry-after (30 seconds). |
rate_limited | 429 | Wait for retry-after, then retry (with the same Idempotency-Key for a send). |
provider_retry_later | 503 | Retry after retry-after, with the same Idempotency-Key. |
messaging_unavailable | 503 | Retry shortly, with the same Idempotency-Key. |
outcome_unknown | 409 | Do not resend under a new Idempotency-Key. Watch the message events (message.sent, message.failed) for the outcome. |