LizzyDocs
Docs navigation Errors and recovery

Reference · Diagnostics

Handle API errors and retries

Distinguish Lizzy errors from upstream responses, retain diagnostic IDs, and decide when to wait, retry, change the request, or stop.

First identify which layer answered

A request through Lizzy can fail in three places. The response shape tells you which recovery policy owns the next step. Do this classification before reading a message or retrying the request.

  1. 1
    Lizzy API

    Authentication, validation, permissions, balance, resource state, and Lizzy service failures use the stable error envelope.

  2. 2
    Teacher model

    BYOK keeps the provider status and body. Managed sources keep the model status and return teacher_unavailable.

  3. 3
    Edge gateway

    If the backend cannot be reached, the Worker returns a minimal backend_unavailable 503 response.

Lizzy-owned

Stable machine code

Look for error.type, error.code, and error.request_id.

Follow this guide
Proxy model call

Source-kind contract

BYOK may return a provider body. Managed failures use the teacher_unavailable envelope.

Keep X-Lizzy-Call-Id
Edge-owned

Minimal 503

The Worker’s fallback body has a code and message but currently no request ID.

Treat write outcome as unknown
jsonedge fallback response
{
  "error": {
    "type": "api_error",
    "code": "backend_unavailable",
    "message": "The service is temporarily unavailable. Please retry."
  }
}

Branch on error.code, not message text

Lizzy-owned failures use one JSON envelope. type is a broad category;code is the stable programmatic decision; message is for a person; and param points to the invalid field when one exists.

jsonexample Lizzy error
{
  "error": {
    "type": "conflict_error",
    "code": "idempotency_key_in_flight",
    "message": "A request with this Idempotency-Key is still being processed.",
    "param": null,
    "request_id": "req_7ff3257080d347da96e2"
  }
}
cURLexample
# This read-only request intentionally returns a Lizzy 404 envelope.
curl --silent --show-error --max-time 10 \
  "https://lizzy.albinilabs.com/v1/not-a-real-route" \
  | jq '.error | {type, code, message, param, request_id}'

Keep the ID owned by the layer you are debugging

X-Request-IdReturned on every Lizzy control-plane response. On an error it matches error.request_id.
error.request_idUse for a failed Lizzy API request and include it in a support report.
X-Lizzy-Call-IdReturned on proxy responses; joins the request to captured-call metadata.
X-Lizzy-External-IdYour request ID, supplied to the proxy so later feedback and investigation can join on it.
X-Lizzy-Served-ByExplains whether upstream, student, shadow, or fallback answered a proxy call.
Lizzy-Event-IdIdentifies the immutable event carried by a managed webhook.
Lizzy-Delivery-IdIdentifies one endpoint delivery and its attempt history.
bashredacted support capture
headers_file="$(mktemp)"
trap 'rm -f "$headers_file"' EXIT


response=$(curl --silent --show-error --max-time 15 \
  --dump-header "$headers_file" \
  -H "Authorization: Bearer $LIZZY_API_TOKEN" \
  "https://lizzy.albinilabs.com/v1/events?limit=1")


# Print identifiers and codes, not the token or an entire sensitive payload.
awk 'BEGIN { IGNORECASE=1 } /^X-Request-Id:/ { gsub("\r", "", $2); print "request_id=" $2 }' \
  "$headers_file"
printf '%s' "$response" | jq '{
  error_code: .error.code,
  error_request_id: .error.request_id,
  first_resource_id: .data[0].id
}'

Retry only after the outcome is classified

  1. 1

    Did you receive an HTTP response?

    If not, the outcome of a write is unknown. Retry a GET. Retry a creating POST only with its original idempotency key and byte-identical body; otherwise inspect the relevant list or existing resource before doing anything new.

  2. 2

    Was it a Lizzy envelope?

    Use error.code. Fix 4xx preconditions instead of looping. A managedteacher_unavailable keeps the model status; apply normal backoff to a 429 or switch the source model. For other 429 errors, wait forretry_after_seconds. For a 5xx, retry reads and idempotent writes with exponential backoff, jitter, and a deadline.

  3. 3

    Was it a BYOK provider response?

    Follow that provider’s status, body, and retry guidance. PreserveX-Lizzy-Call-Id and your X-Lizzy-External-Id; do not try to parse a provider body as a Lizzy envelope.

  4. 4

    Did an async watch simply end?

    A client timeout or exhausted polling budget is not a terminal resource state. Keep the returned ID, pause, and resume GET polling later. Never create replacement work solely because a watch window ended.

pythonidempotent retry with a deadline
import json
import os
import random
import time
import uuid


import requests


url = "https://lizzy.albinilabs.com/v1/distill/loops"
body = b'{"name":"retry-example","description":"One retained byte string"}'
key = str(uuid.uuid4())  # Persist this beside body before the first attempt.
headers = {
    "Authorization": f"Bearer {os.environ['LIZZY_API_TOKEN']}",
    "Content-Type": "application/json",
    "Idempotency-Key": key,
}


for attempt in range(4):
    try:
        response = requests.post(url, headers=headers, data=body, timeout=20)
    except (requests.Timeout, requests.ConnectionError):
        if attempt == 3:
            raise
        time.sleep(min(2 ** attempt, 8) + random.random())
        continue


    parsed = response.json()
    code = parsed.get("error", {}).get("code")
    if response.status_code == 429:
        delay = parsed["error"].get("retry_after_seconds", min(2 ** attempt, 8))
        time.sleep(float(delay) + random.random())
        continue
    if response.status_code >= 500 or code == "idempotency_key_in_flight":
        if attempt == 3:
            response.raise_for_status()
        time.sleep(min(2 ** attempt, 8) + random.random())
        continue


    response.raise_for_status()
    print(json.dumps(parsed, indent=2))
    break
else:
    raise RuntimeError("Retry budget ended; retain the key and inspect before resuming")

Status narrows the problem; code chooses the recovery

400 invalid_request_errorFix the named parameter or request shape. Do not retry unchanged.
400 teacher_model_unknownRead the current model catalog and choose an exact teacher ID before creating or updating the managed source.
401 authentication_errorSupply, rotate, enable, or replace the API token. Never log the token while diagnosing it.
402 quotaResolve balance, spend, or feature access. Test-mode work is not ledger-gated.
403 permission_errorCheck token scopes, dashboard role, feature gate, mode, and human-only boundaries.
404 not_found_errorCheck the ID, workspace, and credential mode. Cross-mode resources intentionally look absent.
409 conflict_errorRe-read state. Wait and reuse the same key for in-flight idempotency; stop on key reuse with different bytes.
410 goneThe content or handoff existed but has expired or been removed. Obtain a new supported reference.
429 rate_limit_errorWait for body retry_after_seconds, add jitter, and keep the same ID or idempotency key.
5xx api_errorRetry reads; for writes, preserve exact bytes and the original key, then stop at the retry deadline.
503 catalog_unavailableNo model catalog snapshot is readable. Retry the catalog read later before changing a managed source.
original status · teacher_unavailableThe managed model failed. A 429 stays 429; apply normal backoff, or switch the source to another catalog model. Failed calls are captured and not billed.

Resume from the resource ID

Dataset versions, uploads, connector pulls, runs, and deployments can outlive the HTTP request that created or discovered them. Persist every returned resource ID before entering a watch loop. Poll its GET endpoint until a documented terminal state, then stop.

Reports are different: a successful run creates its report already inready state. Read the report ID from the completed run and fetch it directly; there is no separate report operation to poll.

typescriptrun watch with a deadline
const token = process.env.LIZZY_API_TOKEN;
if (!token) throw new Error("LIZZY_API_TOKEN is required");


const terminal = new Set(["succeeded", "failed", "canceled"]);
const runId = process.env.LIZZY_RUN_ID;
if (!runId) throw new Error("LIZZY_RUN_ID is required");


for (let attempt = 0; attempt < 20; attempt += 1) {
  const response = await fetch("https://lizzy.albinilabs.com/v1/distill/runs/" + encodeURIComponent(runId), {
    headers: { Authorization: `Bearer ${token}` },
    signal: AbortSignal.timeout(15_000),
  });
  if (!response.ok) throw await response.json();
  const run = await response.json();
  if (terminal.has(run.status)) {
    console.log(run);
    process.exit(run.status === "succeeded" ? 0 : 1);
  }
  await new Promise((resolve) => setTimeout(resolve, Math.min(1_000 * 2 ** attempt, 10_000)));
}


throw new Error("Watch window ended. Keep LIZZY_RUN_ID and resume this GET later.");