---
title: "Errors"
description: "Every status code the Rigyd API returns, with example bodies and how to react."
---

import { Aside } from '@astrojs/starlight/components';

Errors come back as JSON with an `error` field. Some endpoints add extra context (`status`, `details`).

```json
{ "error": "Insufficient credits" }
```

## Status codes

| Status | Meaning                            | Example body                                                                 |
| ------ | ---------------------------------- | ---------------------------------------------------------------------------- |
| `400`  | Bad request                        | `{ "error": "Unsupported format: .xyz. Allowed: glb, gltf, ..." }`           |
| `400`  | Bad ZIP layout                     | `{ "error": "Archive must contain exactly one .obj/.gltf/.usd file" }`       |
| `400`  | Invalid filename                   | `{ "error": "Invalid filename" }`                                            |
| `400`  | Invalid `format` on download       | `{ "error": "Invalid format. Expected one of: usd, mjcf, all" }`             |
| `401`  | Missing / invalid auth             | `{ "error": "Authentication required" }` · `{ "error": "Invalid API key" }` |
| `402`  | Out of credits                     | `{ "error": "Insufficient credits" }`                                        |
| `404`  | Not found / not your job           | `{ "error": "Conversion job not found" }`                                    |
| `404`  | MJCF unavailable                   | `{ "error": "MJCF package is not available for this conversion" }`           |
| `409`  | Wrong job state                    | `{ "error": "Job not yet completed", "status": "running" }`                  |
| `409`  | Source not completed for simulate  | `{ "error": "Source job must be completed before simulation" }`              |
| `413`  | Image too big                      | `{ "error": "Image too large (max 10MB): front.jpg" }`                       |
| `422`  | Conflicting fields on `/generate`  | `{ "error": "Provide either a text prompt or image(s), not both" }`          |
| `422`  | Missing both prompt and images     | `{ "error": "Provide a text prompt or at least one image" }`                 |
| `422`  | Prompt too long                    | `{ "error": "Prompt must be 500 characters or less" }`                       |
| `422`  | Too many images                    | `{ "error": "Maximum 4 images are supported" }`                              |
| `422`  | Bad image format                   | `{ "error": "Unsupported image format: foo.tiff" }`                          |
| `422`  | Cannot simulate a simulation       | `{ "error": "Cannot simulate a simulation job" }`                            |
| `502`  | Upstream pipeline unavailable      | `{ "error": "Conversion service unavailable: ..." }`                         |
| `502`  | Generation service down            | `{ "error": "Generation service unavailable: ..." }`                         |
| `502`  | Simulation service down            | `{ "error": "Simulation service unavailable: ..." }`                         |

## Idempotency and credits

- **Credit reservation is atomic** — a `202 Accepted` means the credit is held, not yet spent.
- **Failed jobs auto-refund** — there's an internal `refund_applied_at` guard so retries don't double-refund.
- **`401` and `400` never charge** — the credit is only reserved after auth + format validation pass.

## How to react

| You see…       | Do this                                                                            |
| -------------- | ---------------------------------------------------------------------------------- |
| `401`          | Check the key. Was it revoked? Has it expired? Is the prefix correct (`rgyd_live_`)? |
| `402`          | Top up at [app.rigyd.com](https://app.rigyd.com). Show the user — don't silently retry. |
| `409` (poll)   | Keep polling. The job isn't done yet.                                              |
| `422`          | Surface the message — it's a request-shape issue and the fix is on the caller side. |
| `502` / `5xx`  | Retry with exponential backoff. The credit is auto-refunded if the job fails.      |

<Aside type="note">
  Rate limiting is not enforced today but may be added without bumping the API version. Plan for `429 Too Many Requests` with a `Retry-After` header — that's the shape we'll use when we ship it.
</Aside>