Skip to main content
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.
Example
{
  "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.
Example
{
  "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.
Example
{
  "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

StatusMeaningRetry?
200OKn/a
201Createdn/a
202Accepted (async work queued)n/a
400Bad request — fix the body / paramsno
401Authentication failedno (re-auth)
403Authenticated but not allowedno
404Not foundno
409Conflict (duplicate, state change race)maybe (after read)
422Validation failedno
429Rate limitedyes (after Retry-After)
500Server erroryes (exponential backoff)
502Bad gatewayyes
503Service unavailableyes
504Gateway timeoutyes
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
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

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.
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

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.
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.
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).