The asynchronous order model
POST /orders answers 201 and brings no result. Nine statuses, five terminal ones, and one that looks final but isn't.
Übersetzung in Arbeit
POST /orders doesn't generate anything. It validates the request, locks the funds and the token quota,
queues the task and answers 201 with status created — in this response result
is null, and there are no files and cannot be any. The result appears minutes later, and finding out
about it is a separate job for the client.
What the 201 response contains
| Field | What it holds at creation |
|---|---|
id | The order UUID. The only thing you must store: every call goes through /orders/{order}/… |
status | created (the operation description also allows pending — right after the task is queued) |
result | null. Neither files[] nor service_files[] is present in the creation response |
total_chunks | How many fragments the server cut the text into. 0 means splitting hasn't happened yet |
completed_chunks, progress_percent | Zeros |
eta_seconds, eta_at | The estimate exists while the order is running; for an order that hasn't started and for a finished one it is null (in spec examples it is sometimes already filled in the 201 response) |
price_units | The locked amount in LMC minor units (the chunk field with the same name holds tokens) |
Create Order
Four order types, not nine
POST /orders requires one of two: template_id or task_type. A template defines the service
and all the settings — that's how TTS is created. A bare task_type works only where the service code
matches the task type.
task_type | How it's created | Where the parameters go | Files in result.files[] |
|---|---|---|---|
tts | only via template_id | input_text at the request root | 1 (the merged track) + subtitles in service_files[] |
sfx | without a template | task_data.text, plus duration_seconds, prompt_influence, loop, output_format | exactly 1 |
music | without a template | task_data.prompt, plus n_variants (1..4), music_length_ms, lyrics_text, force_instrumental | n_variants files, not merged |
voice_search | without a template | audio in input_files + task_data.top (1..50) | no files: the result is a list of similar voices |
The description key for SFX is text, for Music it's prompt; they're easy to mix up, and the rejection will
say “field is required”. Put the keys at the root of task_data, without the sfx_settings /
music_settings wrappers from the configuration reference:
in a template the wrapper gives 422, in an order it is silently dropped.
curl -s -X POST "$BASE/orders" -H "X-API-KEY: $KEY" -H "Content-Type: application/json" -d '{
"task_type": "music",
"task_data": { "prompt": "Lo-fi hip hop, calm, rainy night", "n_variants": 3,
"music_length_ms": 30000, "force_instrumental": true }
}'The price here depends on the number of generations, not their size: sfx and voice_search are one
rate per order, music is the rate x n_variants. A five-minute track costs exactly as much
as a ten-second one. The server mixes the service's default_config in on top of your values
(disabled with the skip_service_defaults: true flag), so the order's saved task_data
will contain keys you never sent.
Stolperfalle
The TaskType schema in the spec lists eight values, and voice_search isn't among them — even though the type
is accepted and orders of that type get created (the enum of the task_type field itself in the POST /orders body
has nine values). The flip side: image, txt2img, remix, image_edit and voice_clone
pass validation but fail at service resolution with 422 service_not_found — they can't be
created with an API key. As a filter, GET /orders?task_type[]=… accepts all nine values; only
the four from the table above can be created.
- Was passiert ist
- 422 with `reason: "service_not_found"` and without `errors`: the request passed shape validation but failed at service resolution. The order was not created.
- Was jetzt zu tun ist
- Check against the list of creatable types and do not send the rest. The same `reason` appears on `GET /usage/{service}` as a 404: there, `service` is the `service_code` from `GET /usage`, not an arbitrary string.
Nine statuses
| Wert |
|---|
| created |
| pending |
| in_progress |
| completed |
| result_delivered |
| failed |
| compensated |
| cancelled |
| partially_completed |
Terminal — five: completed, result_delivered, failed, compensated,
cancelled. An order in these statuses no longer changes, polling it further is pointless, and file
retention (7 days by default) is counted from the moment of this transition.
Non-terminal — four: created, pending, in_progress, partially_completed.
Stolperfalle
The description of the Order.status field in the spec lists only four terminal statuses and spells
canceled with one l. The fuller list from the GET /orders description is correct: there are five terminal
statuses, result_delivered among them. In code, rely on the enum — there and in all response examples
the value is spelled cancelled, with two ls; the single-l spelling appears only in prose.
| Status | Terminal | What to do next |
|---|---|---|
created, pending | no | Wait. While the order is here, you can still cancel it |
in_progress | no | Wait, watch completed_chunks / progress_percent |
partially_completed | no | Finished files can already be collected; failed chunks still need to be brought to completion |
completed | yes | Collect result.files[] via POST /storage/url without delay |
result_delivered | yes | Same: the files are still there until retention expires |
failed, compensated | yes | result = null, no error details in the user context. You need a new order |
cancelled | yes | The order was withdrawn via POST /orders/{order}/cancel; funds and token quota were returned |
The spec doesn't explain the difference between failed and compensated; what they share is result: null.
Cancellation is accepted only in created, pending and partially_completed, otherwise 400.
The trap: partially_completed is not final
An order moves to partially_completed when some of its chunks failed: the finished fragments
are in place, the failed ones await your decision. A handler that treats everything except
created/pending/in_progress as terminal will quietly abandon such an order half-done — even though
it can be brought to completed with two calls.
- Was passiert ist
- This is not an HTTP error. The code arrives in two ways: as the value of `OrderItem.status`, and as the `reason` in the `skipped[]` array of the 202 response to `POST /orders/{order}/items/retry-failed`. The order itself moves to `partially_completed` — a NON-terminal status: completed chunks stay in place, failed ones wait for action.
- Was jetzt zu tun ist
- Fetch the current text of the chunk, rephrase the offending fragment, and retry the chunk individually with the corrected text. The rejection reason is not exposed in any field — edit at your own discretion. Retry constraints: the new text must be ≤ `original_text_length`, no more than 5 attempts, and single-chunk retry is not available at all for `sfx` and `music`. The order-level `POST /orders/{order}/retry` will not help here — it does not accept `partially_completed`.
The playbook for finishing it is in the Retries and idempotency guide.
The order-level POST /orders/{order}/retry won't do for this: it doesn't accept
partially_completed and creates a new paid order.
Chunks
Text is split into chunks (OrderItem) before generation, and the work proceeds chunk by chunk. “Chunk” and “fragment”
mean the same thing in this documentation: in API fields they are chunk_index, total_chunks,
completed_chunks. A chunk has its own
statuses that don't match the order statuses: pending, processing, completed,
failed (a technical error) and policy_flagged (rejected by the content policy).
The list comes in items[] of GET /orders/{order}; the GET /orders list doesn't have this field.
Retries and regenerations live as separate records with the same chunk_index and a
parent_item_id reference, so the length of items[] doesn't equal total_chunks. Calculate progress from
completed_chunks / total_chunks — only the original chunks count there, otherwise the percentage
would climb above a hundred.
List Order Items
Readiness: subscribe or poll
Richtig
Subscribe to private-order.task.{orderId} (a single order) or private-orders.{userId}
(all orders of the account) and listen for status.changed, item.status.changed and progress.
The event arrives at the moment of the transition, with no extra requests at all.
Falsch
Poll GET /orders/{order} once a second until it's done. That's dozens of useless
requests per order, the risk of hitting the key's rate limit (which has no Retry-After),
and still up to a second of delay on the most important transition.
Polling isn't forbidden and works perfectly well for short orders — just do it carefully:
an interval of 2–5 seconds (the spec doesn't set a rate, but every poll consumes the key's window),
base the pause on eta_seconds / eta_at, check a dozen orders with a single
GET /orders?status=in_progress instead of N requests, and always set a cap on attempts —
failed is an answer too.
Bulk chunk retry is the same case: 202 means “plan accepted”, and the result arrives
later via the bulk_retry.completed event or by polling GET /orders/{order}/items.
Tipp
The OpenAPI introduction knows two order channels out of three. There are three: private-orders.{userId},
private-order.task.{orderId} and private-chunks.preview.{userId}; the voice clone and gift code
channels are not about orders — see the Realtime section.
Files appear later
result.files[] are paths, not links: you exchange them for a temporary URL via
POST /storage/url, and it's best to do so right at completed — the files live only until retention.
How to tell a live path from a dead one is covered in the
Files, links and storage guide.
Diese Seite einer KI geben
Aktualisiert 04.09.2026 · Spezifikation 0.0.1