Realtime: Ereignisse über WebSocket
Auftragsstatus, Fortschritt beim Stimmklonen und Geschenkcodes müssen nicht abgefragt werden: Der Server sendet Ereignisse über WebSocket. Protokoll ist Pusher, Server ist Reverb, jedes Abo ist privat und wird mit Ihrem API-Schlüssel autorisiert. Unten: Verbindungsparameter, die vier Autorisierungsschritte, die Kanäle und die häufigsten Stolpersteine.
Verbindungsparameter
Die Werte stammen aus der AsyncAPI, nicht aus Fließtext. Host, Port und Schema müssen manuell gesetzt werden: Es gibt keinen Pusher-Cloud-Cluster, der Server ist selbst betrieben.
| Parameter | Wert | Was es bedeutet |
|---|---|---|
| Host | ws.lumean.app | Adresse des WebSocket-Servers: wsHost in pusher-js, wsHost und wssHost in Laravel Echo. |
| Port | 443 | Derselbe Wert für wsPort und wssPort — einen separaten Port ohne TLS gibt es nicht. |
| Schema | wss | wss bedeutet forceTLS: true. Eine reine ws-Verbindung von einer Produktionsdomain schlägt fehl. |
| App-Key | 7c4dd881d8116ac76fe89aff71d8c2ff | Die Reverb-Anwendungs-ID. Weder ein Geheimnis noch Ihr API-Schlüssel — alle Clients nutzen denselben Wert. |
| Abo-Autorisierung | https://api.lumean.app/api/public/broadcasting/auth | Hierhin sendet der Client socket_id und Kanalnamen, um eine Signatur zu erhalten. Ihr API-Schlüssel wird nur in dieser HTTP-Anfrage übertragen. |
Tipp
Wie ein Abo autorisiert wird
Einen privaten Kanal kann man nicht einfach mithören. Die Client-Bibliothek durchläuft vier Schritte — und nur wer sie kennt, weiß, an welchem der 403 entstanden ist.
Der Socket wird geöffnet
Der Client verbindet sich per App-Key mit dem Host. Ihr API-Schlüssel spielt hier noch keine Rolle — der Socket öffnet sich auch mit einem ungültigen Schlüssel.
socket_id trifft ein
Der Server antwortet mit pusher:connection_established samt socket_id, die genau diese Verbindung identifiziert.
Das Abo wird signiert
Der Client sendet socket_id und Kanalnamen per POST /broadcasting/auth mit dem Header X-API-KEY. Der Server prüft Schlüssel, Recht und Kanalzugehörigkeit und liefert eine Signatur. Genau hier entstehen 401, 403 und 429.
Abo bestätigt, Ereignisse fließen
Der Client übergibt die Signatur, der Kanal bestätigt mit pusher:subscription_succeeded — erst danach treffen die Ereignisse des Kanals ein.
orders.readvoices.readreseller.read— die in Schritt drei geprüften Rechte: Jeder Kanal hat sein eigenes, siehe Kanaltabelle unten.
Verbindungs-Snippets
Drei Clients, dieselbe Abfolge. Der Schlüssel im Code ist ein Platzhalter: Setzen Sie Ihren ein, aber committen Sie ihn nicht.
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': 'YOUR_API_KEY' },
},
});
const channel = pusher.subscribe('private-orders.{userId}');
channel.bind('policy_cooldown.applied', (data) => {
console.log(data);
});import Echo from 'laravel-echo';
import Pusher from 'pusher-js';
window.Pusher = Pusher;
const echo = new Echo({
broadcaster: 'pusher',
key: '7c4dd881d8116ac76fe89aff71d8c2ff',
wsHost: 'ws.lumean.app',
wsPort: 443, wssPort: 443,
forceTLS: true,
enabledTransports: ['ws', 'wss'],
cluster: '',
authEndpoint: 'https://api.lumean.app/api/public/broadcasting/auth',
auth: { headers: { 'X-API-KEY': 'YOUR_API_KEY' } },
});
echo.private('orders.{userId}')
// der führende Punkt gilt für Laravel Echo; pusher-js kennt ihn nicht
.listen('.policy_cooldown.applied', (e) => {
console.log(e);
});# pip install websocket-client requests
import json, requests, websocket
APP_KEY = "7c4dd881d8116ac76fe89aff71d8c2ff"
WS_URL = "wss://ws.lumean.app:443/app/" + APP_KEY + "?protocol=7&client=python&version=1.0"
CHANNEL = "private-orders.{userId}"
ws = websocket.create_connection(WS_URL)
# 1. socket_id aus dem Ereignis pusher:connection_established holen
hello = json.loads(ws.recv())
socket_id = json.loads(hello["data"])["socket_id"]
# 2. privates Abo mit dem eigenen API-Schlüssel autorisieren
auth = requests.post("https://api.lumean.app/api/public/broadcasting/auth",
headers={"X-API-KEY": "YOUR_API_KEY"},
data={"socket_id": socket_id, "channel_name": CHANNEL},
).json()["auth"]
# 3. abonnieren und Ereignisse empfangen
ws.send(json.dumps({"event": "pusher:subscribe",
"data": {"auth": auth, "channel": CHANNEL}}))
while True:
print(ws.recv())Kanäle
Die Kanäle sind privat: Die Namen beginnen mit private-, den Parameter in geschweiften Klammern setzen Sie selbst ein. Jeder Kanal erfordert sein eigenes Recht — es steht in der Tabelle.
- WSprivate-orders.{userId}Events for every order of the account at once.14 Ereignisseorders.readKanal öffnen
- WSprivate-order.task.{orderId}Events for a single order. Someone else's order is out of reach: subscription auth returns `403`.8 Ereignisseorders.readKanal öffnen
- WSprivate-chunks.preview.{userId}The result of a background chunk-preview split calculation. Arrives for large texts submitted to `POST /orders/chunks/preview`.1 Ereignisseorders.readKanal öffnen
- WSprivate-cartesia-voice-clones.{userId}Progress of your Cartesia voice clones: one event per status change of a clone created via `POST /voices/cartesia/clones` (or in the web dashboard). Key permission: `voices.read`.1 Ereignissevoices.readKanal öffnen
- WSprivate-fishaudio-voice-clones.{userId}Progress of your Fish Audio voice clones: one event per status change of a clone created via `POST /voices/fishaudio/clones` (or in the web dashboard). Key permission: `voices.read`.1 Ereignissevoices.readKanal öffnen
- WSprivate-giftcodes.{userId}Gift codes and reseller status of the key owner: a batch issued, a code changing status, the reseller status and the reseller application changing. Events never carry code values, only masks; the full code comes from `GET /reseller/codes` and `GET /reseller/codes/{uuid}`. Key permission: `reseller.read`.4 Ereignissereseller.readKanal öffnen
Zwei Fallen
Das Ereignis .progress gibt es auf zwei Kanälen — mit unterschiedlichem Payload
Gleicher Name, andere Form. Auf dem Auftragskanal des Nutzers zählt es fertige Chunks eines Auftrags; auf dem Kanal eines einzelnen Auftrags ist es der Task-Fortschritt in Prozent samt ETA. Ein Handler für beide Kanäle liest das zweite Ereignis wie das erste und erhält stillschweigend undefined.
// private-orders.{userId} → .progress
{ "order_id": "…", "completed": 3, "total": 7, "percent": 43 }// private-order.task.{orderId} → .progress
{ "task_id": "…", "progress": 43, "chunks_done": 3, "chunks_total": 7, "eta_seconds": 12 }Trennen Sie Handler nach Kanal, nicht nach Ereignisnamen. Die vollständigen Feldtabellen stehen auf den Kanalseiten.
Der führende Punkt: in Echo nötig, in pusher-js falsch
Laravel Echo stellt Ereignisnamen den Anwendungs-Namespace voran; der führende Punkt hebt das auf. pusher-js kennt keinen Namespace, dort steht der Name ohne Punkt. Verwechselt man beides, funktioniert das Abo, es kommen keine Ereignisse — und keine Fehlermeldung.
echo.private(channel).listen('.status.changed', handler)channel.bind('status.changed', handler)Autorisierungsfehler
Alle drei kommen als HTTP-Antwort auf POST /broadcasting/auth, nicht über den Socket. Der Socket ist zu diesem Zeitpunkt bereits offen — „verbunden“ sagt daher nichts über Ihre Rechte aus.
| Code | Wann es auftritt | Was zu tun ist |
|---|---|---|
| 401 | Der Header X-API-KEY fehlt, oder der Schlüssel ist widerrufen bzw. unvollständig kopiert. | Prüfen Sie den Schlüssel mit GET /user. Die Bibliothek erhält ihn in den Headern der Abo-Autorisierung, nicht im Konstruktor. |
| 403 | Der Schlüssel funktioniert, hat aber nicht das Recht für diesen Kanal (orders.read für Aufträge, voices.read für Stimmklone, reseller.read für Geschenkcodes) — oder Sie abonnieren einen fremden Kanal: userId und orderId im Namen müssen dem Schlüsselinhaber gehören. | Ergänzen Sie das Recht im Konto und setzen Sie Ihre eigene Kennung ein. Ein fremder Auftrag wird nie ausgeliefert. |
| 429 | Zu viele Autorisierungsversuche hintereinander — meist eine Reconnect-Schleife mit falschem Schlüssel. | Stoppen Sie den Reconnect, korrigieren Sie den Schlüssel, verbinden Sie neu. Exponentielles Backoff hilft, die Ursache ist aber fast immer der 401 eine Zeile darüber. |
POST /broadcasting/auth
Dieser Endpunkt fehlt bewusst in der Referenz: Niemand ruft ihn von Hand auf, das erledigt die Client-Bibliothek. Wenn ein Abo aber 403 liefert, debuggen Sie genau diese Anfrage — deshalb steht sie hier.
Authorizes a subscription to a private WebSocket channel using your API key. Client
libraries (Laravel Echo, pusher-js) call this endpoint themselves — point their
authEndpoint at it and add X-API-KEY to the auth headers.
Available channels are your own orders (private-orders.{userId},
private-order.task.{orderId}) and chunk-preview results
(private-chunks.preview.{userId}, see POST /orders/chunks/preview) — permission
orders.read — plus your Cartesia voice clones
(private-cartesia-voice-clones.{userId}, see POST /voices/cartesia/clones) and
Fish Audio voice clones (private-fishaudio-voice-clones.{userId}, see
POST /voices/fishaudio/clones) — permission voices.read. Someone else's channel returns 403.
Subscribing removes the need to poll GET /orders/{order}: progress and completion
arrive as events.
Required key permission: orders.read.
Anfrage-Body
The WebSocket connection id delivered to the client in the pusher:connection_established event.
Channel name. A key may only reach its own channels: orders private-orders.{userId} and private-order.task.{orderId}, chunk previews private-chunks.preview.{userId} (permission orders.read), Cartesia voice clones private-cartesia-voice-clones.{userId} and Fish Audio voice clones private-fishaudio-voice-clones.{userId} (permission voices.read), reseller gift codes private-giftcodes.{userId} (permission reseller.read).
Mögliche Antworten
200Subscription authorized. The client receives the subscription auth token.
Subscription authorized. The client receives the subscription auth token.
{
"auth": "a1b2c3d4e5f67890abcdef1234567890abcdef1234567890abcdef1234567890"
}The channel subscription signature. Hand it back to the Pusher/Echo client verbatim — it fills the value in itself. This response carries no envelope: the protocol dictates the shape.
401The key is missing, unknown, disabled or expired.
The key is missing, unknown, disabled or expired.
Diese Antwort hat keinen Body.
403The key lacks the permission the channel requires (orders.read for orders and chunk previews, voices.read for Cartesia and Fish Audio voice clones), or the channel belongs to someone else.
The key lacks the permission the channel requires (orders.read for orders and chunk previews, voices.read for Cartesia and Fish Audio voice clones), or the channel belongs to someone else.
Diese Antwort hat keinen Body.
422Invalid socket_id/channel_name (for example a channel_name without the private- prefix).
Invalid socket_id/channel_name (for example a channel_name without the private- prefix).
Diese Antwort hat keinen Body.
429Rate limit exceeded — 1200 requests per minute.
Rate limit exceeded — 1200 requests per minute.
Diese Antwort hat keinen Body.
Authentifizierung und Rechte·Leitfaden: Realtime statt Polling
Diese Seite einer KI geben
Spezifikation 0.0.1