Introduction
TyreSoft QMS has an open JSON API. The Android TV board and the staff app use the same API, so you can connect your own solutions to it: a second screen, a kiosk, a digital signage system, a mobile app or a CRM integration.
- Base URL:
https://<your-server>/qms-api/v1 - Format: JSON in UTF-8, dates in ISO 8601 with time zone (
2026-11-16T10:31:00+01:00). - Authentication: a device token in the
Authorization: Bearer <token>header. - Versioning: the
/v1path. New fields may be added to responses – clients should ignore unknown fields. - Privacy: responses never contain customer names, phone numbers or e-mail addresses – only the number plate, time, vehicle and bay.
- Language: the
messagetexts in errors and thelangfield follow the language set for the device or location.
The examples on this page come from a running API with test data (fictitious number plates, 16 Nov 2026, 10:31, 6 bays).
Devices and roles
Every device that connects to the API has its own token and one of two roles:
| Role | Examples | Permissions |
|---|---|---|
tv | waiting-room board, second screen, digital signage | read only: /config, /board, /heartbeat |
operator | staff phone, counter tablet, integration | read and write: also /reservations, call actions, notes |
An operator device is assigned to a staff account in the panel. When the staff member loses the “QMS – staff app” permission or the account is disabled, all of their devices lose access immediately.
Pairing a device
A device needs no login or password. It creates its own token, and an administrator approves it with a code shown on screen.
- The device generates a token (32 random bytes encoded as base64url) and sends it in
POST /pair/start. - The API responds with a 6-character code valid for 15 minutes. The device shows the code on screen.
- The administrator enters the code in the panel (“QMS – Android TV boards” or “QMS – staff app”) and selects the location and name.
- The device polls
GET /pair/statusevery 3 seconds. Once approved it receivesactiveand uses its token for all further requests.
The server only stores the SHA-256 hash of the token. Disconnecting a device in the panel takes effect immediately – the next request returns 403 revoked, and the device should return to the pairing screen.
POST /pair/start
No token header. Fields: type (tv or operator), token, name, platform, app_version.
POST /v1/pair/start HTTP/1.1
Host: your-server.com
Content-Type: application/json
{
"type": "tv",
"token": "<32 random bytes, base64url>",
"name": "TV waiting room 1",
"platform": "androidtv",
"app_version": "1.0.0"
}
{
"pair_code": "B8Z8F5",
"expires_at": "2026-11-16T10:46:00+01:00",
"approve_in": "panel → QMS – Android TV boards"
}
GET /pair/status
With the token, also before approval. status: pending, active, expired (the code has expired – start pairing again), revoked.
GET /v1/pair/status HTTP/1.1
Host: your-server.com
Authorization: Bearer <token>
{
"status": "pending",
"device": {
"id": 1,
"type": "tv",
"name": "TV waiting room 1",
"status": "pending",
"location": null
}
}
{
"status": "active",
"device": {
"id": 1,
"type": "tv",
"name": "TV waiting room 1",
"status": "active",
"location": "main"
}
}Configuration
GET /config
Roles: tv, operator. Returns the device's location, language, number of bays, voice and refresh settings and the offer bar. Call it at start-up and every few minutes.
GET /v1/config HTTP/1.1
Host: your-server.com
Authorization: Bearer <token>
{
"device": {
"id": 1,
"type": "tv",
"name": "TV waiting room 1",
"status": "active",
"location": "main"
},
"location": {
"key": "main",
"name": "Tyre Service Central, 1 Example Street",
"title": null
},
"lang": "en",
"bays": 6,
"slot_minutes": 40,
"voice": true,
"blink_seconds": 60,
"queue_rows": 9,
"poll_seconds": 3,
"ad": null,
"server_time": "2026-11-16T10:31:00+01:00",
"timezone": "Europe/Warsaw"
}
| Field | Meaning |
|---|---|
lang | interface and announcement language set in the panel for the device or location |
bays | number of bays at the location |
slot_minutes | booking slot length in minutes |
voice | whether the board should play spoken announcements |
blink_seconds | how long a tile flashes after a call |
queue_rows | how many waiting bookings fit on the board |
poll_seconds | recommended interval between /board or /reservations requests |
ad | offer bar at the bottom of the board (null = none) |
server_time, timezone | server time – use it instead of the device clock |
Board state
GET /board?since=&minute=
Roles: tv, operator. Everything the waiting-room board shows: calls, the queue, occupied bays and delay notes. All devices at a location receive the same state, so several boards always show the same.
GET /v1/board HTTP/1.1
Host: your-server.com
Authorization: Bearer <token>
{
"changed": true,
"last_event_id": 28,
"minute": "202611161031",
"server_time": "2026-11-16T10:31:00+01:00",
"lang": "en",
"bays": 6,
"reservations": [
{
"id": 87817,
"time": "10:00",
"plate": "RA72 ZJR",
"bay": 2,
"status": "urgent",
"since": "2026-11-16T10:31:00+01:00"
},
{
"id": 87818,
"time": "10:00",
"plate": "GX21 UNZ",
"bay": 1,
"status": "invite",
"since": "2026-11-16T10:31:00+01:00"
},
{
"id": 87819,
"time": "10:00",
"plate": "JB22 TJG",
"bay": 4,
"status": "invite",
"since": "2026-11-16T10:31:00+01:00"
},
{
"id": 87820,
"time": "10:40",
"plate": "AN21 AHH",
"bay": 6,
"status": "invite",
"since": "2026-11-16T10:31:00+01:00"
}
],
"busyBays": [
3,
5
],
"notes": {
"10:00": {
"entering": true
},
"10:40": {
"entering": true
},
"11:20": {
"delay_min": 10
}
},
"ad": null
}
| Field | Meaning |
|---|---|
reservations[] | bookings to show: id, time (slot time), plate, bay, status, since (in this status since) |
busyBays | bays occupied by cars being serviced |
notes | notes per slot time: entering (cars driving in), delay_min (delay), text |
last_event_id, minute | markers for lightweight refreshing (see below) |
Lightweight refreshing
Send since=<last_event_id>&minute=<minute> from the previous response. If nothing has changed, the API replies briefly:
{
"changed": false,
"last_event_id": 28,
"minute": "202611161031"
}
The full list only comes after a change or once a minute (as the queue moves on). Polling every 3 seconds is therefore light even on shared hosting. The API also supports ETag / If-None-Match with a 304 Not Modified response.
Spoken announcements: new calls follow from the difference between consecutive responses – a booking that has changed to invite or urgent should be announced.
Bookings of the day
GET /reservations?date=YYYY-MM-DD
Role: operator. The full list of the day's bookings (today by default) with vehicle, status, bay and version number, the state of all bays and the notes. The staff app works on this list.
GET /v1/reservations?date=2026-11-16 HTTP/1.1
Host: your-server.com
Authorization: Bearer <token>
{
"date": "2026-11-16",
"location": "main",
"lang": "en",
"server_time": "2026-11-16T10:31:00+01:00",
"last_event_id": 28,
"bays": [
{
"nr": 1,
"state": "invite",
"reservation_id": 87818,
"plate": "GX21 UNZ"
},
{
"nr": 2,
"state": "urgent",
"reservation_id": 87817,
"plate": "RA72 ZJR"
},
{
"nr": 3,
"state": "in_service",
"reservation_id": 87814,
"plate": "XF19 EZW"
}
],
"items": [
{
"id": 87812,
"time": "08:40",
"slot": "2026-11-16 08:40:00",
"plate": "WP17 XXU",
"vehicle": {
"type": "car",
"make": "BMW",
"model": "320d",
"color": null
},
"deposit": null,
"remarks": null,
"online": true,
"status": "done",
"bay": 2,
"version": 3,
"called_at": "2026-11-16T10:31:00+01:00",
"urgent_at": null,
"started_at": "2026-11-16T10:31:00+01:00",
"finished_at": "2026-11-16T10:31:00+01:00"
},
{
"id": 87813,
"time": "08:40",
"slot": "2026-11-16 08:40:00",
"plate": "DR73 TNS",
"vehicle": {
"type": "car",
"make": "Opel",
"model": "Astra",
"color": null
},
"deposit": null,
"remarks": null,
"online": false,
"status": "done",
"bay": 3,
"version": 3,
"called_at": "2026-11-16T10:31:00+01:00",
"urgent_at": null,
"started_at": "2026-11-16T10:31:00+01:00",
"finished_at": "2026-11-16T10:31:00+01:00"
}
],
"notes": [
{
"time": "11:20",
"delay_min": 10,
"text": null
}
]
}
The list is shortened to two bookings and three bays. bays[].state: free, invite, urgent, in_service.
Booking statuses
| Status | On the board | Set by |
|---|---|---|
waiting | in the “Upcoming bookings” queue | default, actions show and reset |
invite | “Please drive in” tile with the bay number | call |
urgent | red “Final call” tile | urgent |
in_service | leaves the tiles, the bay is occupied | start |
done | disappears | finish or automatically a few hours after drive-in |
hidden | not shown | hide |
no_show | disappears | no_show |
late | disappears (only in /reservations) | not called long after the booking time |
Call actions
POST /reservations/{id}/{action}
Role: operator. Actions: call, urgent, start, finish, hide, show, no_show, reset, undo.
| Body field | Meaning |
|---|---|
bay | bay number – required for call, optional for urgent |
version | booking version known to the client; if it does not match, the API returns 409 conflict |
force | true = call even though the bay is occupied |
Calling to a bay:
POST /v1/reservations/87817/call HTTP/1.1
Host: your-server.com
Authorization: Bearer <token>
Content-Type: application/json
{
"bay": 2
}
{
"item": {
"id": 87817,
"time": "10:00",
"slot": "2026-11-16 10:00:00",
"plate": "RA72 ZJR",
"vehicle": {
"type": "car",
"make": "Skoda",
"model": "Octavia",
"color": null
},
"deposit": null,
"remarks": null,
"online": true,
"status": "invite",
"bay": 2,
"version": 1,
"called_at": "2026-11-16T10:31:00+01:00",
"urgent_at": null,
"started_at": null,
"finished_at": null
},
"last_event_id": 21
}
Final call with version check:
POST /v1/reservations/87817/urgent HTTP/1.1
Host: your-server.com
Authorization: Bearer <token>
Content-Type: application/json
{
"version": 1
}
undo restores the state before the last action on this booking – on all boards too.
Several staff at once
When two staff members act at the same time, the API never overwrites someone else's change:
Occupied bay – another car has already been called to or is standing in this bay:
POST /v1/reservations/87818/call HTTP/1.1
Host: your-server.com
Authorization: Bearer <token>
Content-Type: application/json
{
"bay": 2
}
HTTP/1.1 409 Conflict
{
"error": {
"code": "bay_busy",
"message": "Bay 2 is occupied (RA72 ZJR).",
"bay": 2,
"by": {
"reservation_id": 87817,
"plate": "RA72 ZJR",
"status": "invite"
}
}
}
Version conflict – someone changed the booking after you loaded it. The response contains the current state:
HTTP/1.1 409 Conflict
{
"error": {
"code": "conflict",
"message": "Someone else has changed this booking. Refresh the list.",
"current": {
"status": "invite",
"bay": 2,
"version": 1
}
}
}Delay notes
PUT /notes
Role: operator. Adds “approx. +10 min” or a short custom text next to a slot time.
PUT /v1/notes HTTP/1.1
Host: your-server.com
Authorization: Bearer <token>
Content-Type: application/json
{
"time": "11:20",
"delay_min": 10
}
DELETE /notes?time=HH:MM
Removes the note for that time.
Heartbeat
POST /heartbeat
Roles: tv, operator. Body: {"app_version": "1.0.0", "platform": "androidtv"}. This lets the panel show which devices are online and which version they run. Once a minute is enough – every other request also updates the last-seen time.
Errors
An error always has the form {"error": {"code": "...", "message": "..."}}. message is ready to show to the user, code is meant for handling in code.
| HTTP | error.code | When |
|---|---|---|
| 401 | no_token, unknown_device | missing or unknown token – pair again |
| 403 | not_approved, revoked, forbidden, no_rights | device not approved or disconnected, wrong role, staff member without permission |
| 404 | not_found, bad_action | booking from another location or unknown action |
| 409 | conflict, bay_busy, nothing_to_undo | change by another staff member, occupied bay, nothing to undo |
| 422 | bay_required, bad_bay, bad_time | invalid request data |
{
"error": {
"code": "forbidden",
"message": "This device has no access to this function."
}
}Best practice
- Keep the token in the device's secure storage (Android Keystore, iOS Keychain). Never put it in a URL.
- Poll
/boardeverypoll_secondswith thesinceandminuteparameters. - Send
versionwith actions; after a409, reload the list and show the user themessage. - After
401or403 revoked, delete the token and start pairing again. - Use
server_timefor displaying the time, not the device clock.
Need a test key or help with an integration? Write to biuro@tyresoft.pl.