---
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-10-15
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.

LyricWinter application API responses use a consistent JSON envelope, and application errors carry 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. Proxy errors and network failures may arrive before the application can produce this envelope. 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

Application 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.
- `retry_after_ms`, when present, is a nonnegative integer minimum retry delay in milliseconds. Retryable application `429` and `503` responses also send `Retry-After` in whole seconds or as an HTTP-date. A generic five-second hint is minimum backoff, not a prediction of recovery; nonretryable outages do not get a fabricated retry time.
- `details` is optional structured context, such as the conflicting version.
- The stable `code` is the machine-readable reason. Do not parse human messages or require a duplicate `reason_code` alias.
- `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. |
| 403 | `VOICE_MODEL_SUBSCRIPTION_REQUIRED` | No | ElevenLabs v4 requires an active paid plan. Choose another engine or upgrade. Existing audio remains playable. |
| 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 | Contact support with the `request_id`; do not automatically repeat paid work. |
| 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.
- **`retry_after_ms` or `Retry-After`.** Wait at least the server's delay, even when it exceeds your usual backoff cap. Only retry automatically when the application also marks the error retryable.
- **`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.

For a Cloudflare `504` or `524`, a non-JSON gateway response, or a connection
failure, do not infer that a start request was rejected. It may have committed.
Use the original idempotency key or mutation ID to reconcile the same request,
then poll the returned job. Never replace its ID and start duplicate paid work.
Provider-account quota failures require us to restore access; a delay hint does
not make those failures automatically resumable.

## 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.
