# Errors

> Status codes and error shapes returned by the API.

Inboundr uses conventional HTTP status codes and always returns an `error` field
on failures.

## Status codes

| Code | Meaning |
| --- | --- |
| `200 OK` | Request succeeded. |
| `201 Created` | Resource created (create domain/address/endpoint). |
| `204 No Content` | Succeeded with no body (delete). |
| `400 Bad Request` | Validation failed — check the `error` field. |
| `401 Unauthorized` | Missing/invalid [API key](/api-reference/authentication). |
| `403 Forbidden` | [Read-only key](/api-reference/authentication#scopes) attempting a write. |
| `402 Payment Required` | Plan allotment reached (Free plan). See [Billing](/billing). |
| `404 Not Found` | Resource doesn't exist or isn't yours. |
| `409 Conflict` | Already exists (e.g. domain already registered). |
| `429 Too Many Requests` | Over the [rate limit](/api-reference/rate-limits). |
| `503 Service Unavailable` | A dependency (e.g. attachment storage) isn't configured. |

## Error shape

Most errors return a simple message:

```json
{ "error": "Email not found" }
```

Validation errors (`400`) return a **structured tree** describing which fields
failed, so you can surface field-level messages:

```json
{
  "error": {
    "errors": [],
    "properties": {
      "localPart": { "errors": ["Invalid local part"] },
      "domainId": { "errors": ["Required"] }
    }
  }
}
```

<Tip>
  For `429`, honour the `Retry-After` header. For `402`, you've reached your
  monthly allotment: either upgrade your [plan](/billing), or turn on
  pay-as-you-go to keep going and be billed for the overage.
</Tip>
