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

# Errors

> Stripe-style error envelope and HTTP status code mapping.

Every error response from the API uses the same JSON shape. Whether
it came from Next.js (auth, rate limiting) or the Go backend
(validation, ownership, data fetching), the envelope is identical.

```json theme={null}
{
  "error": {
    "type": "<machine-readable category>",
    "message": "<human-readable explanation>"
  }
}
```

## Status codes you'll actually see

| HTTP  | `error.type`            | When                                                                                                                  |
| ----- | ----------------------- | --------------------------------------------------------------------------------------------------------------------- |
| `400` | `invalid_request_error` | Malformed input — bad UUID, bad date, unknown metric name, wrong HTTP method.                                         |
| `401` | `authentication_error`  | Missing / malformed / unknown / revoked Bearer key.                                                                   |
| `403` | `permission_error`      | Key is valid but the caller doesn't own the requested universe.                                                       |
| `404` | `not_found`             | Universe doesn't exist, or no history row for the given filters.                                                      |
| `429` | `rate_limit_error`      | Per-key request budget exceeded. See [Rate limits](/rate-limits).                                                     |
| `500` | `api_error`             | Server bug or upstream DB failure. Try again with backoff; if it persists, [reach out](mailto:support@verseodin.com). |
| `503` | `api_error`             | Backend temporarily unreachable. Retry with backoff.                                                                  |

## Common cases

### Forgot the header

```http theme={null}
HTTP/1.1 401 Unauthorized
{
  "error": {
    "type": "authentication_error",
    "message": "Missing or malformed Authorization header. Expected: 'Authorization: Bearer <api_key>'."
  }
}
```

### Bad UUID in the path

```http theme={null}
HTTP/1.1 400 Bad Request
{
  "error": {
    "type": "invalid_request_error",
    "message": "universe_id is not a valid UUID"
  }
}
```

### Wrong owner

```http theme={null}
HTTP/1.1 403 Forbidden
{
  "error": {
    "type": "permission_error",
    "message": "access denied: this universe does not belong to your account"
  }
}
```

### No data for that filter combo

```http theme={null}
HTTP/1.1 404 Not Found
{
  "error": {
    "type": "not_found",
    "message": "no data found for this metric/universe/date combination"
  }
}
```

### Bad date

```http theme={null}
HTTP/1.1 400 Bad Request
{
  "error": {
    "type": "invalid_request_error",
    "message": "invalid filter value (check `day` format YYYY-MM-DD)"
  }
}
```

## Idiomatic client handling

```python theme={null}
import time, requests

def get(path, key):
    r = requests.get(f"https://verseodin.com/api/v1{path}",
                     headers={"Authorization": f"Bearer {key}"})

    if 200 <= r.status_code < 300:
        return r.json()

    body = r.json().get("error", {})
    err_type, msg = body.get("type"), body.get("message")

    if err_type == "rate_limit_error":
        time.sleep(int(r.headers.get("Retry-After", "5")) + 1)
        return get(path, key)
    if err_type == "authentication_error":
        raise PermissionError(f"check your VERSEODIN_API_KEY: {msg}")
    if err_type in ("permission_error", "not_found"):
        return None  # caller can decide
    if err_type == "invalid_request_error":
        raise ValueError(msg)
    raise RuntimeError(f"verseodin api {r.status_code} {err_type}: {msg}")
```

## What we don't do (and why)

* **No RFC 7807 `application/problem+json`** — the format hasn't reached
  meaningful adoption among public APIs developers integrate with daily.
  We follow Stripe's pattern instead because that's what the ecosystem
  is already wired to consume.
* **No nested error trees / multiple errors per response** — every
  failed response carries exactly one `error` object. If you submitted
  multiple invalid filters, you'll see the first one we noticed.
* **No localised messages** — `message` strings are English-only.
  Don't show them to end users verbatim; show your own copy and
  log ours.
