> ## Documentation Index
> Fetch the complete documentation index at: https://engineering.unkey.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Error handling

> Understanding and working with API errors

Error responses use the same top-level envelope but return an `error` object instead of `data`:

```json theme={"theme":"kanagawa-wave"}
{
  "meta": {
    "requestId": "req_abc123xyz789"
  },
  "error": {
    "title": "Validation Error",
    "detail": "You must provide a valid API ID.",
    "status": 400,
    "type": "https://unkey.com/docs/errors/validation-error",
    "errors": [
      {
        "location": "body.apiId",
        "message": "API not found",
        "fix": "Provide a valid API ID or create a new API"
      }
    ]
  }
}
```

## Error format

All API errors use `application/json` as the response media type. The error
object is inspired by RFC7807 Problem Details, but Unkey doesn't return a
top-level `application/problem+json` document. The Problem Details-style fields
live inside the Unkey response envelope:

* title: short summary
* detail: human-readable explanation
* status: HTTP status code
* type: URI for documentation
* errors: optional validation details

Don't document new v2 errors as `application/problem+json`. Use the same
`application/json` media type as successful responses so clients, SDKs, and
agents can parse every response with the same content-type rule.

## Common error types

| Status | Error type            | Description                                                                                         |
| ------ | --------------------- | --------------------------------------------------------------------------------------------------- |
| 400    | validation-error      | Request body failed validation                                                                      |
| 401    | unauthorized          | Missing or invalid authorization                                                                    |
| 403    | forbidden             | Valid authorization but insufficient permissions, only when the caller may know the resource exists |
| 404    | not-found             | Resource not found, or the caller may not read it                                                   |
| 409    | conflict              | Conflicts with current state                                                                        |
| 429    | rate-limited          | Rate limit exceeded                                                                                 |
| 500    | internal-server-error | Unexpected server error                                                                             |

## Authorization failures must not reveal resource existence

A 403 and a 404 carry different information. If an endpoint answers 404 when a
resource does not exist and 403 when it exists but the caller may not read it,
a caller with no permissions can enumerate which resources exist by telling the
two responses apart.

The rule: an endpoint returns 403 only when the principal is allowed to know
the resource exists, meaning it holds a permission that covers reading the
resource, but lacks the permission the operation requires. When the principal
may not read the resource, every operation on it returns the same 404 the
missing resource produces, with an identical error type, title, and detail.
This matches GitHub's documented behavior of returning 404 instead of 403 for
private resources.

For read endpoints this collapses to: a permission rejection returns the
resource's not-found error. Two consequences for handler code:

* The not-found branch needs no permission check at all, since unauthorized
  callers receive the identical response either way.
* The masked 404 must be constructed fresh, not by wrapping the authorization
  error. The error middleware joins every public message in a fault chain into
  the response detail, and authorization rejections name the missing
  permissions, including concrete resource IDs, which would leak the existence
  the 404 is masking.

The reference implementation is `svc/api/routes/v2_ratelimit_get_override`,
including a test that probes an existing and a missing resource with a
zero-permission key and asserts the responses are indistinguishable.

## Status codes and domain outcomes

HTTP status codes describe whether Unkey could process the HTTP request. They
don't describe normal product decisions.

Use `200` when Unkey successfully processes a request, even if the domain result
is negative. For example, key verification can return `200` with `data.valid:
false`, and rate limiting can return `200` with `data.success: false`.

Use `4xx` when the request can't be processed as submitted, such as invalid JSON,
failed validation, missing authentication, insufficient permissions, or a missing
resource. Use `5xx` when Unkey fails to process a valid request.

Don't use HTTP status codes as business logic for expected decisions. Model the
decision in the response data instead.

## Validation errors

For validation errors, Unkey returns:

* location: where the error occurred
* message: what went wrong
* fix: suggestion for resolution when available

## Error recovery

Error messages are designed to be actionable. Use the `requestId` when reporting issues.

## Error handling best practices

1. Check status codes first.
2. Parse the error object for details.
3. Retry only on 5xx errors when appropriate.
4. Log the full error response for debugging.

Example handling in JavaScript:

```javascript theme={"theme":"kanagawa-wave"}
const response = await fetch("https://api.unkey.com/v2/keys.create", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${rootKey}`,
    "Content-Type": "application/json"
  },
  body: JSON.stringify(keyData)
});

const data = await response.json();

if (!response.ok) {
  const { meta, error } = data;
  console.error(`Error ${error.status}: ${error.title}`, {
    requestId: meta.requestId,
    detail: error.detail,
    docs: error.type
  });

  if (error.errors) {
    error.errors.forEach(err => {
      console.error(`- ${err.location}: ${err.message}`);
    });
  }

  throw new Error(`API Error: ${error.detail}`);
}
```
