# Errors and HTTP status codes

## Error format

Errors are JSON with a human readable `detail` and, when available, a stable machine `code`:

```json
{ "detail": "invalid, expired or revoked API key", "code": "unauthorized" }
```

Validation errors add an `errors` list, one entry per problem, with the dotted path of the
field (`loc`) and a message (`msg`):

```json
{
  "detail": "invalid scenario",
  "code": "invalid_scenario",
  "errors": [{ "loc": "target.phone", "msg": "Value error, phone must be E.164, e.g. +15550123456" }]
}
```

Match on `code` and the status, never on `detail`: texts can change. Every error has a
`code`. New codes can appear: treat an unknown `code` like the generic code of its status.

## Status codes

| Status | Generic `code` | Meaning | What to do |
|---|---|---|---|
| `200` `201` `204` | | Success. `201` created, `204` no body | |
| `202` | | CI mode: the run is still going at the wait timeout | Poll `GET /runs/{id}` |
| `400` | `bad_request` | The request is valid JSON but cannot be done (see below) | Read `code` and `detail` |
| `401` | `unauthorized` | Missing, malformed, unknown, expired or revoked key, or suspended organization | Check the key |
| `403` | `forbidden` | Not allowed: see the specific codes below | Add the scope, fix the network or upgrade |
| `404` | `not_found` | Unknown id, or an object of another organization | Check the id |
| `409` | `conflict` | Conflict: see the specific codes below | Read `detail` |
| `410` | `endpoint_sunset` | The endpoint was removed after its deprecation period | See [Versioning](versioning.md#deprecation-policy) |
| `412` | | CI mode: the run finished and **failed** the quality gate. Body: the run | Fail the pipeline |
| `422` | `invalid_request` | The request does not match the schema (wrong type, missing field, out of range) | Fix the fields in `errors` |
| `422` | `invalid_scenario` | The scenario or variant is invalid | Fix the fields in `errors` |
| `429` | `rate_limited` | Too many requests | Wait `Retry-After` seconds |
| `503` | `service_unavailable` | A dependency is not available | Retry later |
| `5xx` | | Server error | Retry `GET` requests with backoff; quote `X-Request-Id` to support |

## Specific codes

### `400`

| `code` | Endpoint | Meaning |
|---|---|---|
| `invalid_threshold` | `POST /scenarios/{id}/runs` | `threshold must be between 0 and 100` |
| `run_not_possible` | `POST /scenarios/{id}/runs` | The scenario is archived, or none of the requested variants exist or are enabled |
| `replay_not_possible` | `POST /replays` | The bundle's target number is not registered in this organization |
| `invalid_bundle`, `missing_bundle` | `POST /replays`, `.../variants/from-bundle` | `invalid bundle at <field>: <message>`, or neither `call_id` nor `bundle` given |
| `invalid_target` | `POST /targets` | The number is invalid, or its destination cannot be called |
| `attestation_not_accepted`, `attestation_signer_missing`, `verification_failed` | `POST /targets/{id}/verify`, `.../verification/attest`, `.../verification/start` | The attestation was not accepted (`accept` is not `true`, or the signer name is missing or shorter than 3 characters) |
| `attestation_required` | `.../verification/start` | Sign the attestation first (in the same request, or with `.../verification/attest`) |
| `method_unavailable`, `unknown_method`, `wrong_method` | `.../verification/start`, `.../verification/call` | The verification method is not available for this number, unknown, or not the one of the active code |
| `no_active_code` | `.../verification/call` | No active code: ask for a new one (`.../verification/retry`) |
| `unsafe_url` | `POST /webhooks`, `PATCH /webhooks/{id}` | The URL is not `https`, or its host resolves to a private address |
| `unknown_event` | `POST /webhooks`, `PATCH /webhooks/{id}` | An event type does not exist, or the list is empty |
| `invalid_filters` | `POST /webhooks`, `PATCH /webhooks/{id}` | A filter names an unknown scenario, tag or environment, or an unknown filter key |
| `invalid_tags`, `invalid_request` | `POST /tags/bulk` | Nothing to add or remove, no object, or an unknown object kind |
| `unknown_environment`, `invalid_environment` | Runs, versions, environments, comparisons | The environment (slug, name or id) does not exist in your organization |
| `unknown_version`, `invalid_version` | `POST /scenarios/{id}/runs`, versions, comparisons | The version does not exist in that environment, or its label is invalid |
| `unknown_target`, `unknown_scenario` | Versions, environment bindings | The number or the scenario does not exist in your organization |
| `invalid_name`, `invalid_color`, `invalid_schedule`, `invalid_source_url`, `invalid_action` | Environments, versions, bindings | A field is invalid (see `detail`) |
| `invalid_period` | `GET /environments/stability`, `GET /environments/compare` | `days` must be 7, 30 or 90 |
| `invalid_alert_channel` | `POST /alert-channels`, `PATCH /alert-channels/{id}` | Not a Slack incoming webhook URL, invalid email address, more than 10 recipients, or no rule |
| `invalid_cidr` | `POST /api-keys`, `PATCH /api-keys/{id}` | An allowed network is not a valid IP address or CIDR, or there are more than 20 |
| `key_not_active` | `POST /api-keys/{id}/rotate` | The key is already revoked or expired |
| `invalid_url` | `POST /wallet/checkout`, `/billing/...` | A redirect URL is not `https` |
| `unknown_plan` | `POST /billing/subscription/checkout` | Not a public plan that can be bought online |
| `no_stripe_customer` | `POST /billing/portal` | No subscription was ever bought online: subscribe first |
| `key_creator_missing` | `POST /invitations` | The admin who created the key no longer exists: create a new key |
| `not_allowed` | `POST /invitations`, `PATCH /members/{id}`, `DELETE /members/{id}` | Already a member, or the last admin (the seat limit is a `403`, below) |

### `403`

A plan refusal (`feature_not_in_plan`, `country_not_in_plan`) also carries
`required_plan` (the cheapest plan that lifts it) and `upgrade_url` (a one click upgrade page),
when such a plan exists. An out of range top-up (`POST /wallet/checkout`) is a `422`
`invalid_request` on `amount_cents`, like any schema error.

| `code` | Meaning | Example `detail` |
|---|---|---|
| `missing_scope` | The key lacks the scope the endpoint needs | `This API key lacks the 'write' scope (scenario.create).` |
| `ip_not_allowed` | The key is restricted to IP networks and the request comes from elsewhere | `This API key cannot be used from this IP address.` |
| `feature_not_in_plan` | Your plan does not include the feature | `Custom test data injection is not included in your plan (Starter). Upgrade to Growth to use it.` |
| `feature_not_in_plan` (`feature: seats`) | `POST /invitations`: every seat of the plan is taken (pending invitations count) | `Your plan includes 3 seats, all taken (pending invitations count). Upgrade to Growth to invite more people.` |
| `country_not_in_plan` | The number's country exceeds your plan's countries | `Your plan allows 1 phone country (US); GB is not one of them. Growth includes 5 phone countries.` |
| `blocked_policy` | `POST /scenarios/{id}/runs`: the scenario is held for a human review or refused by the [compliance review](compliance.md) | `Refused by the compliance review of this scenario (acceptable use policy): ... Change the scenario, or contact CallQA support to ask for a human review if you think this is a mistake.` |

### `409`

| `code` | Meaning |
|---|---|
| `already_exists` | A tag, an environment or a version with this name or label already exists |
| `target_in_use` | The number has scenarios or call history: archive it instead |
| `scenario_in_use` | The scenario cannot be deleted: archive it instead |
| `environment_in_use`, `default_required` | The environment has runs or versions (it is kept so their history stays readable), or it is the default environment: make another one the default first |
| `run_not_finished`, `call_not_finished` | `DELETE /runs/{id}` or `DELETE /calls/{id}` while a call is queued or in progress: cancel it first |
| `verification_record` | `DELETE /calls/{id}` on a number verification call, kept as evidence |
| `export_period_over`, `too_large`, `export_error` | `POST /exports`: the export period of a closed organization is over, or the archive is too large (contact support) |
| `call_not_judged` | The call has not been judged yet |
| `subscription_exists` | Already subscribed: change the plan in the billing portal |
| `already_verified` | Number verification: the number is already verified |
| `not_pending` | Number verification: start a verification first |
| `call_in_progress`, `verification_in_progress` | Number verification: a verification call is already running (for this number, or another number of your organization) |
| `number_suspended` | Number verification: the number is suspended by CallQA support |

### `429` (besides `rate_limited`)

| `code` | Meaning |
|---|---|
| `number_locked` | Number verification: 3 failed attempts in 24 hours, the number is locked for 24 hours |
| `org_daily_limit` | Number verification: at most 10 verification calls per organization and 24 hours |

### `503`

| `code` | Meaning |
|---|---|
| `generator_unavailable` | The scenario generator is not configured or not reachable |
| `payments_unavailable` | Payments are not configured on this server |

## Call statuses and block reasons

A call can fail without any HTTP error: the trigger succeeded, but the call itself did not
go through. Look at the call's `status`, `block_reason` and `error`.

| `status` | Final | Meaning |
|---|---|---|
| `queued` | no | Waiting for a slot |
| `dispatching`, `dialing`, `ringing`, `in_progress` | no | Being placed or in conversation |
| `judging` | no | The call ended, verdicts are being computed |
| `completed` | yes | Judged: see `verdict` and `score` |
| `error` | yes | Failed after being queued: launch failure, caller error, or no result after the maximum time (the call's `limits.max_duration_s` plus 10 minutes, 40 minutes at least: `error` says "No completion received from the caller agent (timed out)."). Verdict `error`, reservation released |
| `canceled` | yes | Canceled before dialing |
| `blocked_policy` | yes | Not placed: a rule forbids it (see `block_reason`) |
| `blocked_insufficient_credit` | yes | Not placed: not enough quota or wallet credit |

`block_reason` values:

| `block_reason` | `status` | Meaning | Fix |
|---|---|---|---|
| `number_not_verified` | `blocked_policy` | The target is not verified and has no attestation | [Verify it](number-verification.md) |
| `number_locked` | `blocked_policy` | 3 failed verification attempts in 24 hours | Wait 24 hours or contact support |
| `number_suspended` | `blocked_policy` | Suspended by CallQA support | Contact support |
| `sandbox_no_batch` | `blocked_policy` | Scheduled or CI run on an unverified number | Verify the number |
| `sandbox_daily_cap` | `blocked_policy` | More than 3 calls in 24 hours to an unverified number | Verify the number |
| `attestation_outdated` | `blocked_policy` | The attestation was signed under an older text | Sign the new attestation |
| `target_archived` | `blocked_policy` | The target was archived | Use an active number |
| `country_not_in_plan` | `blocked_policy` | The number's country exceeds your plan | Upgrade the plan |
| `blocked_destination` | `blocked_policy` | This kind of line cannot be called (premium rate, shared cost...) | Use another number |
| `invalid_number` | `blocked_policy` | Not a valid, dialable number | Fix the number |
| `emergency_number` | `blocked_policy` | Emergency and public safety numbers (911, 988, 112, 999, 15, 17, 18, 116xxx...) are never called | |
| `compliance_blocked` | `blocked_policy` | The [compliance review](compliance.md) refused the content of this call; `error_message` gives the reasons | Change the scenario, or ask support for a human review |
| `compliance_review` | `blocked_policy` | The content of this call is held for a human compliance review by CallQA staff | Wait for the decision, or contact support |
| `daily_spend_cap` | `blocked_policy` | CallQA's platform wide daily safety limit is reached (not your account) | Retry later or contact support |
| `org_suspended` | `blocked_policy` | Your organization is suspended | Contact support |
| `org_closed` | `blocked_policy` | Your organization is closed | |
| `subscription_inactive` | `blocked_policy` | No active subscription | Choose a plan |
| `insufficient_credit` | `blocked_insufficient_credit` | The wallet cannot cover the call | Top up the wallet |
| `quota_exhausted` | `blocked_insufficient_credit` | Quota used up, the plan has no overage | Upgrade the plan |
| `spend_cap_reached` | `blocked_insufficient_credit` | Monthly spending cap reached | An admin raises the cap (`PATCH /billing/settings`) |
| `overage_disabled` | `blocked_insufficient_credit` | Quota and bonus calls used up, and paid overage is off | An admin turns overage on, or upgrades |

Some conditions only **delay** a call, it stays `queued`: a [compliance review](compliance.md)
still in progress (a few seconds), your plan's concurrent call limit,
the maximum number of calls per hour to the same number (30), for scheduled runs the call
window of the scenario in that environment, and, for an unverified number in the sandbox, one
call at a time between 8 am and 9 pm local time (see
[Number verification](number-verification.md#sandbox-attestation-signed-number-not-verified)).

## Retrying safely

- `GET` requests are safe to retry.
- A `429` is answered before anything happens: retrying any request after `Retry-After` is safe.
- `POST /scenarios/{id}/runs` and `POST /replays` create a new run each time. After a network
  error, check `GET /runs?scenario_id=...&since=...` before triggering again, or you may pay
  for two runs.
- `POST /scenarios/import` is an upsert by name: retrying it is safe.
