SpikePCPartner APIUKRUENPL

Partner API of an organization (API keys)

An HTTP API a third-party partner of the club owner uses to read the organization's data: a till, a loyalty system, a booking site, a chain's own storefront. The key is issued by the owner in the panel (the API keys section), and the owner also decides what the key may do and until when it works.

This is not the game-station protocol (that one runs over WebSocket) and not the client mobile API — those are separate contracts. There are no client sessions and no human login here: the request comes from the partner's software and is identified by the key.

Postman. Two files are ready for import:

FileWhat it is
spike_api_postman_collection.jsonThe collection: 51 requests in 8 folders — every action plus the refusal checks, with tests included
spike_api_postman_environment.jsonThe environment: base_url and api_key (type *secret*), the rest of the variables fill themselves

Import both (*Import* → drop the files in), pick the environment in the top-right corner and put your api_key into it. Keep the key in the environment, not in the collection: collections get shared and exported, the environment stays with you.

After that the whole thing can be run with the Runner — the lists remember the id of their first item, so gs.get, product.get, session.get, order.get and chat.messages work without typing anything. Folders 1–7 expect a 200, that is they assume a key with full access; a key with trimmed rights lawfully answers 403 forbidden_action on actions it cannot reach — what exactly it can reach is told by meta.actions.

---

1. Where to get a key

The key is issued by the club owner in their panel: the API keys section → "Create". That is also where they choose what the key may do and until when it works, so ask for a key together with the list of actions you need — the owner ticks exactly those.

What to tell the owner:

The finished key is handed to you like a password. In the list of keys it is shown masked (the first and last four characters); in full it is only in the card, which is where it gets copied from.

More about it from the owner's side is in the SpikePC user guide, the section API keys.

A key is not tied to a club: it works for the whole organization, and you point at a particular club with the club_id parameter.

---

2. Transport

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 } }

Write the address with the trailing slash (/spike_api/). Without it the web server answers with a redirect to the address with the slash, and the body of a POST request is lost on a redirect — the request arrives empty and gets 400 bad_request.

The same handler is also reachable at the internal address ?loc=spike_user_api_request — it keeps working, but use /spike_api/ in integrations.

RuleWhy
POST only, the body is one JSON objectQuery parameters never reach the handler: the panel lets only a whitelist through the URL (id, loc, page, token, hash)
The key goes in the X-Api-Key headerVerified: the front proxy passes it through. Authorization: Bearer does not reach PHP at all, so it cannot be used
Fallback — the api_key field in the bodyFor clients that cannot send headers. This field is never written to the log
The key is never passed in the URLAddresses end up in the proxy and web-server logs
Business parameters live inside dataThe system fields (action, v, api_key) sit next to it instead of mixing with yours
org_id in a request is not acceptedThe organization comes from the key. Otherwise a partner of one club would read another's data
club_id is acceptedBut it is checked against the clubs of the key's organization → otherwise club_foreign (403)

v is the contract version, optional (currently 1).

---

3. The response

The same envelope as the station and the mobile app use; code is mirrored in the HTTP status:

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

Codes and error keys

HTTPerror_keyWhen
400bad_requestThe body is not a JSON object, or the action field is missing
401empty_keyNo key was sent
401unknown_keyThe key was not found
401key_expiredThe key's validity has run out
403forbidden_actionThis key is not allowed to call this action
403club_foreignclub_id belongs to another organization
404unknown_actionNo such action
404not_foundThe object was not found (a club, a station)
422bad_paramsA required parameter is missing
429rate_limitedThe rate was exceeded (see §7), with a Retry-After header
500org_unavailableThe organization's database is unavailable
500query_failedA query failed. Do not confuse it with an empty list: on a failure you get an error, not items: []

---

4. Actions

What to do is told by the action field. There is a single entry point, so adding a method changes no address and needs no changes on your side. The right behind every action is the name of a function in the panel: it is printed in the key's card next to the rights checkbox, and that is exactly what the owner ticks to open the action to a partner.

The actions in this section are read-only. The ones that change data are in §5.

Service actions (no right required)

actionPurpose
pingThe key is accepted: organization, key name, access mode, expiry, server time
meta.actionsWhat this key may call — the list of actions and their rights

The club and the floor

actionRightParametersPurpose
club.listspike_clubs_list—Clubs: name, address, phones, coordinates
club.mapspike_club_maps_listclub_id*The floor plan (an object, or null if no map was drawn)
gs_group.listspike_gs_groups_listclub_idStation groups — tariffs and passes hang off them
gs.listspike_gs_listclub_id, group_id, limit, offsetStations: number, type, IP, MAC, group, mode, screen refresh rate, client software version (app_version — the station itself reports it on connect; empty = never reported)
gs.getspike_gs_listid or macA single station
gs.statusspike_club_maps_listclub_idThe live state of the floor (see below)
snap.listspike_gs_snaps_listclub_id*The club's golden images: which one is active, when it was taken

The catalogue

actionRightParametersPurpose
game.listspike_games_listclub_id, genre_id, status, search, locale, limit, offsetGames of a club: how it launches, rating, genres
game_genre.listspike_game_genres_listlocaleGenres of the organization (one list for the whole chain)
banner.listspike_banners_listclub_idBanners: the area on the station screen, the period, the time window
category.listspike_market_category_listlocaleProduct groups
product.listspike_market_products_listclub_id*, cat_id, status, search, locale, limit, offsetProducts with the club's price and the stock left
product.getspike_market_products_listclub_id*, id or code or barcodeA single product (handy from a scanner)
stock.listspike_stock_listclub_id, type, product_id, date_from, date_to, limit, offsetStock movements: receipt, write-off, sale, return, adjustment

search on products looks through the name, the article code and the barcode. locale replaces name and description with the translation of that locale; with no translation the base value stays, so the response never comes back empty.

club_id is required for products: a product has its own price in every club, and without a club we would have to return "the price in general" — a partner would sell at someone else's. If this club has no price, price: null and available: false arrive: the product is not sold here.

Product availability hours come in the hours field — one row per weekday: [{day, from_sec, to_sec}], where day is 0=Mon … 6=Sun and from_sec/to_sec are seconds from midnight. An empty array means the product is sold around the clock. to_sec is the last included second (the window "18:00–22:00" arrives as 64800…79259), and a to_sec lower than from_sec is a window that runs past midnight: the listed day is where it starts and the end falls on the next day. Next to it comes a ready answer, hours_open (true/false — whether the product is on sale right now): working that out yourself would take both our boundary convention and our timezone. The schedule does not affect available — that field still means the price in this club plus the product being active.

Tariffs and passes

actionRightParametersPurpose
tariff.listspike_tariffs_listclub_idTariffs: price per hour, colour, whether discounts apply, status (0 = the tariff is withdrawn: it is not assigned to new schedule hours, yet the hours it already occupies are still charged at its price)
tariff.schedulespike_tariffs_scheduleclub_id*The weekly grid: day+time → station group → tariff
ticket.listspike_tickets_listclub_idPasses: time, price, auto_apply, status (0 = disabled by the club, not for sale)
ticket.schedulespike_tickets_scheduleclub_id*, kind (sale \valid)When a pass is sold, or when it can be spent
discount.listspike_discounts_liststatusThe list of one-off discounts
promocode.listspike_promocodes_liststatus, search, club_id, limit, offsetPromo codes: event, amount, window, activations left
payment_method.listspike_payment_methods_listtype, status, club_idThe organization's tills with a computed balance

Schedule interval bounds are inclusive, and time_end is the last included second of the block (hour 13 → 50399 = 13:59:59). Next to the seconds the response carries ready-made from/to, so you do not repeat this convention on your side and do not lose the last hour.

The two pass grids are different things: kind=sale answers "when it can be bought", kind=valid — "when what was bought can be spent". An empty validity grid means "valid at any time".

For a bonus till balance comes back as null, not zero: bonuses are not real money in a till.

Clients and money

actionRightParametersPurpose
client.listspike_clients_listsearch, is_banned, limit, offsetClients of the organization
client.getspike_clients_listid or email or phoneThe client's card plus their passes
client.packsspike_clients_listid \email \phoneThe passes only
transaction.listspike_transactions_listclient_id, club_id, type, date_from, date_to, limit, offsetThe ledger of operations
shift.listspike_cash_shifts_listclub_id, status, date_from, date_to, limit, offsetCash shifts with totals and the difference
postpay.listspike_cashdesk_listclub_id, status, date_from, date_to, limit, offsetPay-after-play records

search on clients looks through the email, phone, login, first and last name. The phone in client.get is looked up in every format it could have been stored in (380…, +380…, 0…, 38…) — the import and the sign-up wrote it differently.

The service guest records of stations (guest pay-after-play) do not appear among clients. Operator notes and the avatar are not handed out.

A pass's remainder is counted by the wall clock: an activated pass burns down from the moment of activation, so remaining_seconds will not show time that has already burned while the client was away.

Sessions, orders, bookings, chat

actionRightParametersPurpose
session.listspike_gs_sessions_listclub_id, gs_id, client_id, status, date_from, date_to, limit, offsetThe history of sessions
session.getspike_gs_sessions_listid*A session plus its event log (start, game launches, orders, end)
order.listspike_orders_listclub_id, status, gs_id, client_id, date_from, date_to, limit, offsetShop orders with their lines
order.getspike_orders_listid*A single order; a combo line carries the client's choice in options
reserve.listspike_gs_reserve_listclub_id, gs_id, client_id, date_from, date_to, limit, offsetStation bookings
chat.listspike_chats_listclub_id, date_from, date_to, limit, offsetConversations (one conversation = one session)
chat.messagesspike_chats_listsession_id*, limit, offsetMessages of a conversation

An active session is still accruing its total, so the response carries active: true — do not take an interim sum for the final one.

An order does not charge the client's balance — it is paid on the spot to the staff, so total_amount is the sum due, not a payment that went through.

The period for bookings is matched by overlap, not by their start: a booking that started yesterday and is running now falls into today's selection.

For conversations total is the number of conversations, not of messages.

The organization

actionRightParametersPurpose
org.infospike_org_config_list—Time zone, stock rules, pass and pay-after-play limits, clubs with order taking
locale.listspike_locale_liststatusCatalogue languages and which one is default
staff.listspike_staff_list—Staff and their clubs
event.listspike_events_listsearch, date_from, date_to, limit, offsetThe log of changes in the panel
analytics.dailyspike_analytics_listclub_id, date_from, date_toThe daily pre-computation: occupancy and revenue

org.info hands out no credentials: no database access, no SMTP, no Telegram tokens, no station gs_token, no organization balance. The "view settings" right is given to a member of staff inside the panel, and here the response goes to a third party.

event.list does not hand out the before/after snapshots: those carry whole fields, including the club's Windows passwords. What stays is who, when, in which section and over what.

analytics.daily is computed by a nightly job in the organization's time zone, so the current day is usually empty — that is not lost data: for today use the live gs.status, transaction.list, order.list. The default period is the last 30 days.

Pages and filters

Example: the live state of the floor

→ { "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" } }
FieldMeaning
onlineThe station has an open connection to the service. This is about the link, not the mode
modeactive / service (maintenance) / admin (administration) / disabled (withdrawn from rental)
busyA client is playing at the station. A station can be busy with the link lost too — the session is closed by reconciliation
time_leftAn estimate of what is left: the club's free minutes + applicable passes + money at the current rate. unlimited — there is no rate or it is zero. null — nobody is at the station

time_left and online are computed by the same code as the club map, so a partner and an operator see the same figure.

---

5. Writing

These actions change the club's data: they take money, sell a pass, move an order, create a booking, grant a discount and a credit, send a command to a station. Their rights are separate — the owner ticks them in the key's card apart from viewing, so "look at" and "change" never come with the same checkbox.

The rules are the same as for an operator in the panel: no money is taken without an open cash shift, a pass is not sold over the organization's limit, a booking is not created outside the club's hours. These are not "similar checks" — both sides call the very same code, so a partner cannot do what an operator is not allowed to.

actionRightParametersWhat it does
client.topupspike_clients_topupclient_id*, amount*, payment_method_id*, comment, club_idTops up the client's balance through a till
client.sell_ticketspike_clients_sell_ticketclient_id*, ticket_id*Sells a pass, charging the client's balance
transaction.refundspike_transactions_refundtx_id*Refunds a top-up
order.set_statusspike_orders_listorder_id*, status*Moves an order: new→processing→done, or cancelled
reserve.createspike_gs_reserve_addclub_id*, gs_id*, date_start*, date_end*, client_id, descriptionCreates a station booking
reserve.cancelspike_gs_reserve_delreserve_id*Cancels a booking
discount.grantspike_club_maps_gs_discountgs_id*, discount_id*Grants a one-off discount (0 clears it)
credit.setspike_club_maps_gs_creditgs_id*, amount*Sets a credit for the session (0 clears it)
postpay.enablespike_cashdesk_postpay_enablegs_id*, limit, guest, commentSwitches pay-after-play on at a station
postpay.limitspike_cashdesk_postpay_enablepostpay_id*, limitChanges the pay-after-play limit
postpay.stopspike_cashdesk_postpay_enablepostpay_id*Ends pay-after-play (the station gets session_end)
postpay.cancelspike_cashdesk_postpay_enablepostpay_id*Cancels pay-after-play that is still waiting for a client
postpay.payspike_cashdesk_postpay_enablepostpay_id*, payment_method_id*, commentTakes the pay-after-play payment into a till
gs.commandspike_club_maps_gs_commandgs_id*, action*, dataA command to a station: message, poweroff, poweron, session_end

Money

A till is required (payment_method_id); the list comes from payment_method.list. For a bonus till comment is required — a bonus with no explanation is not accepted, otherwise there is nothing to answer "what was it for" with. If a till is shared across the organization and there is more than one club, club_id is needed: without it the operation would be lost for the club's own reports.

An open cash shift is required in that club, otherwise 409 no_shift. A shift is opened by a cashier in the panel and this is not worked around over the API: money has to belong to someone's shift.

A refund (transaction.refund) is possible only for a top-up and only once; the balance may go negative — the money has been handed back from the till, so it leaves the balance in full.

Selling a pass charges the client's balance, taking their permanent discount into account (if the pass allows discounts). The pass goes into store: its clock starts when it is activated at a station, not when it is sold, otherwise a pass bought in advance would burn down before the client arrives.

The floor

A discount: if a client is at the station, the percentage goes as a snapshot into their session (the list may be edited later, while the person plays on what was promised); if the station is free, the request waits for whoever sits down, for 30 minutes.

A credit is the allowed depth of a negative balance until the end of the session, not a deposit: the client plays on in debt and clears it with an ordinary top-up. An active client session is required.

Commands: only message, poweroff, poweron and session_end are allowed. Station modes, golden images and the rest of running the floor stay with the operator — that is the club's business, not an outside system's. The response carries delivery: done/delivered — the station accepted it, offline — it is not connected, timeout — it did not answer in time.

Refusal keys for writing

error_keyWhen
bad_amountThe amount is not above zero (or the credit is negative)
invalid_payment_methodThe till does not exist or is disabled
comment_requiredA bonus till without a comment
club_requiredThe till is shared and club_id was not passed
no_shiftThere is no open cash shift in the club
client_not_found, ticket_not_found, order_not_found, reserve_not_found, tx_not_foundThe object does not exist
no_balanceThe client's balance is not enough for the pass
ticket_disabledThe pass is disabled by the club and not for sale
pack_single_only, pack_limit_reachedThe organization's limits on passes
not_a_topup, already_refundedRefund: this is not a top-up, or it is already refunded
bad_status, order_finalizedOrder: unknown status, or it is already closed
bad_time, past_time, too_far, too_short, too_longBooking: the interval does not fit
outside_window, gs_busy, too_many_reservesBooking: outside the club's hours, the time is taken, too many bookings
no_clientNo client is playing at the station (credit)
discount_not_found, credit_unavailableThe discount does not exist, or credit is unavailable for the organization

---

6. The key's rights

The owner picks the key's access mode:

The right behind every action is the name of a function in the panel, and it is printed in the key's card next to the rights checkbox. So the owner sees exactly what is checked on a request: if a partner gets forbidden_action on gs.list, the item with the right spike_gs_list has to be ticked in the key's card.

The key's expiry is checked on the server: once it has run out, every request answers key_expired (401), even if the partner does not know about it.

---

7. Request rate

The limit is 120 requests per minute per key (the value is changed by the system administrator). Over it you get 429 rate_limited and a Retry-After: 60 header.

Polling the floor state every 2–5 seconds fits the limit with room to spare. If you need it more often, ask the club owner to raise the limit instead of working around it with several keys: the limit is per key precisely so that one partner does not get in the way of another.

---

8. The request log

In the key's card the owner can switch on the request log: then every call made with this key is kept for 7 days — the action, the response code, the duration, the IP and the request body (the key is cut out of the body). It is a tool for sorting out "it does not work for the partner": there is no need to keep it on all the time.

If your request is refused and the reason is unclear, ask the owner to switch the log on and repeat the request — the log will show the action, the code, and exactly what you sent.

---

9. What this API does not have

Editing the reference data. Tariffs, passes, products, clubs, stations, staff, rights and the organization's settings are not changed over the API — that happens in the panel. A partner needs to work with clients and the floor, not rewrite the club's settings, and every such edit would need its own form checks.

The list of keys. A key cannot read the keys table: a partner would see the keys of other partners of the same organization.

The panel's rights structure is not handed out: it tells an outside system nothing, while the response would start carrying the names of our internal functions.

---

10. Webhooks: events come to you

So you do not have to poll the API looking for a new order, give the club owner your address — events will arrive there on their own. It is set up in the organization panel (the "Clients and orders" card): address, switch, signature key and the list of events.

Request: POST <your address>, body is 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 } }

Headers: X-Spike-Event (event name), X-Spike-Org (organization) and X-Spike-Signature — the body signature.

Events

EventWhenWhat is in data
order.newa client placed an order at a stationorder_id, club_id, gs_id, client_id, total, items
order.statusan order status changedorder_id, club_id, gs_id, old_status, status, total, user_login
session.starta client signed in at a stationsession_id, club_id, gs_id, client_id, auth_method
session.enda session was closedthe same plus date_start, date_end, duration_seconds, time_cost, products_cost, total_cost, reason
client.topupa client balance top-upclient_id, login, amount, balance, club_id, payment_method, type, comment, source

An organization may subscribe to a subset of events — then only those arrive.

The signature: check it

X-Spike-Signature: sha256=<hex> is HMAC-SHA256 over the entire request body with the key the club owner set in the settings. Compute it yourself and compare with the header: without this check anyone could post to your address pretending to be us. If no key is set, the header is absent; asking the owner to set one is worth it in any case.

# verification example (bash)
calc=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$SECRET" | awk '{print $2}')
[ "sha256=$calc" = "$SIG" ] || exit 1

Delivery

Sending is done by the queue, not by the operator's web request: the panel and the socket service never wait for your server. Hence the rules you can rely on:

Answer 200 as fast as you can and process the event asynchronously on your side: while you hold the connection, a queue task is busy with you.