# Errors

Failed requests under `/v1/integrations` use a common JSON envelope. Branch on
HTTP status and stable `code`, not the human-readable `message`, and retain the
request ID for logs and support.

See the [API Reference](/api) for endpoint-specific error schemas.

## Error envelope

```http
HTTP/1.1 400 Bad Request
Content-Type: application/json
X-Request-Id: <request-id>
```

```json
{
  "statusCode": 400,
  "message": "Request validation failed",
  "timestamp": "<timestamp>",
  "path": "/v1/integrations/orders",
  "code": "validation-error",
  "details": {
    "errors": ["A required field is missing"]
  },
  "request_id": "<request-id>"
}
```

| Field | Use |
| --- | --- |
| `statusCode` | Numeric HTTP status. |
| `message` | Diagnostic text; do not branch on it. |
| `timestamp` | Error timestamp. |
| `path` | Requested path. |
| `code` | Stable, case-sensitive error category. |
| `details` | Error-specific context. |
| `request_id` | Identifier matching `X-Request-Id`. |

For an unexpected failure, provide the request ID, method, path, time, and
relevant order identifier to support. Never include an API key or sensitive
photo URL.

## HTTP status handling

| Status | Meaning | Client action |
| --- | --- | --- |
| `400 Bad Request` | Invalid body, query, identifier, catalog choice, photo selection, or webhook URL. | Correct the request; do not retry unchanged. |
| `401 Unauthorized` | Credentials could not authenticate the request. | Check the Authorization header and API key. |
| `402 Payment Required` | The account lacks credits for order creation. | Add credits, then reconcile before creating again. |
| `403 Forbidden` | The request is not authorized for this operation. | Check the required scope. |
| `404 Not Found` | The resource or route is unavailable in the authenticated context. | Verify the identifier and request path. |
| `409 Conflict` | The operation conflicts with current order state. | Fetch current order detail before acting. |
| `413 Payload Too Large` | An uploaded file is too large. | Resize or recompress it. |
| `429 Too Many Requests` | An endpoint rate limit was exceeded. | Honor `Retry-After`; retry with backoff and jitter. |
| `5xx` | The service encountered a transient failure. | Retry only safe operations; reconcile ambiguous writes first. |

See [Authentication](/authentication) for scope rules.

## Common and order codes

| Status | Code | Meaning or useful details |
| --- | --- | --- |
| `400` | `validation-error` | Request validation failed; inspect `details.errors`. |
| `400` | `bad-request` | The request is malformed or violates general request rules. |
| `400` | `answer-time-unavailable` | Inspect `available_answer_times` and `next_available_at`. |
| `400` | `mock-outcome-required` | Test keys must send one of the values in `allowed_mock_outcomes`; Live keys must omit `mock_outcome`. |
| `400` | `photo-index-not-found` | The index is invalid for the selected catalog context. |
| `400` | `missing-required-photo-indexes` | Inspect `missing_index_codes`. |
| `400` | `photo-limit-exceeded` | Inspect `index_code`, `max_photos`, and `received_photos`. |
| `400` | `unsupported-photo-url` | Use only a managed URL returned by `POST /photos`. |
| `402` | `not-enough-credits` | The account cannot fund the order. |
| `404` | `not-found` | A catalog entity was not found. |
| `404` | `order-not-found` | The order is unavailable in the authenticated context. |

Refresh [Catalog](/catalog) data before correcting catalog codes.

## Photo errors

| Status | Code | Meaning or useful details |
| --- | --- | --- |
| `400` | `photo-upload-limit-exceeded` | The request contains more than 10 files. |
| `400` | `empty-photo-file` | One of the supplied files is empty. |
| `400` | `INVALID_FILE_TYPE` | The uploaded image format is unsupported. |
| `400` | `RESOLUTION_TOO_LARGE` | An image exceeds 8,000 pixels on one side or 50 megapixels. |
| `400` | `RESOLUTION_MISSING` | Image dimensions could not be read. |
| `413` | `FILE_TOO_LARGE` | The file exceeds the active per-file limit. |
| `409` | `order-not-awaiting-update-photos` | The order is not accepting updated photos; `details.expected_status` is `update-photos`. |
| `400` | `invalid-update-photo` | A resolution violates the documented [photo-update rules](/photos#update-rules). |
| `400` | `photo-index-not-requested` | Inspect `requested_index_codes`. |
| `400` | `missing-requested-photo-indexes` | Inspect `missing_index_codes`. |
| `400` | `photo-limit-exceeded` | Too many photos were submitted for an index. |
| `400` | `unsupported-photo-url` | Upload the replacement through `POST /photos` first. |

Updated-photo submissions have no idempotency key. After a timeout, conflict,
or stale response, retrieve the order before submitting again.

## Webhook errors

| Status | Code | Meaning |
| --- | --- | --- |
| `400` | `validation-error` | Invalid URL or event fields. |
| `400` | `unsafe-webhook-url` | The destination failed the checks documented in [Webhooks](/webhooks#destination-url-checks). |
| `400` | `bad-request` | The endpoint identifier is invalid. |
| `409` | `webhook-already-exists` | The same destination URL is already registered for this API key. |
| `409` | `webhook-limit-reached` | The API key already has the maximum 10 endpoints. |
| `404` | `not-found` | The endpoint is unavailable in the authenticated context. |

## Audit errors

`GET /audit/events` returns `400` with `invalid-audit-date-range` when `from` is
later than `to`. Invalid timestamps, filters, or pagination values return the
normal `validation-error` response.

## Rate limits and retries

Authenticated LegitGrails API responses expose `X-RateLimit-Limit` and
`X-RateLimit-Remaining`. The API-key category limiter uses code
`rate-limit-exceeded` and includes `Retry-After` in seconds; the same value is
available as `details.retry_after_seconds`. Additional abuse protection can
also return `429`, so always branch on the HTTP status and honor `Retry-After`.
Do not use `X-RateLimit-Reset` to calculate the API-key category window; it is
not produced by that limiter.

Treat the response headers as authoritative, wait at least the requested
interval, and add capped exponential backoff with random jitter. The exact
per-category limits and endpoint mapping are documented in
[Rate limits](/rate-limits).

Use these rules for timeouts and transient failures:

| Operation | Safe recovery |
| --- | --- |
| Any `GET` | Retry with capped backoff; honor `Retry-After`. |
| `POST /orders` | Reconcile an ambiguous result before retrying; do not retry automatically. |
| `POST /photos` | This operation is not idempotent; reuse a known successful response and avoid automatic retries. |
| `POST /orders/{id}/update_photos` | Fetch authoritative order detail and its latest request before another submission. |
| `DELETE /webhooks/{id}` | A repeat can return `404`; treat an already absent endpoint as reconciled. |

Do not retry `400`, `401`, `402`, `403`, `404`, `409`, or `413` unchanged. For
`5xx` and transport failures, retry only idempotent or reconciled operations,
with a cap and jitter. If failures persist, retain the most recent
`request_id`.
