# Errors

> Couryo API error format, types, HTTP statuses and every code with cause and fix.

Source: https://couryo.com/en/docs/errors

Every error has the same shape. The `code` is stable: you can rely on it in your code. Each `code` always has the same `type` and the same HTTP status.

```json title="Error"
{
  "error": {
    "type": "invalid_request",
    "code": "domain_not_verified",
    "message": "The domain example.com is not verified in this project yet.",
    "param": "from",
    "doc_url": "https://couryo.com/docs/errors#domain_not_verified",
    "request_id": "req_6f2a9c1e7b0k3m4n"
  }
}
```

| Field | What it is |
|---|---|
| `type` | the error category (table below) |
| `code` | the exact reason, stable |
| `message` | a human explanation, in English or Portuguese following `Accept-Language` (Portuguese by default; send `Accept-Language: en`) |
| `param` | the offending field, when there is one (for example `to[1]` or `emails[2].from`) |
| `doc_url` | a link to the explanation of this code |
| `request_id` | the request ID, same as the `Request-Id` header; send it to support if you need help |

## Types

| `type` | HTTP | When |
|---|---|---|
| `invalid_request` | 400 or 422 | the request has a problem you can fix |
| `authentication` | 401 | missing or invalid key |
| `permission` | 403 | the key or the plan does not allow this |
| `not_found` | 404 | the resource or route does not exist |
| `conflict` | 409 | a conflict, such as a reused idempotency key |
| `rate_limited` | 429 | too many calls in a short time |
| `tier_limit` | 429 | a limit of your level, your plan or your spending limit |
| `api_error` | 500 or 503 | a problem on our side; you can retry |

## Codes

| `code` | `type` | HTTP | Summary |
|---|---|---|---|
| [`missing_field`](#missing_field) | `invalid_request` | 400 | a required field is missing |
| [`invalid_field`](#invalid_field) | `invalid_request` | 400 | a field has an invalid value |
| [`invalid_json`](#invalid_json) | `invalid_request` | 400 | the body is not JSON |
| [`batch_too_large`](#batch_too_large) | `invalid_request` | 400 | more than 100 emails in a batch |
| [`invalid_cloudflare_token`](#invalid_cloudflare_token) | `invalid_request` | 400 | Cloudflare token refused |
| [`cloudflare_zone_not_found`](#cloudflare_zone_not_found) | `invalid_request` | 400 | the token cannot reach the domain's zone |
| [`domain_not_verified`](#domain_not_verified) | `invalid_request` | 422 | the `from` domain is not verified |
| [`recipient_suppressed`](#recipient_suppressed) | `invalid_request` | 422 | every recipient is suppressed |
| [`missing_api_key`](#missing_api_key) | `authentication` | 401 | no key |
| [`invalid_api_key`](#invalid_api_key) | `authentication` | 401 | invalid key |
| [`not_authenticated`](#not_authenticated) | `authentication` | 401 | dashboard call without a session |
| [`insufficient_scope`](#insufficient_scope) | `permission` | 403 | the key's scope is not enough |
| [`ip_not_allowed`](#ip_not_allowed) | `permission` | 403 | IP outside the key's list |
| [`sandbox_recipient`](#sandbox_recipient) | `permission` | 403 | level 0 account sending outside the account |
| [`domain_limit_reached`](#domain_limit_reached) | `permission` | 403 | the account already has the plan's domains |
| [`forbidden_origin`](#forbidden_origin) | `permission` | 403 | dashboard write from another site |
| [`resource_not_found`](#resource_not_found) | `not_found` | 404 | unknown ID |
| [`route_not_found`](#route_not_found) | `not_found` | 404 | unknown route |
| [`idempotency_conflict`](#idempotency_conflict) | `conflict` | 409 | same idempotency key, different body |
| [`idempotency_in_progress`](#idempotency_in_progress) | `conflict` | 409 | the same request is still running |
| [`domain_exists`](#domain_exists) | `conflict` | 409 | domain already registered |
| [`already_suppressed`](#already_suppressed) | `conflict` | 409 | address already on the list |
| [`last_project`](#last_project) | `conflict` | 409 | trying to delete the only project |
| [`rate_limited`](#rate_limited) | `rate_limited` | 429 | too many calls per second |
| [`daily_limit_reached`](#daily_limit_reached) | `tier_limit` | 429 | daily limit reached |
| [`monthly_limit_reached`](#monthly_limit_reached) | `tier_limit` | 429 | monthly quota reached |
| [`spend_limit_reached`](#spend_limit_reached) | `tier_limit` | 429 | overage reached your spending limit |
| [`sending_paused`](#sending_paused) | `tier_limit` | 429 | sending paused, with the reason |
| [`internal_error`](#internal_error) | `api_error` | 500 | an error on our side |
| [`billing_unavailable`](#billing_unavailable) | `api_error` | 503 | billing unavailable right now |

### missing_field

A required field is missing, named in `param`. To send, `from`, `to`, `subject` and one of `html` or `text` are required (with neither, `param` is `html`).

### invalid_field

The field named in `param` has a value we do not accept: a malformed address, more than 50 recipients across `to`, `cc` and `bcc`, a `scheduled_at` in the past or more than 30 days ahead, a webhook URL without `https://`. The `message` says what is wrong. In a batch, `param` includes the index: `emails[2].to[0]`.

### invalid_json

The body is not valid JSON. Check `Content-Type: application/json` and your quotes.

### batch_too_large

The batch has more than 100 emails. Split it into smaller calls.

### invalid_cloudflare_token

Cloudflare did not accept the token. It needs the **Zone > DNS > Edit** permission for the domain. See [Domains and DNS](https://couryo.com/en/docs/domains.md#connect-cloudflare).

### cloudflare_zone_not_found

The token is valid but does not give access to the domain's zone on Cloudflare. Create the token including that zone.

### domain_not_verified

The `from` domain is not verified in the key's project. Verify it in [Domains and DNS](https://couryo.com/en/docs/domains.md) and try again. `ck_test_` keys do not need a verified domain.

### recipient_suppressed

**Every** recipient is on the [suppression list](https://couryo.com/en/docs/suppressions.md), because of a hard bounce, a complaint, an unsubscribe or a manual entry. `param` points to the first one (`to[n]`). If only some recipients are suppressed, the email goes to the others and the timeline gets a `suppressed` event for each skipped address.

### missing_api_key

The `Authorization: Bearer ck_...` header is missing. See [Authentication](https://couryo.com/en/docs/authentication.md).

### invalid_api_key

The key does not exist, was deleted or was copied only in part. Create a new one in the dashboard if needed.

### not_authenticated

A dashboard call (`/api`) without a session. Sign in again at [app.couryo.com](https://app.couryo.com). It never happens on the public API (`/v1`).

### insufficient_scope

The key is not allowed to do this. For example, a `send` key trying to read emails, which needs `read`. The `message` says which scope is missing.

### ip_not_allowed

The key has an allowed IP list and the call came from another address. Adjust the list in the dashboard.

### sandbox_recipient

The account is on level 0 (sandbox), or the sender is `teste@sandbox.couryo.com`, and that path only sends to the account's own addresses. Verify a domain to move to level 1. See [Limits and levels](https://couryo.com/en/docs/limits.md).

### domain_limit_reached

The account already has the number of domains its plan allows: Free 1, Pro 10, Scale 50, Enterprise unlimited (across all projects). Delete a domain you do not use or change plans. The `message` gives the current limit and the next plan's.

### forbidden_origin

A dashboard write came from another site (CSRF protection). It never happens on the public API.

### resource_not_found

The ID does not exist in this project, or belongs to another project. The `message` says which resource was not found.

### route_not_found

The path does not exist. Check the method and the URL, for example `POST /v1/emails`.

### idempotency_conflict

The same `Idempotency-Key` was used in the last 24 hours with a different body. Use a new key for a new request. See [Idempotency](https://couryo.com/en/docs/sending.md#idempotency).

### idempotency_in_progress

A request with the same `Idempotency-Key` is still being processed. Wait for `Retry-After` (1 second) and repeat: you get the first request's response.

### domain_exists

This domain is already registered on Couryo, in this or another account. If it is yours and lives in another account, delete it there first.

### already_suppressed

The address is already on the suppression list for that stream.

### last_project

The account needs at least one project. Create another one before deleting this one.

### rate_limited

Too many calls in a short time (the default is 10 per second per key). Wait the seconds in the `Retry-After` header and retry with the same `Idempotency-Key`. The `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset` headers show your headroom.

### daily_limit_reached

You reached your trust level's daily limit, or the Free plan's (100 per day). The `message` gives the limit, how much was sent and what is missing to move up a level. The counter resets at midnight in your account's time zone.

### monthly_limit_reached

You reached the monthly quota: 3,000 on the Free plan and also on level 1 of any plan. Sending resumes next month (on a paid plan, next period). On level 1, the quota goes up when the account reaches level 2; on Free, when you subscribe to Pro or Scale.

### spend_limit_reached

This month's overage reached the spending limit you set under **Billing**. **All** sending stops (transactional and marketing) until you raise the limit or the period renews. Nothing is charged above the cap.

### sending_paused

Sending is paused. On a regular pause, marketing stops and transactional email keeps going with up to 30 per day; on a full pause, everything stops. The `message` brings the reason, the numbers and how to fix it, and the dashboard has a **Request review** button. See [Limits and levels](https://couryo.com/en/docs/limits.md#pauses).

### internal_error

A problem on our side. The request was **not** processed. Retry with the same `Idempotency-Key`: there is no risk of a duplicate.

### billing_unavailable

Billing is unavailable right now (checkout or payment portal). Try again in a few minutes; sending email is not affected.
