---
title: "Retries and idempotency"
url: https://lumean.app/en/developers/docs/guide/retries
locale: en
description: "The API has no idempotency keys. Which of the four “retries” is free, which is billed as a new order, and which is pointless to call at all."
updated: 2026-09-04
sources: [post-orders, get-orders, post-orders-order-retry, post-orders-order-items-retry-failed, post-orders-order-items-item-retry, post-orders-order-items-item-regenerate, get-orders-order-items-item-text]
errors: [content-blocked, policy-flagged, max-attempts-reached, invalid-order-status]
spec_version: 0.0.1
---

# Retries and idempotency

**The public API has no idempotency keys.** There is no `Idempotency-Key` header, no deduplication
field in the `POST /orders` body, and no server that would recognize a repeated submission. Two
identical requests mean two orders and two funds holds. Everything below follows from this.

## Four “retries” you must not confuse

| Call | Works on | What it does | Cost |
| --- | --- | --- | --- |
| `POST /orders/{order}/items/retry-failed` | all `failed` chunks at once | queues them for retry in a single call: `202` + a `queued`/`skipped` plan | free, from the held remainder |
| `POST /orders/{order}/items/{item}/retry` | a single `failed` or `policy_flagged` chunk | retries the chunk, optionally with new `text` | free within `free_length` |
| `POST /orders/{order}/items/{item}/regenerate` | a `completed` chunk | redoes a finished fragment with new text | paid, new quota |
| `POST /orders/{order}/retry` | the whole order | creates a **new** order with the same parameters | paid, like creation |

The first three work on an order in `partially_completed` status; the last one, by contrast, does not
accept that status.

## Order-level retry does not mean “try again”

`POST /orders/{order}/retry` does not revive the order. It reads its parameters and creates a
**new** order from them: a new `id`, a new funds hold, a new token quota check.
It is billed in full: this is a second order, not a second attempt.

Conditions: the original order is in `completed` or `result_delivered` status and has no `failed` chunks. The body
is the same as for creation: empty in the normal case, with `confirm_payg_topup` + `quote_token`
only after a `402`. The billing flow and the codes are the same: `402 payg_topup_required`,
`409 quote_mismatch`, `429` for the token quota.

It fits exactly one scenario: “I need another identical result”, for example when the files were deleted
under the retention policy. It is unsuitable for completing a `partially_completed` order, both technically and economically.

### POST /orders/{order}/retry

Retry Order

Справочник: https://lumean.app/en/developers/docs/api/post-orders-order-retry

## Playbook: `partially_completed` → `completed`

**Step 1. One call picks up all technical failures.** The body is empty.

Сниппет: https://lumean.app/en/developers/docs/api/post-orders-order-items-retry-failed

A `202` response means the plan was accepted and the work runs in the background. `data` contains `queued[]` (what was queued)
and `skipped[]` with a machine-readable reason for each skipped chunk:

| `reason` in `skipped[]` | What to do |
| --- | --- |
| `policy_flagged` | Retry the chunk individually with **corrected** text (step 2) |
| `max_attempts_reached` | There were 5 attempts and there won't be a sixth; only a new order will do |
| `insufficient_funds` | No held remainder is left; only a new order will do |
| `active_retry_exists` | A retry is already in progress; wait for it |
| `superseded` | The chunk already has a successful version; it is healthy, nothing to do |

Next to it is `reason_label`: the same code in words, in the account's language. Don't parse it
programmatically: the wording changes whenever the localization is edited.

**Step 2. Each `policy_flagged` chunk individually, with different text.**

```bash
curl -s "$BASE/orders/<ORDER>/items/<ITEM>/text" -H "X-API-KEY: $KEY"
curl -s -X POST "$BASE/orders/<ORDER>/items/<ITEM>/retry" -H "X-API-KEY: $KEY" \
  -H "Content-Type: application/json" -d '{"text": "<corrected text>"}'
```

`GET .../items/{item}/text` returns the limits along with the text: `free_length` (within it
a retry is free), `max_billable_length`, `max_raw_length` (the technical ceiling for a chunk;
beyond it you get `422`), and the `surcharge_applies` flag, meaning “length above the norm incurs a surcharge”. The
`original_text_length` field is a synonym for `free_length`, kept for compatibility.

> [!NOTE]
> The LLM reference puts it more strictly: “`text` length ≤ `original_text_length`”. The spec
> allows longer text, it's just paid; go by `surcharge_applies`
> and check it **before** sending so the charge doesn't come as a surprise.

**Step 3. Wait.** A bulk retry has no synchronous result: poll
`GET /orders/{order}/items` or listen for the `bulk_retry.completed` event.

### 422 · `invalid_order_status` — invalid_order_status — order is in the wrong status

| Что произошло | Что делать |
| --- | --- |
| An order-level refusal of bulk retry: `POST /orders/{order}/items/retry-failed` → 422 `{"success": false, "message": "…", "reason": "invalid_order_status"}`. No chunks were queued. | Check the order status before calling. Neighboring refusals from the same endpoint differ by `reason`: 409 `stitch_in_progress` (the order is being stitched — wait and retry), 422 `generative_not_supported` (`sfx`/`music`: they have no single-chunk retry), 422 `nothing_to_retry` (no failed chunks were found). |

`Retry-After`: нет. Разбор: https://lumean.app/en/developers/docs/errors/invalid-order-status

## `retry` vs. `regenerate`

`retry` accepts only a **failed** chunk (`failed` or `policy_flagged`); `text` is optional, and without
it the same text is retried. It is free within the length already paid for (`free_length`);
longer text incurs a surcharge.

`regenerate` accepts only a **finished** (`completed`) chunk, `text` is required, and it is always
paid: new funds are held, the quota is pre-checked, and if the quota is exceeded you get `429`.

Neither is available for `sfx` and `music`: their generation is one-shot.

> [!WARNING]
> The `can_retry` / `can_regenerate` flags are not a guarantee. The spec claims that `can_retry` accounts for
> the status, the task type, and exhausted attempts; llm.md §7.11 refutes this: the flag looks **only
> at the chunk status**. `can_retry: true` happily coexists with a `400` on the actual call.

## Retrying `content_blocked` is pointless

### 422 · `content_blocked` — content_blocked — text is on the blocklist

| Что произошло | Что делать |
| --- | --- |
| The order was not created. Body: `{"success": false, "message": "…", "reason": "content_blocked"}` — WITHOUT `errors`, because the request shape has nothing to do with it. Single-chunk retry and chunk regeneration return the same `reason`. | Retrying is pointless and costly: the outcome is deterministic, and every attempt consumes resources and moves the counter toward `policy_cooldown_active`. The only action that works is to edit the offending fragment; because matching is per chunk, it is enough to fix that one fragment without rewriting the whole text. The API does not tell you why the policy triggered: that is admin-only, there is no reason field in the response and there will not be one — do not look for it. |

`Retry-After`: нет. Разбор: https://lumean.app/en/developers/docs/errors/content-blocked

The text has already been rejected by the policy and is on the global blocklist: a retry returns the same `422`. Worse,
every attempt moves the counter toward `policy_cooldown_active`: five blocked tasks
in a row stop new ones from being created for 5 minutes, then for 10 and 15.

**Правильно**

Edit the problematic fragment. Matching is done per chunk and by exact match, so
in a long text it's enough to fix one piece instead of rewriting everything.

**Неправильно**

Put the same text into a retry queue with exponential backoff. After five attempts
the whole account gets a cooldown, and the order still won't be created.

The API doesn't return the block reason in any field, so don't look for it.

## Network drop after `POST /orders`

There is no response: the order may or may not have been created. A blind retry here is exactly what causes duplicates
and double charges.

You need to check **before** retrying, with `GET /orders`. There are two traps: the list has no time filter
at all (only `status`, `template_id`, `task_type[]`, `page`, `per_page`), and the spec doesn't document the sort
order. Fetch the first page and compare `created_at` yourself.

There is one way to make this check easier in advance: put your own request identifier in the
`name` field. It affects nothing except the file name, but it makes the order unambiguously identifiable in the list.

```bash
# at creation
-d '{"template_id": "…", "input_text": "…", "name": "job-2026-09-04-0041"}'
# after a drop, before retrying: look for this name on the first page of the list
curl -s "$BASE/orders?per_page=20" -H "X-API-KEY: $KEY"
```

There is no server-side deduplication by `name`: this is your client's own convention. It is also another argument
for subscribing to `private-orders.{userId}`: the `created` event arrives regardless of whether
the HTTP response reached you.

---

- This page in the browser: https://lumean.app/en/developers/docs/guide/retries
- Full machine-readable reference: https://api.lumean.app/docs/llm.md
