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. On a 10DLC submission (tendlc_validation_failed) error is an object keyed by field — including phone when it’s one of your Dial numbers, which carriers won’t verify.
401Missing or invalid API key.
402Insufficient Dial credit — a pay-as-you-go number with a credit balance at or below zero, or a balance below a one-time fee such as 10DLC registration. See Billing.
403The action isn’t available for this resource, account, or destination — e.g. iMessage on an ineligible account, 10DLC registration on a number it doesn’t apply to (tendlc_not_applicable, tendlc_not_available), or a destination in a region Dial can’t reach (unsupported_region).
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.
429A rate or concurrency limit was hit — an iMessage sending limit, either a number at its daily new-conversation cap or a new contact who hasn’t replied yet (rate_limited), or a free account already at its concurrent-call limit (call_limit_reached).
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.

Restricted destinations

A 403 with unsupported_region means the destination’s region isn’t reachable from Dial — either it’s embargoed, or calling and messaging permissions for that country aren’t enabled on the account placing the request. It’s a property of the number you asked for, not of your request, so retrying won’t change the outcome:

1{
2 "error": "Calls to this destination are not available due to regional restrictions. To request access to this region, contact support@getdial.ai or reach out to us on Discord (https://discord.com/invite/tEmpesw6GW).",
3 "code": "unsupported_region"
4}

POST /api/v1/messages returns the same code with wording for messages. Where the restriction is a permission rather than an embargo, the message says so and how to ask — some regions can be enabled on request.

Unexpected failures

Rarely, a request fails in a way Dial can’t classify. Those responses carry a generic message with a reference code:

1{
2 "error": "Please contact support@getdial.ai, or reach out to us on Discord (https://discord.com/invite/tEmpesw6GW). Error reference code: ref_3f9a2c1b7d4e",
3 "code": "bad_request"
4}

The ref_… code identifies that exact failure in Dial’s logs. Quote it to support or on Discord and we can look up what happened without needing you to reproduce it. The HTTP status still describes the class of failure, so keep branching on status as usual.

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).

Free accounts

An account that has never added credit and never subscribed runs with two guardrails:

  • Call length — every call is hard-capped at 5 minutes, regardless of any maxCallDurationSeconds configured on the call, the number, or the account.
  • Concurrent calls — at most 2 calls in progress at once, inbound and outbound combined. An outbound call over the limit is rejected with 429 (error code call_limit_reached); an inbound call over the limit is declined.

Both limits lift permanently the first time you add credit or start a subscription.

Read the exact values from limits on GET /api/v1/account rather than hardcoding them — it’s null once the account has paid:

1{
2 "maxCallDurationSeconds": 300,
3 "limits": { "maxCallDurationSeconds": 300, "maxConcurrentCalls": 2 }
4}

Asking for more than the cap

A maxCallDurationSeconds you send explicitly above limits.maxCallDurationSeconds is rejected with 400 — on POST /api/v1/calls, PATCH /api/v1/numbers/{id}, and PATCH /api/v1/account alike. The request is refused rather than quietly given a shorter call than you asked for.

A cap inherited from the number or account chain is not an error: it’s clamped down to the free ceiling and the call proceeds. So a number whose cap was set to 1800 before the account hit the free tier still places calls — they just end at 5 minutes.

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.