Retries and idempotency
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.
Übersetzung in Arbeit
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.
Retry Order
Playbook: partially_completed → completed
Step 1. One call picks up all technical failures. The body is empty.
curl -X POST 'https://api.lumean.app/api/public/orders/{order}/items/retry-failed' \
-H 'Accept: application/json' \
-H 'X-API-KEY: YOUR_API_KEY'const res = await fetch('https://api.lumean.app/api/public/orders/{order}/items/retry-failed', {
method: 'POST',
headers: {
'Accept': 'application/json',
'X-API-KEY': 'YOUR_API_KEY',
},
});
const data = await res.json();
console.log(data);import requests
url = "https://api.lumean.app/api/public/orders/{order}/items/retry-failed"
headers = {
"Accept": "application/json",
"X-API-KEY": "YOUR_API_KEY",
}
res = requests.request("POST", url, headers=headers)
print(res.status_code, res.json())<?php
$ch = curl_init('https://api.lumean.app/api/public/orders/{order}/items/retry-failed');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Accept: application/json',
'X-API-KEY: YOUR_API_KEY',
]);
$res = curl_exec($ch);
curl_close($ch);
echo $res;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.
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.
Hinweis
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.
- Was passiert ist
- 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.
- Was jetzt zu tun ist
- 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 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.
Stolperfalle
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
- Was passiert ist
- 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`.
- Was jetzt zu tun ist
- 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.
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.
Richtig
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.
Falsch
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.
# 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.
Diese Seite einer KI geben
Aktualisiert 04.09.2026 · Spezifikation 0.0.1