---
title: "Errors"
description: "The error envelope and every error code, with what to do about each."
url: https://developer.tendralhealth.com/docs/errors
---

# Errors

Authenticated endpoints answer every failure, including a failure on our side, with one envelope. Branch your code on `error.type`; use `error.code` for the exact reason.

## The envelope

```json Error response
{
  "error": {
    "type": "invalid_request_error",
    "code": "MISSING_PARAMETER",
    "message": "`since` query parameter is required.",
    "details": { "parameter": "since" },
    "doc_url": "https://developer.tendralhealth.com/docs/errors#MISSING_PARAMETER",
    "request_id": "req_018f5a2e7c127c8da123cafed00dfeed"
  }
}
```

- `message` is for people and your logs. Do not parse it; it can change.
- `details` is present when there is more to say, such as the parameter at fault.
- `doc_url` links to this page, to the entry for that code.
- `request_id` matches the `X-Request-Id` header. Quote it when you write to us.

## Types

| Type | HTTP | Retry? |
| --- | --- | --- |
| `authentication_error` | 401 | No |
| `permission_error` | 403 | No |
| `invalid_request_error` | 400, 404 | No |
| `idempotency_error` | 409 | No |
| `rate_limit_error` | 429 | Yes, after Retry-After |
| `api_error` | 500 | Yes, with backoff |

New codes can be added to an existing type. Code that branches on `type` keeps working when they are.

## Codes

### UNAUTHORIZED

The bearer key is missing, malformed, expired or revoked.

| HTTP | Type | Retry? |
| --- | --- | --- |
| 401 | `authentication_error` | No |

**What to do:** Send `Authorization: Bearer <key>` with a current key. If the key was rotated or revoked, use its replacement.

### INSUFFICIENT_SCOPE

The key is valid but does not carry the scope this endpoint needs.

| HTTP | Type | Retry? |
| --- | --- | --- |
| 403 | `permission_error` | No |

**What to do:** `error.details.required_scope` names the scope. Ask for a key that carries it.

### INVALID_TOKEN

A path token is not in the expected format.

| HTTP | Type | Retry? |
| --- | --- | --- |
| 400 | `invalid_request_error` | No |

**What to do:** Check the token against the pattern the reference gives for that path parameter.

### INVALID_CAMPAIGN_ID

A campaign id is not in the prefixed `cmp_<hex>` form.

| HTTP | Type | Retry? |
| --- | --- | --- |
| 400 | `invalid_request_error` | No |

**What to do:** Pass the prefixed id exactly as the API returned it.

### TOKEN_NOT_FOUND

The token is well formed, but no record exists for it.

| HTTP | Type | Retry? |
| --- | --- | --- |
| 404 | `invalid_request_error` | No |

**What to do:** Check that the record has been published to you. A record that was withdrawn is not found.

### MISSING_PARAMETER

A required query parameter is missing.

| HTTP | Type | Retry? |
| --- | --- | --- |
| 400 | `invalid_request_error` | No |

**What to do:** `error.details.parameter` names it. Add it and send the request again.

### INVALID_PARAMETER

A parameter is present but has the wrong type, format or value.

| HTTP | Type | Retry? |
| --- | --- | --- |
| 400 | `invalid_request_error` | No |

**What to do:** `error.details.parameter` names the parameter. Common causes: a timestamp or date that is not ISO 8601, a `limit` outside 1–500, a `cursor` that is not one the API returned.

### INVALID_EXPAND

A value in `expand[]` is not allowed on this endpoint.

| HTTP | Type | Retry? |
| --- | --- | --- |
| 400 | `invalid_request_error` | No |

**What to do:** `error.details.allowed` lists the values this endpoint accepts.

### INVALID_TYPE_FILTER

A value in `type` or `types[]` is not one of the event types this endpoint lists.

| HTTP | Type | Retry? |
| --- | --- | --- |
| 400 | `invalid_request_error` | No |

**What to do:** `error.details.allowed` lists the accepted types.

### IDEMPOTENCY_KEY_REUSED

An `Idempotency-Key` was sent again with a different request body.

| HTTP | Type | Retry? |
| --- | --- | --- |
| 409 | `idempotency_error` | No |

**What to do:** Use a new key for a new operation. Reuse a key only to retry the identical request.

### RATE_LIMITED

The key has used its requests for the current window.

| HTTP | Type | Retry? |
| --- | --- | --- |
| 429 | `rate_limit_error` | Yes, after Retry-After |

**What to do:** Wait the number of seconds in `Retry-After`, then retry. Do not retry sooner.

### INTERNAL_ERROR

Something failed on our side. Nothing about your request was wrong.

| HTTP | Type | Retry? |
| --- | --- | --- |
| 500 | `api_error` | Yes, with backoff |

**What to do:** Retry with exponential backoff. If it keeps failing, write to us with the `request_id`.
