К содержанию
Lumean API

Realtime: подключение и подписка

Pusher-совместимый WebSocket: каналы заказов, клонов голоса и подарочных кодов. Почему `progress` нельзя разбирать, не глядя на канал.

Статусы заказов приходят по WebSocket в Pusher-совместимом протоколе: подходят pusher-js и Laravel Echo, своего SDK у Lumean нет. Подписка бесплатна и окна ключа не расходует — их тратит только REST.

Подключение

ПараметрЗначение
Хост / порт / схемаws.lumean.app / 443 / wss (TLS обязателен)
App key7c4dd881d8116ac76fe89aff71d8c2ff
clusterпустая строка (кластеров у сервера нет)
Транспорты['ws', 'wss']
Auth-эндпоинтPOST https://api.lumean.app/api/public/broadcasting/auth

App key публичный: он идентифицирует приложение, а не вас, и лежит открытым в спеке. Секрет здесь один — X-API-KEY, и он уходит только на auth-эндпоинт, в кадры WebSocket не попадая.

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 владельца ключа
ch.bind('status.changed', (e) => console.log(e.previous_status, '→', e.new_status))

У Laravel Echo те же параметры (authEndpoint + auth.headers), но префикс private- он ставит сам: echo.private('orders.42').

Авторизация подписки

Приватный канал требует подписи, и библиотека получает её сама: socket_id из кадра pusher:connection_established уходит вместе с channel_name (с префиксом private-) на POST /broadcasting/auth под заголовком X-API-KEY, ответная строка auth кладётся в кадр pusher:subscribe. Право зависит от канала: orders.read у заказных и канала превью, voices.read у каналов клонов голоса, reseller.read у канала кодов. Ответ идёт вне общего конверта API: ни success, ни data в нём нет, только auth.

КодЧто значитЧто делать
401Ключа нет, он неизвестен, деактивирован или истёкРетрай бесполезен; проверить ключ через GET /user
403Нет права этого канала (orders.read, voices.read или reseller.read) либо канал чужой (владелец сверяется по user_id в имени)Брать user_id из GET /user, а не из своей конфигурации
422Невалидные socket_id/channel_name — чаще всего имя без private-Чинить имя: то же тело даст тот же ответ
429Лимит auth-эндпоинта — 300 запросов в минутуНе переподписываться в цикле: одно соединение держит все каналы

Ловушка

300 — число из OpenAPI (и описание операции, и текст ответа 429); llm.md §10 называет 1200 в минуту. Расхождение не разрешено — планируйте по меньшему. Лимит у эндпоинта собственный: окна ключа он не расходует.

Каналы

КаналЧто шлётСобытийПараметрПраво
private-orders.{userId}все заказы аккаунта14числовой id владельца ключа из GET /userorders.read
private-order.task.{orderId}один заказ8UUID заказа (data.id из POST /orders)orders.read
private-chunks.preview.{userId}итог фонового превью разбиения1тот же числовой userIdorders.read
private-cartesia-voice-clones.{userId}ход клонирования голосов Cartesia1тот же числовой userIdvoices.read
private-fishaudio-voice-clones.{userId}ход клонирования голосов Fish Audio1тот же числовой userIdvoices.read
private-giftcodes.{userId}подарочные коды и статус реселлера4тот же числовой userIdreseller.read

Ловушка

В AsyncAPI параметр orderId объявлен integer — ошибка генератора, верно текстовое описание: идентификатор заказа везде UUID, его и подставляйте. А {userId} в остальных каналах и правда целое число.

GET/user

Get profile of API key owner

Канал аккаунта (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. Канал заказа (8): те же, кроме created, archive.* и policy_cooldown.*. Канал превью (1): ready (status, preview_id, summary, page) — чанков в нём нет, их берут через GET /orders/chunks/preview/{previewId}?page=1. Каналы клонов (по 1): voice_clone.updated на каждую смену статуса клона (pending → cloning → ready или failed); без WebSocket статус опрашивают списком клонов. Канал кодов (4): issued, status.changed, reseller.status.changed, reseller.application.status.changed. Значений кодов в событиях нет — только маски; полный код отдают GET /reseller/codes и GET /reseller/codes/{uuid}.

Готовность ловите по status.changed (previous_status → new_status), а не по счётчикам прогресса: последний чанк и терминальный статус — разные моменты.

Ловушка: два разных progress

Событие progress есть на обоих заказных каналах, но payload у них разный, а различить их можно только по каналу: имя одинаковое, и общий обработчик читает несуществующие поля.

private-orders.{userId}private-order.task.{orderId}
Идентификаторorder_id (UUID)task_id (тот же UUID заказа)
Счётчикиcompleted / totalchunks_done / chunks_total
Доляpercent — целое 0..100progress — число, пример 42.5
Ещё—duration_seconds, eta_seconds, eta_confidence, timestamp

Осторожно

Поля progress и chunks_done на канале одного заказа сохранены для совместимости и фактического прогресса не отражают — так сказано в самой спеке. Настоящие числа — percent и completed/total с канала аккаунта, оценка времени — eta_seconds с eta_confidence (при low показывайте со знаком «≈»). Прогресс-бар на progress из order.task врёт молча.

Оба progress считают только исходные чанки: повторы и перегенерации в них не входят, поэтому у переделанного заказа доля до 100 может не дойти вовсе.

Ловушка: ведущая точка в имени события

Правильно

Echo — с точкой: echo.private('orders.42').listen('.status.changed', fn). pusher-js — без: pusher.subscribe('private-orders.42').bind('status.changed', fn).

Неправильно

Скопировать имя из примера для Echo в bind('.status.changed'). Подписка не сработает молча: ошибки нет, соединение живо, событий не приходит ни одного.

Чего в payload нет

  • created и status.changed несут снимок заказа без списка чанков — вместо него items_summary: { total, by_status }; чанки берите из GET /orders/{order}.
  • Файлы приходят метаданными: ссылка на скачивание — всегда POST /storage/url.
  • renamed и archive.* по ключу инициировать нечем — они живут только в веб-кабинете; событие прилетит, если владелец сделал это руками.
  • policy_cooldown.applied предупреждает о кулдауне до того, как вы упрётесь в 429: ограничение накладывается при финализации провалившейся задачи, а не в ответ на запрос. Считайте по retry_at (ISO-8601): событие идёт через очередь, и retry_after стареет в пути.
429policy_cooldown_activepolicy_cooldown_active — кулдаун после блокировок политики
Что произошло
Пятый источник 429 и единственный «заказный», у которого есть `Retry-After`: в теле дополнительно приходят `retry_after` (секунды) и `retry_at` (ISO-8601). Создание новых заказов приостановлено целиком, а не только для спорного текста.
Что сделать прямо сейчас
Пауза лечит только формально. Если после кулдауна отправить тот же текст, он снова заблокируется, счётчик пойдёт по новой и ступень вырастет — лечится это правкой текста, а не ожиданием. Любая успешно выполненная задача обнуляет и счётчик, и достигнутую ступень.
Открыть страницу кода

Когда WebSocket не нужен

Одиночный короткий заказ дешевле дождаться поллингом GET /orders/{order}: соединение и разбор событий окупаются на потоке заказов или там, где прогресс видит пользователь. И даже с подпиской источник истины остаётся у REST — событие может не дойти. Ожидание без WS — в гайде Асинхронная модель.

Скормить страницу ИИ

Открыть .mdllm.md

Обновлено 17.10.2026 · спека 0.0.1