Error Codes
Every non-2xx response from the DialStack REST API shares the same shape:
{
"error": "Human-readable message describing what went wrong.",
"code": "machine_readable_code",
"details": { "field_name": "reason" }
}
error is always present. code is present on errors that have a stable
machine-readable identifier (listed below). details is optional and carries
field-level validation context when applicable.
Branch on code, not on error. The error string may be rephrased
without notice; code values are stable and versioned. When no code
is present, fall back to the HTTP status.
HTTP status codes
| Status | Meaning | Retry? |
|---|---|---|
400 | Malformed request — invalid JSON, missing required fields. | No — fix the request. |
401 | Missing or invalid API key (Authorization: Bearer ...). | No — rotate / check the key. |
403 | Valid key, but no access to this account or resource. | No — check DialStack-Account. |
404 | Resource does not exist on this account. | No. |
409 | Conflict — e.g., duplicate handle, already assigned. | No. |
422 | Validation error — payload is well-formed but semantically wrong. | No — fix the payload. |
429 | Rate limit exceeded. Retry-After header tells you when to retry. | Yes, after Retry-After. |
5xx | Server error. Transient unless documented otherwise. | Yes, with exponential backoff. |
Stable error codes
The following code values are stable and safe to branch on. Codes not listed
here are either legacy or platform-internal; do not rely on them.
| Code | HTTP | Meaning |
|---|---|---|
validation_error | 400 | The request body failed validation. See details for field-level reasons. |
invalid_pagination | 400 | The pagination cursor is malformed or expired. Restart from the first page. |
invalid_sandbox_phone_number | 400 | The supplied phone number is not a valid sandbox number for this platform. |
sandbox_feature_unsupported | 422 | The requested feature is not available in sandbox mode for this platform. |
Phone numbers
| Code | HTTP | Meaning |
|---|---|---|
phone_number_limit_exceeded | 409 | The request would take the account past its phone number limit. Raise the limit and retry. |
phone_numbers_already_claimed | 409 | One or more of the requested numbers are already in service and cannot be ordered. The error string names them. |
port_order_invalid_state | 409 | The port order's current status does not allow the action. |
phone_number_limit_exceeded is returned when buying numbers, when creating or
editing a port order, and when recovering a released number, since each of those
grows the account's count. Raising the account's limit — or releasing numbers it
no longer needs — clears it.
phone_numbers_already_claimed names the numbers that are unavailable — in the
error string for a human, and in a details object for your code:
{
"code": "phone_numbers_already_claimed",
"error": "phone number(s) already claimed — already on this account: +17702126011",
"details": {
"already_on_account": ["+17702126011"],
"in_service_elsewhere": []
}
}
This is the only code that carries details, because which of your numbers
conflicted is the one thing here you cannot get from any other request. Both keys
are always present; an empty group is []. The two are separated because they
call for different actions:
- Already on your account. The number is yours; it does not need to be ordered again. Take it off the request.
- Already in service elsewhere. The number is live on another account with the same carrier, so there is no port to run — moving it is an intra-carrier transfer, which support arranges. Contact support rather than retrying.
A number counts as claimed only while it is live — active or inactive — on any
account. A number you released is not claimed, so it will not be reported
here; request it back with POST /v1/phone-numbers/{phone_number_id} and
status: active instead, which is the one case that can return
phone_number_limit_exceeded on an update.
Note that a conflict confirms the number is in service with this carrier. That is not treated as a disclosure boundary: you supplied the number in the request, so the response tells you nothing about it you did not already know, beyond whether it is available — which is the question you asked. Abuse of this endpoint to enumerate numbers is addressed by rate limiting, not by withholding the answer.
port_order_invalid_state is returned when the action does not fit the order's
current status — editing an order that has moved past the point where changes
are accepted, cancelling one in a terminal status, submitting one that is not
approved, approving one that is no longer a draft, or uploading a document to an
order that is neither a draft nor in exception. The error message names both
the status and the attempted action, so it reads as
port order is in "submitted" status, cannot update.
The code is the whole machine-readable contract on these responses. There is no
details object — everything one could carry is either already known to the
caller (the numbers they sent, the endpoint they called, their own cap) or not
theirs to see. Read error for a human-readable description.
Some conflicts on these endpoints carry no code, because there is no action a
client can take automatically: a caller ID change already being processed, an
account that already has a directory listing on another number, a number that is
not in a recoverable state, a recovery the carrier declined, and a disconnect
refused inside the 30-day minimum hold. Branch on the status and surface the
error message for those.
Device provisioning
| Code | HTTP | Meaning |
|---|---|---|
unsupported_vendor | 400 | The phone vendor/model is not supported for provisioning. |
invalid_configuration | 400 | The provisioning request is missing required device fields. |
credentials_not_found | 404 | No SIP credentials exist for this endpoint. |
generation_failed | 500 | The provisioning file could not be generated. Safe to retry. |
Handling errors
- Treat missing
codeas an unstable error. Log theerrorstring for debugging, but make a business decision off HTTP status alone. - Do not parse the
errorstring. It is subject to i18n and rephrasing. - On
429, respectRetry-After. Typical values are small (≤ 60s). - On
5xx, use exponential backoff. Start at 1s, double up to ~60s, cap at ~5 attempts. Every DialStack mutation is keyed by an operation ID so retries are safe — idempotency is built into the API.