SpikePCAPI для партнёровUKRUENPL

Партнёрский API организации (API-ключи)

HTTP-API, по которому сторонний партнёр владельца клуба читает данные его организации: касса, система лояльности, сайт с онлайн-записью, собственная витрина сети. Ключ выдаёт владелец в панели (раздел API-ключи), он же решает, что ключу доступно и до какого момента он работает.

Это не протокол игровой станции (он работает по WebSocket) и не API мобильного приложения клиента — это отдельные контракты. Здесь нет клиентских сессий и авторизации человека: запрос приходит от программы партнёра и опознаётся ключом.

Postman. Для импорта готовы два файла:

ФайлЧто это
spike_api_postman_collection.jsonКоллекция: 51 запрос в 8 папках — все действия плюс проверки отказов, с готовыми тестами
spike_api_postman_environment.jsonОкружение: base_url и api_key (тип *secret*), остальные переменные заполняются сами

Импортируйте оба (*Import* → перетащить файлы), выберите окружение в правом верхнем углу и впишите в него api_key. Ключ держите в окружении, а не в коллекции: коллекцию передают и экспортируют, а окружение остаётся у вас.

Дальше можно запускать Runner'ом целиком — списки запоминают id первого элемента, поэтому gs.get, product.get, session.get, order.get и chat.messages работают без ручного ввода. Папки 1–7 ждут ответа 200, то есть рассчитаны на ключ с полным доступом; ключ с урезанными правами законно ответит 403 forbidden_action на недоступные действия — что именно ему доступно, говорит meta.actions.

---

1. Где взять ключ

Ключ выдаёт владелец клуба в своей панели: раздел API-ключи → «Создать». Там же он выбирает, что ключу доступно и до какого момента он работает, поэтому просить ключ нужно вместе с перечнем нужных вам действий — владелец отметит именно их.

Что сказать владельцу:

Готовый ключ он передаёт вам как пароль. В списке ключей он виден маской (первые и последние четыре символа), целиком — в карточке, откуда его и копируют.

Подробнее со стороны владельца — в руководстве пользователя SpikePC, раздел API-ключи.

Ключ не привязан к клубу: он действует на всю организацию, а конкретный клуб вы указываете параметром club_id.

---

2. Транспорт

POST https://spikepc.com/dev/spike_api/
X-Api-Key: 7b2d4f9a1c3e5d7f9b0a2c4e6f8a0b1d
Content-Type: application/json
 
{ "action": "gs.list", "v": 1, "data": { "club_id": 3, "limit": 50, "offset": 0 } }

Адрес указывайте со слэшем на конце (/spike_api/). Без слэша веб-сервер отвечает редиректом на адрес со слэшем, а тело POST-запроса при редиректе теряется — запрос придёт пустым и получит 400 bad_request.

Тот же обработчик доступен и по внутреннему адресу ?loc=spike_user_api_request — он остаётся рабочим, но в интеграциях используйте /spike_api/.

ПравилоПочему так
Только POST, тело — один JSON-объектПараметры в query до обработчика не доходят: панель пропускает из URL лишь белый список (id, loc, page, token, hash)
Ключ — заголовок X-Api-KeyПроверено: фронтовый прокси его пропускает. Authorization: Bearer до PHP не доходит вовсе, использовать его нельзя
Фолбэк — поле api_key в телеДля клиентов, которые не умеют заголовки. В журнал это поле не пишется
Ключ не передаётся в URLАдреса оседают в логах прокси и веб-сервера
Прикладные параметры — внутри dataСистемные поля (action, v, api_key) лежат рядом, не смешиваясь с прикладными
org_id в запросе не принимаетсяОрганизация берётся из ключа. Иначе партнёр одного клуба читал бы чужие данные
club_id принимаетсяНо сверяется с клубами организации ключа → иначе club_foreign (403)

v — версия контракта, необязательна (сейчас 1).

---

3. Ответ

Тот же конверт, что у станции и мобильного приложения; code дублируется в HTTP-статусе:

{
  "code": 200,
  "error_key": "",
  "error_text": "",
  "action": "gs.list",
  "data": { "items": [ … ], "total": 38, "limit": 50, "offset": 0 }
}

Коды и ключи ошибок

HTTPerror_keyКогда
400bad_requestТело не JSON-объект или нет поля action
401empty_keyКлюч не прислан
401unknown_keyКлюч не найден
401key_expiredСрок действия ключа истёк
403forbidden_actionКлючу не разрешено это действие
403club_foreignclub_id принадлежит другой организации
404unknown_actionТакого действия нет
404not_foundОбъект не найден (клуб, станция)
422bad_paramsНе хватает обязательного параметра
429rate_limitedПревышена частота (см. §7), в заголовке Retry-After
500org_unavailableБаза организации недоступна
500query_failedСбой выборки. Не путайте с пустым списком: при сбое приходит именно ошибка, а не items: []

---

4. Действия

Что делать — говорит поле action. Точка входа одна, поэтому добавление метода не меняет адрес и не требует правок на стороне партнёра. Право каждого действия — это имя функции панели: оно подписано в карточке ключа рядом с пунктом прав, и именно его владелец отмечает, чтобы открыть партнёру действие.

Действия этого раздела только читают. Изменяющие — в §5.

Служебные (без права)

actionНазначение
pingКлюч принят: организация, имя ключа, область доступа, срок, время сервера
meta.actionsЧто доступно этому ключу — список действий и их прав

Клуб и зал

actionПравоПараметрыНазначение
club.listspike_clubs_list—Клубы: название, адрес, телефоны, координаты
club.mapspike_club_maps_listclub_id*Схема зала (объект либо null, если карта не нарисована)
gs_group.listspike_gs_groups_listclub_idГруппы станций — на них держатся тарифы и абонементы
gs.listspike_gs_listclub_id, group_id, limit, offsetСтанции: номер, тип, IP, MAC, группа, режим, частота экрана, версия клиентского ПО (app_version — её сообщает сама станция при подключении; пусто = не сообщала)
gs.getspike_gs_listid или macОдна станция
gs.statusspike_club_maps_listclub_idЖивое состояние зала (см. ниже)
snap.listspike_gs_snaps_listclub_id*Золотые образы клуба: какой активен, когда снят

Каталог

actionПравоПараметрыНазначение
game.listspike_games_listclub_id, genre_id, status, search, locale, limit, offsetИгры клуба: способ запуска, рейтинг, жанры
game_genre.listspike_game_genres_listlocaleЖанры организации (список один на сеть)
banner.listspike_banners_listclub_idБаннеры: область на экране станции, период, окно времени
category.listspike_market_category_listlocaleГруппы товаров
product.listspike_market_products_listclub_id*, cat_id, status, search, locale, limit, offsetТовары с ценой клуба и остатком
product.getspike_market_products_listclub_id*, id или code или barcodeОдин товар (удобно со сканера)
stock.listspike_stock_listclub_id, type, product_id, date_from, date_to, limit, offsetДвижение склада: приход, списание, продажа, возврат, корректировка

search у товаров ищет по названию, артикулу и штрихкоду. locale подменяет name и description переводом этой локали; нет перевода — остаётся базовое значение, пустым ответ не приходит.

club_id у товаров обязателен: цена у товара своя в каждом клубе, и без клуба пришлось бы отдавать «цену вообще» — партнёр продавал бы по чужой. Если в этом клубе цены нет, приходит price: null и available: false — товар здесь не продаётся.

Время доступности товара приходит полем hours — по строке на день недели: [{day, from_sec, to_sec}], где day 0=Пн … 6=Вс, а from_sec/to_sec — секунды от полуночи. Пустой массив означает, что товар продаётся круглосуточно. to_sec — последняя включённая секунда (окно «18:00–22:00» приходит как 64800…79259), а to_sec меньше from_sec — это окно через полночь: отмеченный день задаёт начало, конец приходится на следующие сутки. Рядом идёт готовый ответ hours_open (true/false — продаётся ли прямо сейчас): считать это самому пришлось бы и по нашей конвенции границ, и в нашем часовом поясе. На поле available расписание не влияет — оно по-прежнему про цену в клубе и активность товара.

Тарификация

actionПравоПараметрыНазначение
tariff.listspike_tariffs_listclub_idТарифы: цена за час, цвет, действуют ли скидки, status (0 = тариф выведен из обращения: в новые часы расписания его не назначают, но часы, где он уже стоит, считаются по нему)
tariff.schedulespike_tariffs_scheduleclub_id*Недельная сетка: день+время → группа станций → тариф
ticket.listspike_tickets_listclub_idАбонементы: время, цена, auto_apply, status (0 = выключен клубом и не продаётся)
ticket.schedulespike_tickets_scheduleclub_id*, kind (sale \valid)Когда абонемент продаётся либо когда его можно расходовать
discount.listspike_discounts_liststatusСправочник ситуативных скидок
promocode.listspike_promocodes_liststatus, search, club_id, limit, offsetПромокоды: событие, сумма, окно, остаток активаций
payment_method.listspike_payment_methods_listtype, status, club_idКассы организации с вычисляемым балансом

Границы интервалов расписания включительные, причём time_end — последняя включённая секунда блока (час 13 → 50399 = 13:59:59). В ответе рядом с секундами идут готовые from/to, чтобы не повторять эту конвенцию у себя и не терять последний час.

Две сетки абонемента — разные: kind=sale отвечает «когда его можно купить», kind=valid — «когда купленное можно тратить». Пустая сетка действия означает «действует всегда».

У бонусной кассы balance приходит null, а не ноль: бонусы — не живые деньги в кассе.

Клиенты и деньги

actionПравоПараметрыНазначение
client.listspike_clients_listsearch, is_banned, limit, offsetКлиенты организации
client.getspike_clients_listid или email или phoneКарточка клиента + его абонементы
client.packsspike_clients_listid \email \phoneТолько абонементы
transaction.listspike_transactions_listclient_id, club_id, type, date_from, date_to, limit, offsetЛеджер операций
shift.listspike_cash_shifts_listclub_id, status, date_from, date_to, limit, offsetКассовые смены с итогами и расхождением
postpay.listspike_cashdesk_listclub_id, status, date_from, date_to, limit, offsetПостоплаты

search у клиентов ищет по почте, телефону, логину, имени и фамилии. Телефон в client.get ищется во всех форматах, в которых он мог сохраниться (380…, +380…, 0…, 38…) — импорт и регистрация писали его по-разному.

Служебные гостевые записи станций (гостевая постоплата) в выдачу клиентов не попадают. Заметки оператора и аватар наружу не отдаются.

Остаток абонемента считается по стенным часам: активированный абонемент горит с момента активации, поэтому remaining_seconds не покажет время, которое уже сгорело, пока клиент не играл.

Сессии, заказы, брони, чат

actionПравоПараметрыНазначение
session.listspike_gs_sessions_listclub_id, gs_id, client_id, status, date_from, date_to, limit, offsetИстория сессий
session.getspike_gs_sessions_listid*Сессия + журнал событий (старт, запуски игр, заказы, конец)
order.listspike_orders_listclub_id, status, gs_id, client_id, date_from, date_to, limit, offsetЗаказы магазина с позициями
order.getspike_orders_listid*Один заказ; в options позиции — выбор клиента в комбо-наборе
reserve.listspike_gs_reserve_listclub_id, gs_id, client_id, date_from, date_to, limit, offsetБрони станций
chat.listspike_chats_listclub_id, date_from, date_to, limit, offsetДиалоги (один диалог = одна сессия)
chat.messagesspike_chats_listsession_id*, limit, offsetСообщения диалога

У активной сессии итог ещё копится, поэтому в ответе есть active: true — не принимайте промежуточную сумму за окончательную.

Заказ не списывает баланс клиента — его платят на месте персоналу, поэтому total_amount это сумма к оплате, а не проведённый платёж.

Период у броней считается по пересечению, а не по началу: бронь, начавшаяся вчера и идущая сейчас, попадает в выборку за сегодня.

У диалогов total — число диалогов, а не сообщений.

Организация

actionПравоПараметрыНазначение
org.infospike_org_config_list—Часовой пояс, правила склада, лимиты абонементов и постоплаты, клубы с приёмом заказов
locale.listspike_locale_liststatusЯзыки каталога, какой по умолчанию
staff.listspike_staff_list—Сотрудники и их клубы
event.listspike_events_listsearch, date_from, date_to, limit, offsetЖурнал изменений в панели
analytics.dailyspike_analytics_listclub_id, date_from, date_toПредрасчёт по дням: занятость и выручка

org.info не отдаёт реквизиты: ни доступов к БД, ни SMTP, ни токенов Telegram, ни gs_token станций, ни баланса организации. Право «просмотр настроек» выдаётся сотруднику внутри панели, а здесь ответ уходит третьей стороне.

event.list не отдаёт снимки «до/после»: в них попадают поля целиком, включая пароли Windows клуба. Остаётся кто, когда, в каком разделе и над чем.

analytics.daily считает ночной крон в часовом поясе организации, поэтому текущий день обычно пуст — это не потеря данных: за сегодня берите живые gs.status, transaction.list, order.list. Период по умолчанию — последние 30 дней.

Страницы и фильтры

Пример: живое состояние зала

→ { "action": "gs.status", "data": { "club_id": 3 } }
 
← { "code": 200, …, "data": { "items": [
      { "gs_id": 587, "club_id": 3, "number": "2",
        "online": true, "mode": "active",
        "busy": true, "client_id": 1402, "started_at": "2026-10-01 14:05:11",
        "time_left": { "seconds": 4260, "text": "1:11", "unlimited": false } }
    ], "total": 38, "server_time": "2026-10-01 16:10:59" } }
ПолеСмысл
onlineУ станции открыто соединение с сервисом. Это про связь, не про режим
modeactive / service (обслуживание) / admin (администрирование) / disabled (выведена из проката)
busyЗа станцией играет клиент. Станция может быть busy и при потерянной связи — сессию закроет реконсиляция
time_leftОценка остатка: бесплатные минуты клуба + применимые абонементы + деньги по действующей ставке. unlimited — ставки нет или она нулевая. null — за станцией никого нет

time_left и online считаются тем же кодом, что и карта клуба, поэтому партнёр и оператор видят одну и ту же цифру.

---

5. Запись

Эти действия меняют данные клуба: принимают деньги, продают абонемент, двигают заказ, создают бронь, выдают скидку и кредит, шлют команду станции. Права у них свои — владелец отмечает их в карточке ключа отдельно от просмотра, поэтому «посмотреть» и «изменить» не выдаются одной галочкой.

Правила те же, что у оператора в панели: без открытой кассовой смены деньги не принимаются, абонемент сверх лимита организации не продаётся, бронь в закрытые часы не создаётся. Это не «похожие проверки» — обе стороны зовут один и тот же код, поэтому партнёр не может то, что запрещено оператору.

actionПравоПараметрыЧто делает
client.topupspike_clients_topupclient_id*, amount*, payment_method_id*, comment, club_idПополняет баланс клиента через кассу
client.sell_ticketspike_clients_sell_ticketclient_id*, ticket_id*Продаёт абонемент, списывая с баланса клиента
transaction.refundspike_transactions_refundtx_id*Возвращает пополнение
order.set_statusspike_orders_listorder_id*, status*Двигает заказ: new→processing→done, либо cancelled
reserve.createspike_gs_reserve_addclub_id*, gs_id*, date_start*, date_end*, client_id, descriptionСоздаёт бронь станции
reserve.cancelspike_gs_reserve_delreserve_id*Снимает бронь
discount.grantspike_club_maps_gs_discountgs_id*, discount_id*Выдаёт ситуативную скидку (0 — снимает)
credit.setspike_club_maps_gs_creditgs_id*, amount*Ставит кредит на сессию (0 — снимает)
postpay.enablespike_cashdesk_postpay_enablegs_id*, limit, guest, commentВключает постоплату на станции
postpay.limitspike_cashdesk_postpay_enablepostpay_id*, limitМеняет лимит постоплаты
postpay.stopspike_cashdesk_postpay_enablepostpay_id*Завершает постоплату (станции уходит session_end)
postpay.cancelspike_cashdesk_postpay_enablepostpay_id*Снимает постоплату, которая ждёт входа клиента
postpay.payspike_cashdesk_postpay_enablepostpay_id*, payment_method_id*, commentПринимает оплату постоплаты в кассу
gs.commandspike_club_maps_gs_commandgs_id*, action*, dataКоманда станции: message, poweroff, poweron, session_end

Деньги

Касса обязательна (payment_method_id), и список её берут из payment_method.list. Для бонусной кассы обязателен comment — бонус без пояснения не принимаем, иначе на вопрос «за что начислено» ответить нечем. Если касса общая для организации, а клубов несколько, нужен club_id: без него операция потерялась бы для отчётов клуба.

Нужна открытая кассовая смена в этом клубе, иначе 409 no_shift. Смену открывает кассир в панели — это не обходится по API: деньги должны принадлежать чьей-то смене.

Возврат (transaction.refund) возможен только для пополнения и только один раз; списание разрешено «в минус» — деньги вернули из кассы, значит с баланса они уходят целиком.

Продажа абонемента списывает с баланса клиента, учитывая его перманентную скидку (если абонемент её допускает). Пакет кладётся в запас: часы стартуют при активации на станции, а не при продаже, иначе купленный впрок абонемент сгорел бы до прихода клиента.

Зал

Скидка: за станцией клиент — процент идёт снимком в его сессию (справочник потом правят, а человек доигрывает на обещанном); станция свободна — заявка ждёт того, кто сядет, 30 минут.

Кредит — разрешённая глубина минуса до конца сессии, а не зачисление денег: клиент доигрывает в долг и гасит его обычным пополнением. Нужна активная клиентская сессия.

Команды: разрешены только message, poweroff, poweron и session_end. Режимы станции, золотые образы и прочее управление залом остаются оператору — ими распоряжается клуб, а не внешняя система. В ответе delivery: done/delivered — станция приняла, offline — не на связи, timeout — не ответила за отведённое время.

Коды отказа записи

error_keyКогда
bad_amountСумма не больше нуля (или отрицательный кредит)
invalid_payment_methodКассы нет или она выключена
comment_requiredБонусная касса без комментария
club_requiredКасса общая, а club_id не передан
no_shiftВ клубе нет открытой кассовой смены
client_not_found, ticket_not_found, order_not_found, reserve_not_found, tx_not_foundОбъекта нет
no_balanceНа балансе клиента не хватает денег на абонемент
ticket_disabledАбонемент выключен клубом и не продаётся
pack_single_only, pack_limit_reachedЛимиты организации на абонементы
not_a_topup, already_refundedВозврат: это не пополнение либо оно уже возвращено
bad_status, order_finalizedЗаказ: неизвестный статус либо он уже закрыт
bad_time, past_time, too_far, too_short, too_longБронь: интервал не подходит
outside_window, gs_busy, too_many_reservesБронь: вне часов клуба, время занято, слишком много броней
no_clientНа станции нет играющего клиента (кредит)
discount_not_found, credit_unavailableСкидки нет либо кредит в организации недоступен

---

6. Права ключа

Область доступа ключа выбирает владелец:

Право каждого действия — это имя функции панели, и оно подписано в карточке ключа рядом с пунктом прав. То есть владелец видит ровно то, что сверяется при запросе: если партнёр получает forbidden_action на gs.list, в карточке ключа нужно отметить пункт с правом spike_gs_list.

Срок действия ключа проверяется на сервере: истёк — любой запрос отвечает key_expired (401), даже если партнёр об этом не знает.

---

7. Частота запросов

Лимит — 120 запросов в минуту на ключ (значение меняет администратор системы). При превышении приходит 429 rate_limited и заголовок Retry-After: 60.

Опрос состояния зала раз в 2–5 секунд в лимит укладывается с запасом. Если нужно чаще — попросите владельца клуба поднять лимит, а не обходите его несколькими ключами: лимит считается на ключ именно для того, чтобы один партнёр не мешал другим.

---

8. Журнал обращений

Владелец может включить в карточке ключа журнал обращений: тогда каждый вызов по этому ключу хранится 7 дней — действие, код ответа, длительность, IP и тело запроса (ключ из тела вырезается). Это инструмент разбора «у партнёра не работает»: постоянно журнал не нужен.

Если ваш запрос получает отказ, а причина непонятна, попросите владельца включить журнал и повторите запрос — в журнале будет видно и действие, и код, и то, что именно вы прислали.

---

9. Чего в этом API нет

Правки справочников. Тарифы, абонементы, товары, клубы, станции, сотрудники, права, настройки организации по API не меняются — это делают в панели. Партнёру нужно работать с клиентами и залом, а не переписывать настройки клуба, и каждая такая правка требовала бы своих проверок формы.

Список ключей. По ключу нельзя прочитать spike_apikeys: партнёр увидел бы ключи других партнёров этой же организации.

Структуру прав панели (access_groups_list) не отдаём: внешней системе она ни о чём не говорит, а в ответе появлялись бы имена наших внутренних функций.

Чувствительные поля вырезаны даже там, где право на раздел есть: реквизиты БД, SMTP и токены ботов (org.info), пароли Windows клуба и снимки «до/после» (event.list), заметки оператора о клиенте (client.*), пароли сотрудников (staff.list). Из персональных данных gs.status отдаёт только client_id — без имени, телефона и почты.

---

10. Вебхуки: события сами приходят к вам

Чтобы не опрашивать API в поисках нового заказа, укажите владельцу клуба свой адрес — и события будут приходить на него сами. Настраивается в панели организации (карточка «Клиенты и заказы»): адрес, выключатель, ключ подписи и список событий.

Запрос: POST <ваш адрес>, тело — JSON:

{ "event": "order.new",
  "org_id": 11,
  "sent_at": "2026-10-08T12:46:08+03:00",
  "data": { "order_id": 42, "club_id": 1, "gs_id": 11, "client_id": 35839,
            "total": 320.00, "items": 3 } }

Заголовки: X-Spike-Event (имя события), X-Spike-Org (организация) и X-Spike-Signature — подпись тела.

События

СобытиеКогдаЧто в data
order.newклиент создал заказ на станцииorder_id, club_id, gs_id, client_id, total, items
order.statusстатус заказа изменёнorder_id, club_id, gs_id, old_status, status, total, user_login
session.startклиент авторизовался на станцииsession_id, club_id, gs_id, client_id, auth_method
session.endсессия закрытато же плюс date_start, date_end, duration_seconds, time_cost, products_cost, total_cost, reason
client.topupпополнение баланса клиентаclient_id, login, amount, balance, club_id, payment_method, type, comment, source

Организация может подписаться на часть событий — тогда придут только они.

Подпись: проверяйте её

X-Spike-Signature: sha256=<hex> — это HMAC-SHA256 от тела запроса целиком на ключе, который владелец клуба задал в настройках. Считайте подпись сами и сравнивайте с заголовком: без этой проверки на ваш адрес сможет писать кто угодно, выдавая себя за нас. Ключ не задан — заголовка не будет; просить владельца задать его стоит в любом случае.

# пример проверки (bash)
calc=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$SECRET" | awk '{print $2}')
[ "sha256=$calc" = "$SIG" ] || exit 1

Доставка

Отправку делает очередь, а не веб-запрос оператора: панель и сокет-сервис никогда не ждут ваш сервер. Отсюда правила, на которые стоит рассчитывать:

Отвечайте 200 как можно быстрее и обрабатывайте событие у себя асинхронно: пока вы держите соединение, задача очереди занята вами.