Errors

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.

{
  "error": {
    "type": "invalid_request_error",
    "code": "invalid_domain",
    "message": "domain is not verified",
    "param": null
  }
}
FieldTypePurpose
typestringCoarse error class to switch on at a high level.
codestringStable machine code for the specific condition. Branch on this.
messagestringHuman-readable explanation, for display and logs, not for branching.
paramstring \| nullThe 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:

codeStatusMeaning
invalid_request400Malformed request (bad JSON, missing or blank required field).
invalid_domain400 / 422The domain is unknown, unverified, or not usable for this action.
idempotency_key_mismatch400The Idempotency-Key header and body idempotencyKey are set to different values.
validation_failed422The request was well-formed but a field failed validation.
duplicate_resource409The resource already exists (domain, webhook URL, inbox, and so on).
scope_missing401 / 403The key lacks the scope this endpoint requires.
not_permitted401 / 403Authenticated, but not allowed to perform this action.
resource_not_found404No such resource for this account, or the route does not exist.
link_no_longer_available410A share link was revoked, expired, or used up.
payload_too_large413The request body or an attachment exceeds the size limit.
plan_limit_reached429 / 413A plan quota was hit (monthly email cap, storage cap).
rate_limited429Too many requests; retry after the Retry-After window.
budget_exceeded429A spend budget for the scope was exceeded.
spend_cap_reached429The account spend cap was reached.
provider_unavailable502Delivery is temporarily unavailable; safe to retry with backoff.
service_unavailable503The service is temporarily unavailable; safe to retry with backoff.
internal_error500An 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:

typeCovers
invalid_request_errorinvalid_request, invalid_domain, idempotency_key_mismatch, validation_failed, payload_too_large
authentication_errorscope_missing, not_permitted (on a 401)
permission_errorscope_missing, not_permitted (on a 403)
not_found_errorresource_not_found, link_no_longer_available
conflict_errorduplicate_resource, and the condition-specific 409 codes
rate_limit_errorrate_limited, plan_limit_reached, budget_exceeded, spend_cap_reached
api_errorprovider_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:

import { DairoError } from "dairo";

try {
  await dairo.messages.send({ inboxId: "inbox_123", to: "x@example.com", subject: "Hi", text: "…" });
} catch (err) {
  if (err instanceof DairoError) {
    switch (err.code) {
      case "invalid_domain":   /* verify the sending domain */ break;
      case "scope_missing":    /* this key can't send mail */ break;
      case "rate_limited":     /* back off and retry */ break;
      default:                 console.error(err.type, err.code, err.message, err.param);
    }
  }
}

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.

On this page