Errors & status codes

View as Markdown

Dial uses standard HTTP status codes. Successful reads return 200; successful creates (a sent message, a placed call, a provisioned number) return 201.

Status codes

CodeMeaning
200Success.
201Resource created (message sent, call placed, number provisioned).
400The request body failed validation.
401Missing or invalid API key.
402Insufficient Dial credit — a pay-as-you-go number with a credit balance at or below zero. See Billing.
404The resource doesn’t exist on your account.
408A events/wait request timed out before a matching event arrived.
409Conflict — e.g. an Idempotency-Key whose original request is still processing.
503The event stream isn’t configured on this deployment.

Error shape

Errors carry an error field. For most failures it’s a string:

1{ "error": "Phone number not found" }

For 400 validation failures it can be an object describing the invalid fields:

1{ "error": { "fieldErrors": { "to": ["Required"] } } }

Always branch on the HTTP status first, then read error for detail.

Retries and idempotency

Reads are safe to retry. GET requests — list numbers, messages, calls, account, usage — have no side effects, so retry them freely.

Placing a call is safe to retry with an Idempotency-Key. POST /api/v1/calls accepts an optional Idempotency-Key header — a unique client-generated key (e.g. a UUID) identifying the call attempt:

  • If the original attempt placed the call, a retry with the same key returns the original call with 200 instead of dialing again. (A fresh call returns 201, so you can tell them apart.)
  • If a duplicate arrives while the original is still processing, it waits for the original and then replays its result. If the original is still going when the wait window closes, the duplicate gets 409 — retry with the same key shortly.
  • A non-2xx response guarantees no live call: if the call already fired when the failure happened, Dial cancels it before responding. Retrying with the same key is always safe.
$curl -X POST https://api.getdial.ai/v1/calls \
> -H "Authorization: Bearer $DIAL_API_KEY" \
> -H "Idempotency-Key: 4cf1a3b2-7c4e-4a2f-9d3e-2f1a8b9c0d1e" \
> -H "Content-Type: application/json" \
> -d '{"to": "+14155550123", "fromNumberId": "pn_123", "outboundInstruction": "..."}'

Other writes are not idempotent. Without an Idempotency-Key, every accepted POST is a distinct action and a retry creates a new resource:

  • Retrying POST /api/v1/messages sends a second SMS.
  • Retrying POST /api/v1/calls without the header places a second call.
  • Retrying POST /api/v1/numbers provisions another number.

The SDKs and CLI make a single attempt per call and never retry on your behalf, so each retry is a deliberate choice you make.

When a non-idempotent write fails ambiguously — a network timeout, a dropped connection, or an unexpected 5xx — you can’t tell whether it took effect. Don’t blind-retry. Confirm first: list recent messages or calls (GET /api/v1/messages, GET /api/v1/calls) and re-send only if the action isn’t already there. Retrying is always safe after a 400 or 401, because the request was rejected before any action ran.

Billing: PAYG vs FIXED

Billing is per phone number. A number is either:

  • Pay-as-you-go (PAYG) — its calls, SMS, and monthly ownership are billed against your account’s credit wallet. When the wallet balance is at or below zero, new spend on a PAYG number is rejected with 402: placing a call, sending an SMS, and provisioning a number all return 402; inbound calls are declined. Add credit to continue.
  • Subscription (FIXED) — covered by an active subscription; never gated by the wallet.

Check balances and per-number mode with GET /api/v1/billing (or dial billing).

Handling timeouts

A events/wait call returns 408 when no matching event arrives in the window — this is expected, not a failure. Retry the wait, or keep a live stream open instead. The CLI’s dial wait-for exits non-zero on timeout for the same reason.