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

# Errors & Idempotency

> Three error envelopes coexist today. Status code reference, retry rules, and current idempotency state with workaround.

Bavlio is honest about its current state. Three error envelope shapes coexist today (a unification migration is parked as a follow-up). Idempotency-Key middleware is not yet implemented. Here's how to deal with both.

## Error envelope shapes

Match on HTTP status code first; treat the body as supplementary detail. Newer endpoints conform to ADR-010 (flat shape with stable code); older endpoints still emit FastAPI defaults; validation failures emit a Pydantic-shaped envelope.

### EpicHTTPError (flat)

Used by newer endpoints. The `detail` field is a stable machine-readable code; `message` is a human-readable explanation.

```json Example theme={null}
{
  "detail": "VALIDATION_ERROR",
  "message": "Email is required"
}
```

Seen on: `/api/v1/personalize`, `/api/v1/personalize/preview`, `/api/v1/email-finder/*` (most routes).

### Legacy HTTPException

Older FastAPI default. The `detail` field is a human-readable string; no separate code field. Treat `detail` as opaque text — match on status code and endpoint, not on `detail` content.

```json Example theme={null}
{
  "detail": "Email is required"
}
```

Seen on: older endpoints that have not yet migrated to EpicHTTPError.

### Pydantic request validation

Returned automatically when request bodies fail Pydantic validation. The `details` array contains structured per-field errors; `path` is the request path; `message` is constant.

```json Example theme={null}
{
  "error": "Request Validation Error",
  "details": [
    {
      "loc": ["body", "email"],
      "msg": "field required",
      "type": "value_error.missing"
    }
  ],
  "path": "/api/v1/email-finder/search",
  "message": "The request failed validation"
}
```

Seen on: any endpoint with a Pydantic request body.

## Status codes & retry rules

| Status | Meaning                                 | Retry?                    |
| ------ | --------------------------------------- | ------------------------- |
| 200    | OK                                      | n/a                       |
| 201    | Created                                 | n/a                       |
| 202    | Accepted (async work queued)            | n/a                       |
| 400    | Bad request — fix the body / params     | no                        |
| 401    | Authentication failed                   | no (re-auth)              |
| 403    | Authenticated but not allowed           | no                        |
| 404    | Not found                               | no                        |
| 409    | Conflict (duplicate, state change race) | maybe (after read)        |
| 422    | Validation failed                       | no                        |
| 429    | Rate limited                            | yes (after `Retry-After`) |
| 500    | Server error                            | yes (exponential backoff) |
| 502    | Bad gateway                             | yes                       |
| 503    | Service unavailable                     | yes                       |
| 504    | Gateway timeout                         | yes                       |

A safe default agent: retry 429 honoring `Retry-After`, retry 5xx with exponential backoff (max 5 attempts), surface everything else immediately.

## Retry pattern

Drop-in helper that respects `Retry-After` and backs off exponentially.

```python Python theme={null}
import time
import httpx

def call_with_backoff(client, method, path, *, max_retries=5, **kwargs):
    """Retry on 429 and 5xx with exponential backoff. Stop on 4xx others."""
    for attempt in range(max_retries):
        response = client.request(method, path, **kwargs)
        if response.status_code < 400:
            return response
        if response.status_code == 429:
            retry_after = int(response.headers.get("Retry-After", "1"))
            time.sleep(retry_after)
            continue
        if 500 <= response.status_code < 600:
            time.sleep(2 ** attempt)
            continue
        response.raise_for_status()
    response.raise_for_status()
```

## Idempotency

<Warning>
  The `Idempotency-Key` header is **not yet honored** by Bavlio. Sending it has no effect server-side. A retried POST after a network failure will create a duplicate.
</Warning>

**Workaround until the middleware ships:** after a failed mutation, follow up with a read before retrying. For example, after a failed `POST /api/v1/campaigns/`, call `GET /api/v1/campaigns/?name=<your-name>` to check whether your campaign was already created. Match on a stable client-side correlation field (campaign name, lead-set hash, etc.).

**Roadmap:** a FastAPI middleware that accepts `Idempotency-Key` on POST/PUT/PATCH/DELETE, hashes the request, stores the response in Redis for 24 hours, and returns the cached response on retry.

## FAQ

<AccordionGroup>
  <Accordion title="How do I tell if I should retry?">
    Retry on 429 (after waiting `Retry-After` seconds) and on 5xx server errors with exponential backoff. Do NOT retry on 4xx (other than 429) — fix the request and try once. Check `error.detail` or `error.message` for the specific cause.
  </Accordion>

  <Accordion title="What is the error envelope shape?">
    Three envelopes coexist today. Newer endpoints use a flat shape: `{ detail: <code>, message: <text> }` per ADR-010 (EpicHTTPError). Older endpoints use the FastAPI default: `{ detail: <text> }`. Pydantic validation failures return `{ error, details, path, message }`. Match on status code first.
  </Accordion>

  <Accordion title="What is the difference between 401 and 403?">
    401 means the API key is missing, malformed, or revoked. Re-authenticate. 403 means the key is valid but lacks permission for the operation — for example, attempting to mint a new API key with an API key (only Supabase JWTs can mint keys).
  </Accordion>
</AccordionGroup>
