---
title: Errors
description: The error envelope, common codes, and how to handle them.
---

Every error response from the API uses the same shape. One
handler, three channels.

## The error envelope

```json
{
  "error": {
    "code": "invalid_phone",
    "message": "Recipient must be a phone number in international format, e.g. +2348012345678.",
    "param": "to",
    "type": "validation_error",
    "request_id": "req_01HXYZ..."
  }
}
```

| Field         | Meaning                                                            |
| ------------- | ------------------------------------------------------------------ |
| `code`        | Stable, machine-readable identifier. Switch on this in your code. |
| `message`     | Human-readable explanation. Safe to show to developers.            |
| `param`       | The request field at fault, when applicable.                       |
| `type`        | High-level category: `validation_error`, `auth_error`, etc.        |
| `request_id`  | Unique id for this request. Include it when you contact support.   |

## Status codes

| Status | Type                  | Example codes                                       |
| ------ | --------------------- | --------------------------------------------------- |
| `400`  | `validation_error`    | `invalid_phone`, `invalid_email`, `body_too_long`   |
| `401`  | `auth_error`          | `missing_api_key`, `invalid_api_key`                 |
| `403`  | `permission_error`    | `key_domain_scope`, `team_suspended`                |
| `404`  | `not_found`           | `template_not_found`, `domain_not_found`            |
| `409`  | `conflict`            | `template_channel_mismatch`                         |
| `422`  | `validation_error`    | `missing_variable`, `unknown_variable`              |
| `429`  | `rate_limited`        | `rate_limited`                                      |
| `500`  | `internal_error`      | `internal_error`                                    |

## Common error codes

### `invalid_phone`

The `to` field isn't a valid phone number in E.164 format.
E.164 means international form with a leading `+` and country
code, no spaces or dashes: `+2348012345678`.

### `invalid_email`

The `to` field isn't a valid email address.

### `template_not_found`

The `template_id` you passed doesn't exist on this team, or
isn't enabled. Check that the template hasn't been archived.

### `wrong_template_channel`

You sent a template through the wrong endpoint — e.g. an SMS
template via `POST /v1/emails`. Send it through the matching
`/v1/{channel}` endpoint.

### `missing_variable` / `unknown_variable`

Your `variables` object doesn't match the template's variable
manifest. The `param` field names the offending key.

### `no_approved_brand_domain`

You're in live mode but no verified domain on the team is
approved yet. Add a verified domain and complete the brand
profile.

### `rate_limited`

You exceeded the rate limit. The response includes a
`Retry-After` header (seconds) telling you when to retry.
See [Rate limits](/api/rate-limits).

### `internal_error`

Something went wrong on our side. Include the `request_id`
when you contact support. These errors are safe to retry with
exponential backoff.

## Handling errors

A reasonable client retries `429` and `5xx` with exponential
backoff and surfaces everything else to the developer:

```ts
async function send(payload: SendInput, attempt = 0): Promise<Sent> {
  const res = await fetch('https://api.zevsend.com/v1/emails', {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${process.env.ZEVSEND_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(payload),
  });

  if (res.ok) return res.json();

  const body = await res.json();
  const retryable = res.status === 429 || res.status >= 500;
  if (retryable && attempt < 5) {
    const wait =
      res.status === 429
        ? Number(res.headers.get('Retry-After') ?? 1) * 1000
        : 2 ** attempt * 250;
    await new Promise((r) => setTimeout(r, wait));
    return send(payload, attempt + 1);
  }
  throw new Error(`${body.error.code}: ${body.error.message}`);
}
```