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

# Errors

> Predictable error responses with stable codes you can switch on.

Every `/tryon/v1/*` endpoint returns errors in the same JSON shape. Branch on `code` — it's the stable contract. Treat `message` as human-readable text that may be polished between releases.

```json Error shape theme={null}
{
  "code": "PRODUCT_NOT_FOUND",
  "message": "Product 'shirt-42' not found.",
  "status": 404
}
```

For validation failures (Zod rejections of the request body) the response also includes a `details` array with one entry per offending field:

```json Validation shape theme={null}
{
  "code": "VALIDATION_FAILED",
  "message": "Request body failed validation. See `details` for per-field errors.",
  "status": 400,
  "details": [
    { "path": "product.images.0.url", "message": "Required" },
    { "path": "validForDays",        "message": "Expected number, received string" }
  ]
}
```

<Note>
  The HTTP status in the response always matches the `status` field. You can read either — both are part of the contract.
</Note>

## Codes

Codes are grouped by what triggered them. Branching on the code is enough — the same code always means the same thing across endpoints.

### Auth

| Code              | Status | When you'll see it                                          |
| ----------------- | ------ | ----------------------------------------------------------- |
| `MISSING_API_KEY` | 401    | `x-api-key` header is missing.                              |
| `INVALID_API_KEY` | 401    | The key was rejected — wrong, revoked, or inactive account. |

### Authorization

| Code        | Status | When you'll see it                                                       |
| ----------- | ------ | ------------------------------------------------------------------------ |
| `FORBIDDEN` | 403    | Requested resource (product, generation) belongs to a different account. |

### Request validation (400)

| Code                          | When you'll see it                                                                                                                                                       |
| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `VALIDATION_FAILED`           | Zod rejected the request body. `details[]` lists `{ path, message }` for every offending field.                                                                          |
| `EXTERNAL_ID_REQUIRED`        | `POST /products` without `externalId`.                                                                                                                                   |
| `PRODUCT_IMAGES_REQUIRED`     | Creating or one-shotting a product without any image.                                                                                                                    |
| `CUSTOMER_IMAGE_REQUIRED`     | `POST /try-on` without a `customer` object.                                                                                                                              |
| `FILE_REQUIRED`               | `POST /images/upload` called with no `file` field.                                                                                                                       |
| `MULTIPART_FILE_NOT_FOUND`    | A `fileKey` referenced from `data` doesn't appear in the multipart payload.                                                                                              |
| `INVALID_IMAGE_INPUT`         | An `images[]` entry has neither `url` nor `fileKey`.                                                                                                                     |
| `INVALID_IMAGE`               | Uploaded bytes are too small to be an image, or otherwise malformed before format detection.                                                                             |
| `UNSUPPORTED_IMAGE_TYPE`      | The uploaded file isn't JPEG, PNG, WebP, or HEIC.                                                                                                                        |
| `CUSTOMER_IMAGE_FETCH_FAILED` | `customer: { url }` could not be downloaded — DNS failed, host unreachable, or the URL returned a non-2xx status.                                                        |
| `PRODUCT_IMAGE_FETCH_FAILED`  | A product image URL could not be downloaded during generation processing. Surfaces on `GET /generations/:id` as `errorCode` once the generation transitions to `FAILED`. |

### Unprocessable product

| Code                              | Status | When you'll see it                                                                                                                                                                                                                                                                                                                                                                                       |
| --------------------------------- | ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `MISSING_REQUIRED_CLASSIFICATION` | 422    | The engine selected for this product type requires a product image with a specific classification (e.g. pasties need an image classified `on_model`), and none of the product's images qualifies. `details.key` names the missing classification. Add a suitable image or pass an override classification. Also surfaces on `GET /generations/:id` as `errorCode` when the check runs during processing. |

### Payload size

| Code             | Status | When you'll see it                                                           |
| ---------------- | ------ | ---------------------------------------------------------------------------- |
| `FILE_TOO_LARGE` | 413    | Any single image attachment exceeds 10 MB (customer image or product image). |

### Billing

| Code                   | Status | When you'll see it                                                                                 |
| ---------------------- | ------ | -------------------------------------------------------------------------------------------------- |
| `INSUFFICIENT_CREDITS` | 402    | Account has 0 credits remaining. Top up and retry.                                                 |
| `QUOTA_EXCEEDED`       | 402    | The account-level quota check rejected the call. Back off and retry — these are usually transient. |

### Rate limiting

| Code           | Status | When you'll see it                                                                                                       |
| -------------- | ------ | ------------------------------------------------------------------------------------------------------------------------ |
| `RATE_LIMITED` | 429    | Per-account request budget exceeded. Back off and retry. Burst limits are higher than steady-state — see your dashboard. |

### Not found

| Code                   | Status | When you'll see it                                                                                                         |
| ---------------------- | ------ | -------------------------------------------------------------------------------------------------------------------------- |
| `PRODUCT_NOT_FOUND`    | 404    | `GET`/`DELETE /products/:externalId`, or `POST /try-on` referencing a missing externalId, or stats endpoint with a bad id. |
| `GENERATION_NOT_FOUND` | 404    | `GET /generations/:id` for an id that doesn't exist on the account.                                                        |
| `ACCOUNT_NOT_FOUND`    | 404    | Account lookup failed (very rare — the API key check usually fails first).                                                 |
| `ROUTE_NOT_FOUND`      | 404    | Request hit a path that doesn't exist on `/tryon/v1/*`. For unknown routes the standard error shape may not apply.         |

### Conflicts

| Code                   | Status | When you'll see it                                                                                              |
| ---------------------- | ------ | --------------------------------------------------------------------------------------------------------------- |
| `RESERVED_EXTERNAL_ID` | 409    | `POST /products` with an `externalId` starting with `_anon_`. That prefix is reserved for server-generated IDs. |

### Server

| Code                | Status | When you'll see it                                                                                                                                                                                       |
| ------------------- | ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `INTERNAL_ERROR`    | 500    | Unexpected failure on our side. Safe to retry with exponential backoff. Contact support if it recurs.                                                                                                    |
| `GENERATION_FAILED` | 500    | The generation pipeline failed mid-way (model unavailable, upstream timeout, or another internal hiccup). Surfaces on `GET /generations/:id` once the generation transitions to `FAILED`. Safe to retry. |

## Handling errors in code

Switch on `code` and let the message bubble up to your logs/UI for debugging. Don't parse `message` — it's polished text, not a stable identifier.

<CodeGroup>
  ```python Python theme={null}
  import requests

  r = requests.post(f"{BASE}/tryon/v1/try-on", headers=HDR, json=payload)
  if not r.ok:
      err = r.json()
      code = err.get("code")
      if code == "INSUFFICIENT_CREDITS":
          notify_billing(err["message"])
      elif code == "PRODUCT_NOT_FOUND":
          log_missing_product(payload["product"]["externalId"])
      elif code == "VALIDATION_FAILED":
          # err["details"] = [{ "path": "...", "message": "..." }, ...]
          log_validation_issues(err["details"])
      else:
          log_unexpected(err)
      r.raise_for_status()
  ```

  ```ts TypeScript theme={null}
  const r = await fetch(`${BASE}/tryon/v1/try-on`, {
    method: "POST",
    headers: HDR,
    body: JSON.stringify(payload),
  });

  if (!r.ok) {
    const err = (await r.json()) as {
      code: string;
      message: string;
      status: number;
      details?: Array<{ path: string; message: string }>;
    };
    switch (err.code) {
      case "INSUFFICIENT_CREDITS":
        notifyBilling(err.message);
        break;
      case "PRODUCT_NOT_FOUND":
        logMissingProduct(payload.product.externalId);
        break;
      case "VALIDATION_FAILED":
        logValidationIssues(err.details ?? []);
        break;
      default:
        logUnexpected(err);
    }
    throw new Error(`${err.code}: ${err.message}`);
  }
  ```
</CodeGroup>

## Retry guidance

| Code                              | Retry?              | Notes                                                                                                             |
| --------------------------------- | ------------------- | ----------------------------------------------------------------------------------------------------------------- |
| `MISSING_API_KEY`                 | No                  | Fix the request.                                                                                                  |
| `INVALID_API_KEY`                 | No                  | Re-issue the key from the dashboard if it was rotated.                                                            |
| `VALIDATION_FAILED`               | No                  | Fix the offending fields listed in `details`.                                                                     |
| `PRODUCT_NOT_FOUND`               | No                  | Inline-upsert the product (send a full payload on the next call), or store it via `POST /products`.               |
| `GENERATION_NOT_FOUND`            | No                  | The id is wrong or belongs to a different account.                                                                |
| `CUSTOMER_IMAGE_FETCH_FAILED`     | Sometimes           | Retry once if it might be transient; otherwise fix the source URL.                                                |
| `PRODUCT_IMAGE_FETCH_FAILED`      | Sometimes           | Surfaces on `GET /generations/:id`. Retry once for transient hiccups; otherwise fix the product image URL.        |
| `FILE_TOO_LARGE`                  | No                  | Resize/compress before resending.                                                                                 |
| `MISSING_REQUIRED_CLASSIFICATION` | No                  | Add a product image that satisfies the classification named in `details.key`, or pass an override classification. |
| `INSUFFICIENT_CREDITS`            | After top-up        | Top up from the Genlook dashboard.                                                                                |
| `QUOTA_EXCEEDED`                  | Yes (back off)      | Usually transient. Exponential backoff.                                                                           |
| `RATE_LIMITED`                    | Yes (back off)      | Honour `Retry-After` if present; otherwise exponential backoff.                                                   |
| `INTERNAL_ERROR`                  | Yes (back off, ≤3×) | Exponential backoff. Contact support if it persists.                                                              |
| `GENERATION_FAILED`               | Yes (back off, ≤3×) | Surfaces on `GET /generations/:id`. Usually transient — retry the request. Contact support if it persists.        |
