Ошибки
Формат ошибки, классы и полный перечень кодов.
Ошибка возвращается с кодом ответа HTTP от 400 и объектом error в теле:
{ "error": { "type": "invalid_request", "code": "viewers_below_minimum", "message": "Зрителей на канале — не меньше 15.", "param": "viewers", "request_id": "req_2f9cA1bQ7xLm0Rk4TzP8sWd3" }}| Поле | Что содержит |
|---|---|
type | Класс ошибки. По нему удобно решать, повторять ли запрос |
code | Точная причина. Код не меняет смысла между выпусками — ветвите обработку по нему |
message | Описание для человека. Текст может уточняться, не разбирайте его программно |
param | Поле запроса, к которому относится ошибка. Есть не у всех ошибок |
request_id | Идентификатор запроса, тот же, что в заголовке X-Request-Id. В сохранённом ответе на повтор с Idempotency-Key — идентификатор первого запроса |
Классы
type | HTTP | Что делать |
|---|---|---|
authentication | 401 | Проверить ключ. Повтор с тем же ключом не поможет |
permission | 403 | Изменить права или настройки ключа в панели |
invalid_request | 400, 402, 413, 415, 422 | Исправить запрос |
not_found | 404 | Проверить идентификатор и адрес |
conflict | 409 | Объект не в том состоянии: например, запуск уже идёт. Прочитать объект и решить заново |
rate_limit | 429 | Повторить запрос через время из заголовка Retry-After |
internal | 500, 502, 503, 504 | Повторить запрос позже. Если ошибка повторяется, сообщить request_id в поддержку |
Повторяя изменяющий запрос после ошибки класса internal, передавайте тот же Idempotency-Key: так действие не выполнится второй раз, если первый запрос всё-таки прошёл. Подробнее — в разделе Идемпотентность.
Ошибки при списании
Ошибки, связанные со списанием, — insufficient_funds, daily_spend_limit_exceeded, order_not_started — означают, что деньги не списаны или уже возвращены. Интеграции не нужно проверять баланс после такой ошибки.
Обработка ошибок
Пример клиента на JavaScript (Node.js 18 и новее). Он повторяет то, что имеет смысл повторять, и передаёт наверх остальное:
- превышение частоты — после паузы из
Retry-After; - сбои и обрыв связи — с нарастающей паузой;
- изменяющие запросы — всегда с
Idempotency-Key: ключ создаётся один раз и одинаков во всех попытках, поэтому повтор не выполнит действие второй раз.
import { randomUUID } from "node:crypto";
const API_BASE = "https://api.skystream.su/app/v1";
const MAX_ATTEMPTS = 5;
const sleep = (seconds) => new Promise((resolve) => setTimeout(resolve, seconds * 1000));
export class ApiError extends Error {
constructor(status, error) {
super(error.message);
this.status = status;
this.type = error.type;
this.code = error.code;
this.param = error.param;
this.requestId = error.request_id;
}
}
/** Повторять ли запрос после такой ошибки. */
function retryable(error, method) {
if (error.type === "rate_limit") return true;
// Первый запрос с этим ключом ещё выполняется: дождаться его ответа.
if (error.code === "idempotency_key_in_progress") return true;
if (error.type !== "internal") return false;
// internal_error изменяющего запроса сохранён под ключом: повтор вернёт его же.
// Проверьте чтением, выполнилось ли действие.
return method === "GET" || error.code !== "internal_error";
}
export async function api(method, path, body) {
const idempotencyKey = method === "GET" ? null : randomUUID();
for (let attempt = 1; ; attempt++) {
const last = attempt === MAX_ATTEMPTS;
let response;
try {
response = await fetch(`${API_BASE}${path}`, {
method,
headers: {
Authorization: `Bearer ${process.env.API_KEY}`,
...(idempotencyKey && { "Idempotency-Key": idempotencyKey }),
...(body && { "Content-Type": "application/json" }),
},
body: body && JSON.stringify(body),
});
} catch (networkError) {
// Ответа нет: запрос мог выполниться. С тем же ключом повтор безопасен.
if (last) throw networkError;
await sleep(2 ** attempt);
continue;
}
if (response.ok) return response.json();
const { error } = await response.json();
if (last || !retryable(error, method)) throw new ApiError(response.status, error);
await sleep(Number(response.headers.get("Retry-After")) || 2 ** attempt);
}
}Остальные ошибки обрабатываются по коду:
try {
const order = await api("POST", "/orders/views", { channel: "example_channel", views: 1000 });
console.log(`Заказ ${order.id}: списано ${order.price.amount / 100} ₽`);
} catch (error) {
if (!(error instanceof ApiError)) throw error;
switch (error.code) {
case "insufficient_funds":
case "daily_spend_limit_exceeded":
// Деньги не списаны. Повтор не поможет: нужно пополнить счёт или поднять потолок.
break;
case "quantity_out_of_range":
// Пределы количества — в GET /prices.
break;
default:
// Неизвестный код — по классу error.type. request_id — для поддержки.
console.error(`${error.code}: ${error.message} (${error.requestId})`);
}
}Перечень кодов
Коды сгруппированы по классам. Новые коды могут появляться в рамках версии; обрабатывайте неизвестный код по его классу.
Ключ и доступ
| Код | HTTP | Что значит |
|---|---|---|
api_key_expired | 401 | Срок ключа истёк. Выпустите новый ключ в панели. |
api_key_invalid | 401 | Ключ не найден или передан не целиком. Запросы с неверным ключом считаются: после 20 за 10 минут адрес временно блокируется (too_many_invalid_keys). |
api_key_missing | 401 | Не передан ключ. Передайте его в заголовке Authorization: Bearer <ключ>. |
api_key_revoked | 401 | Ключ отозван. Выпустите новый ключ в панели. |
api_key_wrong_product | 401 | Ключ выпущен для другого раздела API. |
| Код | HTTP | Что значит |
|---|---|---|
account_locked | 403 | Доступ к панели для аккаунта закрыт. Ключи аккаунта не действуют, пока доступ не восстановлен. |
api_access_disabled | 403 | Доступ к API для аккаунта выключен. Включает его поддержка. |
api_unavailable | 403 | API для этого аккаунта недоступен. |
app_only | 403 | У аккаунта нет личного лимита зрителей, а раздел работает только с ним. |
daily_spend_limit_exceeded | 403 | Заказ превысил бы суточный потолок трат ключа. Ничего не списано. Потраченное за сутки — в key.spent_today ответа GET /account; сутки считаются по московскому времени. |
insufficient_scope | 403 | У ключа нет права, которого требует запрос. Права ключа меняются в панели; расширение прав подтверждается кодом. |
ip_not_allowed | 403 | Запрос пришёл с адреса, которого нет в списке IP ключа. Список меняется в панели. |
service_disabled | 403 | Услуга временно недоступна. |
Ошибки в запросе
| Код | HTTP | Что значит |
|---|---|---|
api_key_in_query | 400 | Ключ передан в строке запроса. Запрос отклоняется, даже если ключ верный: строка запроса сохраняется в журналах. Передавайте ключ только в заголовке и замените этот ключ в панели — его следует считать раскрытым. |
auto_views_panel_only | 400 | Автопросмотры включаются только в панели: они оплачиваются поминутно, мимо суточного потолка ключа. Через API их можно только выключить. |
channel_limit_reached | 400 | Заведено наибольшее число каналов. Удалите ненужный канал. |
channel_not_on_platform | 400 | Канала с таким именем на площадке нет. |
choice_not_found | 400 | Варианта нет в текущем опросе. Варианты — в GET /chat-panels/{id}/poll. |
country_unavailable | 400 | Страны нет в списке доступных. Список — в available.geo_countries настроек канала. |
duration_above_cap | 400 | Срок больше наибольшего срока одного запуска. |
duration_not_supported | 400 | Срок запуска задаётся только у разового канала. |
duration_out_of_range | 400 | Срок вне пределов услуги. Пределы — в GET /prices. |
graph_limit_reached | 400 | Создано наибольшее число графиков (max_graphs в GET /account). Удалите ненужный график. |
graph_not_found | 400 | Графика с таким id нет у аккаунта. Графики — в GET /graphs. |
https_required | 400 | Запрос отправлен по HTTP. API принимает только HTTPS и на HTTP не перенаправляет. Ключ, отправленный по HTTP, мог быть перехвачен: отзовите его в панели и выпустите новый. |
idempotency_key_invalid | 400 | Idempotency-Key — строка от 8 до 128 знаков из латинских букв, цифр и символов _ : . -. |
idempotency_key_missing | 400 | Запрос списывает деньги, и заголовок Idempotency-Key для него обязателен. |
interval_out_of_range | 400 | Пауза между сообщениями вне пределов услуги. Пределы — в GET /prices. |
invalid_body | 400 | Тело запроса не того вида: например, массив вместо объекта. |
invalid_channel_name | 400 | Имя канала не подходит для площадки. |
invalid_json | 400 | Тело запроса — не JSON. |
limit_expired | 400 | Срок лимита истёк. До продления нельзя заводить каналы, повышать их потолок и запускать зрителей; снижать потолок, удалять каналы и менять настройки можно. |
limit_not_granted | 400 | Аккаунту не выдан лимит зрителей. |
limit_too_small | 400 | Лимит аккаунта меньше наименьшего потолка канала. |
nothing_to_change | 400 | Запрос на изменение не содержит ни одного изменения. |
order_not_cancellable | 400 | Отменить можно только чат-ботов и чат-панель. |
order_not_extendable | 400 | Продлить можно только чат-ботов и чат-панель. |
outcome_not_found | 400 | Исхода нет в текущем предсказании. Исходы — в GET /chat-panels/{id}/prediction. |
parameter_invalid | 400 | Значение поля не подходит. Поле — в param, причина — в message. |
parameter_missing | 400 | Не передано обязательное поле. Его имя — в param. |
parameter_unknown | 400 | Передано поле, которого у запроса нет. Опечатка в имени поля не проходит молча: поле — в param. |
platform_not_supported | 400 | Возможности нет у площадки панели: баллов, наград, опросов и предсказаний нет у Kick, истории чата — у Twitch. |
points_votes_disabled | 400 | В опросе нельзя голосовать за баллы канала. Голосуйте без points. |
quantity_out_of_range | 400 | Количество вне пределов услуги. Пределы — в GET /prices. |
reply_message_not_found | 400 | Kick: сообщения, на которое нужен ответ, нет среди последних сообщений чата. Id берутся из GET /chat-panels/{id}/messages. |
reward_text_required | 400 | Награда требует текст. Передайте его в text. |
run_duration_cap_reached | 400 | Запуск уже работает наибольший срок одного запуска, продлевать некуда. Чтобы зрители работали дальше, запустите канал заново после остановки. |
run_not_extendable | 400 | Продлевается только разовый запуск со сроком окончания. |
setting_not_supported | 400 | Настройки нет у каналов этой площадки: например, поле Twitch у канала Kick. |
setting_unavailable | 400 | Возможность сейчас выключена владельцем сервиса. |
viewers_above_channel_cap | 400 | Число зрителей больше потолка канала. |
viewers_above_limit | 400 | Потолок канала больше лимита аккаунта. |
viewers_below_minimum | 400 | Потолок канала меньше наименьшего допустимого. |
insufficient_funds | 402 | На счёте недостаточно средств. Ничего не списано. |
request_too_large | 413 | Тело запроса больше 64 КБ. |
unsupported_media_type | 415 | Тело передано не как application/json. |
idempotency_key_reused | 422 | Ключ уже использован для другого запроса: другого адреса, строки запроса или тела. Для нового запроса нужен новый ключ. |
Объект не найден
| Код | HTTP | Что значит |
|---|---|---|
account_not_in_panel | 404 | Аккаунта с таким логином нет в панели: его могли заменить. Текущий список — в GET /chat-panels/{id}. |
resource_missing | 404 | Объекта с таким id нет у аккаунта. Чужой объект, несуществующий и id не того вида в адресе запроса дают один и тот же ответ. |
route_not_found | 404 | Такого адреса в API нет. |
Конфликт состояния
| Код | HTTP | Что значит |
|---|---|---|
active_limit_reached | 409 | Работает наибольшее число чат-панелей на этой площадке. Число — в chat_panel.max_active ответа GET /prices. |
channel_exists | 409 | Канал с этим именем уже заведён на этой площадке. |
channel_offline | 409 | Twitch: канал не в эфире, и аккаунты панели отключены до начала эфира. Они подключатся сами; status панели в это время — paused. |
chat_panel_starting | 409 | Панель ещё поднимается на сервере. Повторите запрос через минуту. |
graph_in_use | 409 | График назначен каналам, и без force=true он не удаляется. Снимите его с каналов (graph_id: null в настройках) или повторите удаление с force=true — назначения снимутся вместе с графиком. |
idempotency_key_in_progress | 409 | Запрос с этим ключом ещё выполняется. Дождитесь его ответа и повторите запрос с тем же ключом. |
insufficient_points | 409 | У аккаунта не хватает баллов канала. Баланс — в GET /chat-panels/{id}/accounts?include=points. |
no_free_accounts | 409 | Нет аккаунтов, которые могут участвовать: все уже проголосовали, стоят на другом исходе или отключены. |
order_not_active | 409 | Заказ не работает: ещё не запущен, уже завершён или отменён. |
panel_state_changed | 409 | Состояние панели изменилось во время запроса: например, аккаунт заменили в тот же момент. Повторите запрос. |
poll_not_open | 409 | На канале нет опроса, который принимает голоса. |
prediction_not_open | 409 | На канале нет предсказания, которое принимает ставки. |
reward_redeem_failed | 409 | Площадка отказала в покупке награды; причина — в тексте ошибки. Баллы не списаны. |
reward_unavailable | 409 | Награду сейчас нельзя купить: её нет в наличии, она на паузе или в откате. |
run_already_active | 409 | На канале уже есть запуск. |
run_not_active | 409 | На канале нет запуска. |
sending_in_progress | 409 | По панели уже идёт раздача голосов или ставок. Дождитесь её конца или остановите её запросом cancel. |
sending_not_active | 409 | Раздача не идёт: она уже закончилась или её не было. |
Частота
| Код | HTTP | Что значит |
|---|---|---|
rate_limited | 429 | Превышен предел частоты запросов. Повторите запрос через время из заголовка Retry-After. |
too_many_invalid_keys | 429 | С адреса пришло слишком много запросов с неверным ключом. Адрес заблокирован до времени из заголовка Retry-After. |
Сбои
| Код | HTTP | Что значит |
|---|---|---|
internal_error | 500 | Внутренняя ошибка. Если запрос шёл с Idempotency-Key, прежде чем повторять его с новым ключом, проверьте, выполнилось ли действие: повтор с тем же ключом получит тот же ответ и действие второй раз не выполнит. |
live_apply_failed | 502 | Настройки сохранены, но к уже идущим просмотрам не применились. Повторите запрос: он безопасен. |
order_not_started | 502 | Сервис не принял заказ. Списанная сумма возвращена на счёт и в суточный потолок ключа. Запрос можно повторить с тем же Idempotency-Key. |
run_partially_updated | 502 | Сервис запусков применил изменение не целиком. Запрос задаёт значения, а не прибавляет их, и повторить его безопасно. |
service_error | 502 | Сервис запусков или чат-сервис не выполнил запрос. Повторите его позже; если ошибка повторяется, сообщите в поддержку request_id. |
chat_panel_unavailable | 503 | Заказ работает, но панели сейчас нет на сервере: например, он перезапускается и восстанавливает панели. Повторите запрос через минуту. |
platform_unavailable | 503 | Площадка не ответила на проверку канала или временно отключена. Повторите запрос позже. |
server_unavailable | 503 | API временно недоступен: например, идёт обновление. Повторите запрос позже. Запрос мог оборваться уже во время выполнения, поэтому изменяющий запрос повторяйте с тем же Idempotency-Key. |
service_unavailable | 503 | Сервис запусков или чат-сервис не ответил. Повторите запрос позже. |
outcome_unknown | 504 | Чат-сервис не ответил вовремя, но действие — сообщение, голоса, ставки, покупка награды — могло выполниться. Повтор выполнил бы его второй раз, в том числе с тем же Idempotency-Key: ключ освобождается при ошибке. Сначала проверьте результат чтением. |
server_timeout | 504 | API не успел ответить. Запрос мог выполниться: повторите его с тем же Idempotency-Key — действие второй раз не выполнится — или проверьте результат чтением. |