Dairo's error model: one error shape, a stable code to branch on, the full code list with HTTP statuses, and how the SDKs surface it.
When a request fails, Dairo returns one predictable shape and a stable code that says
exactly what went wrong, so your app or your agent can react instead of parsing English.
| Field | Type | Purpose |
|---|---|---|
type | string | Coarse error class to switch on at a high level. |
code | string | Stable machine code for the specific condition. Branch on this. |
message | string | Human-readable explanation, for display and logs, not for branching. |
param | string \| null | The request field the error refers to, such as to.country, when one applies; otherwise null. |
A few structured errors add a details object. Deleting a domain that still owns inboxes
returns 409 with code cascade_confirmation_required and
details: { requiresCascadeConfirmation, inboxes, messages }, so a client can prompt
before deleting.
Branch on code, never on message (the prose can change) and never on HTTP status alone
(one status maps to several codes: a 429 can be rate_limited, plan_limit_reached,
budget_exceeded, or spend_cap_reached). The code names the exact condition, so your
app can self-correct: invalid_domain → verify the domain; scope_missing → request the
missing scope; idempotency_key_mismatch → fix the reused key.
The full code list
Every code an API key request can return, with its typical HTTP status:
code | Status | Meaning |
|---|---|---|
invalid_request | 400 | Malformed request (bad JSON, missing or blank required field). |
invalid_domain | 400 / 422 | The domain is unknown, unverified, or not usable for this action. |
idempotency_key_mismatch | 400 | The Idempotency-Key header and body idempotencyKey are set to different values. |
validation_failed | 422 | The request was well-formed but a field failed validation. |
duplicate_resource | 409 | The resource already exists (domain, webhook URL, inbox, and so on). |
scope_missing | 401 / 403 | The key lacks the scope this endpoint requires. |
not_permitted | 401 / 403 | Authenticated, but not allowed to perform this action. |
resource_not_found | 404 | No such resource for this account, or the route does not exist. |
link_no_longer_available | 410 | A share link was revoked, expired, or used up. |
payload_too_large | 413 | The request body or an attachment exceeds the size limit. |
plan_limit_reached | 429 / 413 | A plan quota was hit (monthly email cap, storage cap). |
rate_limited | 429 | Too many requests; retry after the Retry-After window. |
budget_exceeded | 429 | A spend budget for the scope was exceeded. |
spend_cap_reached | 429 | The account spend cap was reached. |
provider_unavailable | 502 | Delivery is temporarily unavailable; safe to retry with backoff. |
service_unavailable | 503 | The service is temporarily unavailable; safe to retry with backoff. |
internal_error | 500 | An unexpected error; retry, then contact support. |
Some endpoints return a more specific code for a particular condition, in the same
envelope: letter_not_cancelable (409, a letter already dispatched),
letter_content_clipped (422, param templateId: a template letter's rendered
text is cut off, so it is not printed),
upload_link_consumed (409, a one-time upload link reused), and
cascade_confirmation_required (409, the domain-delete case above).
Plan limits on resource counts
Creating an inbox (including connecting a Telegram bot), a domain, or a storage
bucket beyond your plan's count is a 429 whose message names the limit, for
example Your inbox plan limit is reached (3/3). Today that response carries
the code rate_limited, not plan_limit_reached. Waiting does not clear it:
remove an unused resource or upgrade the plan, and compare usage with limits
from GET /v1/whoami before you create more.
The type classes
type groups codes into a coarse class, so you can switch at a high level before drilling
into code:
type | Covers |
|---|---|
invalid_request_error | invalid_request, invalid_domain, idempotency_key_mismatch, validation_failed, payload_too_large |
authentication_error | scope_missing, not_permitted (on a 401) |
permission_error | scope_missing, not_permitted (on a 403) |
not_found_error | resource_not_found, link_no_longer_available |
conflict_error | duplicate_resource, and the condition-specific 409 codes |
rate_limit_error | rate_limited, plan_limit_reached, budget_exceeded, spend_cap_reached |
api_error | provider_unavailable, service_unavailable, internal_error |
Handling errors in the SDKs
The SDKs raise a typed DairoError that surfaces status, type, code, param, and
(when present) requestId. Branch on code:
The status column above carries each code's HTTP status; for the 201-on-create and
204-on-delete conventions, see Response shape.
provider_unavailable, service_unavailable, and internal_error are the codes that
carry no actionable detail; all three are safe to retry with backoff. A reused
idempotency key is handled separately from errors; see
Idempotency.