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-ключі → «Створити». Там само він вибирає, що ключу доступно і до якого моменту він працює, тому просити ключ потрібно разом із перелiком потрібних вам дій — власник позначить саме їх.

Що сказати власникові:

Готовий ключ він передає вам як пароль. У списку ключів він видний маскою (перші та останні чотири символи), повністю — у картці, звідки його й копіюють.

Докладніше з боку власника — у посібнику користувача SpikePC, розділ API-ключі.

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

---

2. Транспорт

POST https://spikepc.com/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 не змінюються — це роблять у панелі. Партнерові потрібно працювати з клієнтами та залом, а не переписувати налаштування клубу, і кожна така правка вимагала б своїх перевірок форми.

Списку ключів. За ключем не можна прочитати таблицю ключів: партнер побачив би ключі інших партнерів цієї ж організації.

Структуру прав панелі не віддаємо: зовнішній системі вона ні про що не говорить, а у відповіді з'являлися б назви наших внутрішніх функцій.

---

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 якнайшвидше та обробляйте подію в себе асинхронно: поки ви тримаєте з'єднання, задача черги зайнята вами.