Realtime: connecting and subscribing
Pusher-compatible WebSocket: channels for orders, voice clones and gift codes. Why you can't parse `progress` without looking at the channel.
Çeviri hazırlanıyor
Order statuses arrive over WebSocket using a Pusher-compatible protocol: pusher-js
and Laravel Echo both work, and Lumean has no SDK of its own. Subscribing is free and doesn't consume the key's windows;
only REST uses them.
Connection
| Parameter | Value |
|---|---|
| Host / port / scheme | ws.lumean.app / 443 / wss (TLS required) |
| App key | 7c4dd881d8116ac76fe89aff71d8c2ff |
cluster | empty string (the server has no clusters) |
| Transports | ['ws', 'wss'] |
| Auth endpoint | POST https://api.lumean.app/api/public/broadcasting/auth |
The app key is public: it identifies the application, not you, and is published openly in the spec. The only secret
here is X-API-KEY, and it goes only to the auth endpoint, never into WebSocket frames.
import Pusher from 'pusher-js'
const pusher = new Pusher('7c4dd881d8116ac76fe89aff71d8c2ff', {
wsHost: 'ws.lumean.app', wsPort: 443, wssPort: 443,
forceTLS: true, enabledTransports: ['ws', 'wss'], cluster: '',
channelAuthorization: {
endpoint: 'https://api.lumean.app/api/public/broadcasting/auth',
headers: { 'X-API-KEY': process.env.LUMEAN_API_KEY },
},
})
const ch = pusher.subscribe('private-orders.42') // 42 = user_id of the key owner
ch.bind('status.changed', (e) => console.log(e.previous_status, '→', e.new_status))Laravel Echo takes the same parameters (authEndpoint + auth.headers), but it adds the private- prefix
itself: echo.private('orders.42').
Subscription authorization
A private channel requires a signature, and the library obtains it on its own: the socket_id from the
pusher:connection_established frame is sent along with channel_name (with the private- prefix)
to POST /broadcasting/auth with the X-API-KEY header, and the returned auth string goes
into the pusher:subscribe frame. The required scope depends on the channel: orders.read for the order channels and the preview channel, voices.read for the voice clone channels, reseller.read for the codes channel. The response comes
outside the standard API envelope: it has no success and no data, only auth.
| Code | What it means | What to do |
|---|---|---|
401 | The key is missing, unknown, deactivated, or expired | Retrying is useless; check the key with GET /user |
403 | The key lacks this channel's scope (orders.read, voices.read, or reseller.read) or the channel belongs to someone else (the owner is checked against the user_id in the name) | Take user_id from GET /user, not from your own configuration |
422 | Invalid socket_id/channel_name, most often a name without private- | Fix the name: the same body will get the same response |
429 | Auth endpoint limit: 300 requests per minute | Don't resubscribe in a loop: one connection holds all channels |
Tuzak
300 is the figure from OpenAPI (both the operation description and the 429 response text); llm.md §10 says 1200
per minute. The discrepancy is unresolved, so plan for the lower one. The endpoint has its own limit:
it doesn't consume the key's windows.
Channels
| Channel | What it sends | Events | Parameter | Scope |
|---|---|---|---|---|
private-orders.{userId} | all of the account's orders | 14 | numeric id of the key owner from GET /user | orders.read |
private-order.task.{orderId} | a single order | 8 | order UUID (data.id from POST /orders) | orders.read |
private-chunks.preview.{userId} | result of a background chunking preview | 1 | the same numeric userId | orders.read |
private-cartesia-voice-clones.{userId} | progress of Cartesia voice cloning | 1 | the same numeric userId | voices.read |
private-fishaudio-voice-clones.{userId} | progress of Fish Audio voice cloning | 1 | the same numeric userId | voices.read |
private-giftcodes.{userId} | gift codes and reseller status | 4 | the same numeric userId | reseller.read |
Tuzak
In AsyncAPI the orderId parameter is declared as integer. That is a generator bug, and the text description is correct:
the order identifier is a UUID everywhere, so that is what you pass. The {userId} in the other channels, however, really is
an integer.
Get profile of API key owner
Account channel (14): created, status.changed, item.status.changed, progress,
bulk_retry.completed, paragraph_regenerate.completed, voice_search.queue.updated,
renamed, stitch.changed, policy_cooldown.applied/.cleared, archive.progress/.ready/.failed.
Order channel (8): the same, except created, archive.*, and policy_cooldown.*.
Preview channel (1): ready (status, preview_id, summary, page). It contains no chunks;
fetch them with GET /orders/chunks/preview/{previewId}?page=1.
Clone channels (1 each): voice_clone.updated on every clone status change
(pending → cloning → ready or failed); without WebSocket, poll the status through the clone list.
Codes channel (4): issued, status.changed, reseller.status.changed,
reseller.application.status.changed. Events carry no code values, only masks; the full code
is returned by GET /reseller/codes and GET /reseller/codes/{uuid}.
Detect completion by status.changed (previous_status → new_status), not by progress
counters: the last chunk and the terminal status are different moments.
Trap: two different progress events
The progress event exists on both order channels, but its payload is different, and you can tell them apart
only by the channel: the name is the same, and a shared handler reads fields that don't exist.
private-orders.{userId} | private-order.task.{orderId} | |
|---|---|---|
| Identifier | order_id (UUID) | task_id (the same order UUID) |
| Counters | completed / total | chunks_done / chunks_total |
| Fraction done | percent, an integer 0..100 | progress, a number, e.g. 42.5 |
| Also | — | duration_seconds, eta_seconds, eta_confidence, timestamp |
Dikkat
The progress and chunks_done fields on the single-order channel are kept for compatibility and
do not reflect actual progress; the spec itself says so. The real numbers are percent
and completed/total from the account channel, and the time estimate is eta_seconds with eta_confidence
(when it is low, display it with a “≈” sign). A progress bar driven by progress from order.task lies silently.
Both progress events count only the original chunks: retries and regenerations are not included, so
the percentage of a reworked order may never reach 100 at all.
Trap: the leading dot in the event name
Doğru
Echo, with the dot: echo.private('orders.42').listen('.status.changed', fn).
pusher-js, without it: pusher.subscribe('private-orders.42').bind('status.changed', fn).
Yanlış
Copy the name from the Echo example into bind('.status.changed'). The subscription will fail silently:
no error, the connection is alive, and not a single event arrives.
What the payload doesn't contain
createdandstatus.changedcarry an order snapshot without the chunk list; instead there isitems_summary: { total, by_status }. Get the chunks fromGET /orders/{order}.- Files arrive as metadata: the download link always comes from
POST /storage/url. renamedandarchive.*can't be triggered with an API key; they exist only in the web dashboard. The event arrives if the owner did it manually.policy_cooldown.appliedwarns about a cooldown before you hit a429: the restriction is applied when a failed task is finalized, not in response to a request. Go byretry_at(ISO-8601): the event travels through a queue, andretry_aftergoes stale in transit.
- Ne oldu
- The fifth 429 source, and the only "order-side" one that has `Retry-After`: the body additionally includes `retry_after` (seconds) and `retry_at` (ISO-8601). Creation of new orders is suspended entirely, not just for the disputed text.
- Şimdi ne yapmalı
- The pause only helps on paper. If you send the same text after the cooldown, it will be blocked again, the counter starts over, and the step goes up — the fix is editing the text, not waiting. Any successfully completed task resets both the counter and the step reached.
When you don't need WebSocket
For a single short order, polling GET /orders/{order} is cheaper: the connection and event parsing
pay off with a stream of orders or where the user watches the progress. And even with
a subscription, REST remains the source of truth, because an event may never arrive. Waiting without WS is covered in the
Asynchronous order model guide.
Bu sayfayı yapay zekâya verin
Güncellendi: 17.10.2026 · spesifikasyon 0.0.1