# Errors

When a request fails, the EvoHub API answers with an HTTP status code and a JSON body that says what went wrong in a form your code can act on. This page explains the format and the codes you are most likely to meet.

## The error format

Every error body has an `error` object:

```json
{
  "error": {
    "code": "FORBIDDEN",
    "message": "insufficient permissions",
    "request_id": "req_4f1c2a9e7b3d5a6c8e0f1a2b"
  }
}
```

| Field | Meaning |
| --- | --- |
| `code` | A stable, machine-readable code in capitals. Branch on this. |
| `message` | A human-readable explanation. Show or log it, but do not parse it; the wording can change. |
| `request_id` | The ID of this request. Present on most errors; the same value is always in the `X-Request-ID` response header. |

Validation errors can add a `details` array that names each field that failed:

```json
{
  "error": {
    "code": "VALIDATION_FAILED",
    "message": "request validation failed",
    "details": [
      { "field": "email", "message": "email is required" }
    ]
  }
}
```

## Status codes

| Status | Meaning | What to do |
| --- | --- | --- |
| **400 Bad Request** | The request is malformed or a field is invalid. | Fix the request. Read `message` and `details`. |
| **401 Unauthorized** | No credentials, or the credentials are not valid: the API key is missing, unknown, revoked or expired. | Check the key and the header. Retrying the same request will not help. |
| **403 Forbidden** | You are authenticated, but not allowed to do this. | Give the key the scope it needs, or use a person's account where a person is required. |
| **404 Not Found** | The resource does not exist, or you are not allowed to know it exists. | Check the ID and which organization or team the key belongs to. |
| **409 Conflict** | The request conflicts with the current state, for example a name already taken or a limit reached. | Change the request; it will not succeed as is. |
| **429 Too Many Requests** | You went over the rate limit. | Wait for the `Retry-After` seconds, then retry with backoff. |
| **500 Internal Server Error** | Something went wrong on EvoHub's side. | Retry later. If it persists, contact support with the request ID. |
| **503 Service Unavailable** | EvoHub could not complete the request right now. | Retry shortly with backoff. |

## 401 versus 403

EvoHub keeps these two strictly apart:

- **401** always means "we do not know who you are". The credential is missing or not valid.
- **403** always means "we know who you are, and you may not do this". A missing scope, an action reserved for administrators, or an action that only a person may take all return 403, never 401.

So a 401 is a credential problem, and a 403 is a permission problem. Rotating a key will not fix a 403; changing its scopes or role will.

> [!NOTE]
> A resource that belongs to a team the key cannot see answers **404**, not 403. Saying "forbidden" would confirm that something with that ID exists.

## Common error codes

| Code | Status | Meaning |
| --- | --- | --- |
| `UNAUTHORIZED` | 401 | No valid credential was sent, or the API key is invalid or expired. The message says which header to send. |
| `FORBIDDEN` | 403 | The key or person lacks the permission this endpoint needs. Also returned when you try to give a key or role a permission you do not hold. |
| `ADMIN_ONLY` | 403 | Only an administrator can do this, for example giving a key the Admin role. |
| `PERSON_REQUIRED` | 403 | This action (such as approving a change request) must be done by a person signed in to EvoHub, not with an API key. |
| `API_KEY_NOT_ALLOWED` | 403 | This endpoint is for a signed-in person acting on their own account, such as managing sessions or two-factor authentication. |
| `NOT_A_PERSON` | 403 | An organization key tried to do something that must name a person, such as sending an invitation. Use a personal key. |
| `VALIDATION_ERROR` | 400 | A field is missing or invalid. The message names it. |
| `VALIDATION_FAILED` | 400 | One or more fields are invalid. Each is listed in `details`. |
| `INVALID_BODY` | 400 | The request body is not valid JSON. |
| `UNKNOWN_PERMISSION` | 400 | A scope or permission name is not one EvoHub knows. |
| `RATE_LIMITED` | 429 | Too many requests. See [Rate limits](https://docs-dev.evohub.io/api-overview.md#rate-limits). |
| `AUTH_UNAVAILABLE` | 503 | The key could not be checked right now. Your key is fine; retry shortly. |
| `INTERNAL_ERROR` | 500 | An unexpected error on EvoHub's side. |

Not-found errors often name the resource, for example `MONITOR_NOT_FOUND`. Treat any 404 the same way, whatever its code.

## Request IDs

Every response, successful or not, carries an `X-Request-ID` header, and most error bodies repeat it as `request_id`. Log it with every failed call. When you contact support at info@evosync.io, include the request ID and the time of the request; it lets us find that exact call.

You can also set your own `X-Request-ID` header on a request, for example your CI job's run ID, and EvoHub uses it instead of generating one.

## Handling errors well

- Branch on `code` and the status, not on `message`.
- Retry only **429**, **500** and **503**, with exponential backoff. Do not retry **400**, **401**, **403**, **404** or **409** unchanged.
- On **401** from a key that used to work, check whether it was revoked or has expired in **My Account → API Keys** or **Organization → Organization Keys**.

## Related

- [API overview](https://docs-dev.evohub.io/api-overview.md)
- [API keys and scopes](https://docs-dev.evohub.io/api-keys-and-scopes.md)
- [Roles and permissions](https://docs-dev.evohub.io/roles-and-permissions.md)
