---
title: "Errors, retries, and idempotency"
description: "The LyricWinter API response envelope, error codes, request IDs, retry rules, idempotency keys, and client mutation IDs for safe retries of paid operations."
canonical_url: https://lyricwinter.com/docs/errors-and-retries
markdown_url: https://lyricwinter.com/docs/errors-and-retries.md
last_updated: 2026-09-30
status: beta
---
# Errors, retries, and idempotency

> The LyricWinter API response envelope, error codes, request IDs, retry rules, idempotency keys, and client mutation IDs for safe retries of paid operations.

Every LyricWinter API response uses a consistent JSON envelope, and every error carries a stable `code` and a `retryable` flag. Most paid or state-changing requests support safe retries with an idempotency key or a client mutation ID. This page explains how to read errors and when it is safe to retry.

## Response envelope

Successful responses wrap the result in `data`:

```json
{
  "data": { "id": "8f14e45f-ceea-4e7a-9c2d-3b1a5f2e7c10" },
  "request_id": "req_01J9Z3K8QF4"
}
```

List endpoints that paginate add a `pagination` object next to `data`. The public OpenAPI document at `/api/v1/openapi.json` is the one exception: it is returned unwrapped.

## Error format

Errors use the same shape on every endpoint:

```json
{
  "error": {
    "code": "STUDIO_DOCUMENT_CONFLICT",
    "message": "Studio changed before this save could commit.",
    "retryable": false
  },
  "request_id": "req_01J9Z3K8QF4"
}
```

- `code` is stable and safe to branch on. `message` is human-readable and may change.
- `retryable` is `true` when repeating the same request later can succeed.
- `details` is optional structured context, such as the conflicting version.
- `request_id` is also sent as the `x-request-id` response header. Include it when you contact support.

## Error codes

| HTTP status | `code` | Retryable | What to do |
| --- | --- | --- | --- |
| 400 | `BAD_REQUEST` | No | Fix the request. `message` names the invalid field. |
| 401 | `UNAUTHORIZED` | No | Send a valid, unexpired, unrevoked API key. |
| 403 | `FORBIDDEN` | No | The key is missing a required scope, or the resource belongs to another account. |
| 404 | `NOT_FOUND` | No | Check the ID. Deleted resources return 404. |
| 409 | `CONFLICT` or a Studio conflict code | No | Re-read the resource, then retry with the current version. |
| 429 | `RATE_LIMITED` | Yes | Wait for the `Retry-After` header, then retry. |
| 500 | `INTERNAL_SERVER_ERROR` | No | Retry once later; if it persists, contact support with the `request_id`. |
| 502 | `UPSTREAM_UNAVAILABLE` | Yes | A provider is unavailable. Retry with backoff. |
| 503 | `SERVICE_UNAVAILABLE` | Yes | Studio is temporarily unavailable. Retry with backoff. |

Studio endpoints add more specific codes. Common ones:

| `code` | Meaning |
| --- | --- |
| `STUDIO_DOCUMENT_CONFLICT` | The document changed since the version you sent. |
| `STUDIO_SECTION_ORDER_CONFLICT` | The project's section order changed since the version you sent. |
| `STUDIO_IDEMPOTENCY_KEY_REUSED` | A `client_mutation_id` was reused with a different request body. |
| `STUDIO_GENERATION_PRICE_CHANGED` | The ElevenLabs v4 price changed before generation started. Review the updated quote, then retry. Status 409. |
| `STUDIO_GENERATION_PRICE_REJECTED` | One or more generation targets has an invalid price. Refresh the quote before retrying. Status 409. |
| `IDEMPOTENCY_KEY_REQUIRED` | The endpoint requires an `Idempotency-Key` header, for example `POST /studio/exports`. |

Workflow steps report their own failures in `error_code` and `error_message` on each step. For example, `STUDIO_INSUFFICIENT_BALANCE` means the account ran out of words during generation.

## When is it safe to retry?

- **Network errors and timeouts.** The request may or may not have been applied. Retry with the *same* idempotency key or `client_mutation_id`; never generate a new one for a retry.
- **`retryable: true`.** Retry with exponential backoff, starting at about one second and capping at about a minute.
- **`429 RATE_LIMITED`.** Wait at least the number of seconds in `Retry-After`.
- **`409` conflicts.** Do not retry blindly. Re-read the resource, decide whether your change still applies, and send it with the current version and a new mutation ID.
- **Other `4xx` errors.** Fix the request first.

## Idempotency

The LyricWinter API uses two idempotency mechanisms. Each endpoint in the [API reference](/docs/api) says which one it uses.

**`client_mutation_id` in the body (most Studio writes).** Send a new UUID for each intended change. Replaying the same ID with the same body returns the original result. Some creation endpoints signal a replay with `200` and `idempotent_replay: true` instead of `201`. Reusing an ID with a different body returns `409`.

For `POST /studio/sections/{sectionId}/script`, retry an uncertain response with the same body and `client_mutation_id`. The API verifies the original command receipt before accepting a replay, even if the document versions advanced; the response contains the current canonical document, not a historical snapshot. A changed body returns `409`. If the original command can no longer be reconstructed after a capability update, the retry also returns `409` without importing again; read the document to confirm the outcome. This endpoint never starts paid work.

**`Idempotency-Key` header (exports and some other endpoints).** Send a unique key of up to 255 characters per logical request. Replaying the same key with the same body returns the original result with the response header `Idempotency-Replayed: true`. Reusing a key with a different body returns `409`.

> [!IMPORTANT]
> Paid operations such as Create Audio charge once per distinct `client_mutation_id`. Keep the ID with your pending job so a retry after a crash reuses it.

## Polling long-running work

Workflows and exports return `202 Accepted` and keep running after the response. Poll the returned resource instead of repeating the start request:

- Poll every 2–3 seconds at first, then back off to every 5–10 seconds for long runs.
- Stop when the status is terminal. For workflows that is `completed`, `partially_completed`, `failed`, or `cancelled`; for exports it is `completed`, `failed`, `cancelled`, or `superseded`.
- Treat signed media URLs as short-lived. Fetch fresh ones when they expire instead of storing them.
