Machines
List, register, and inspect your vending machines. Machines are identified by
their machineNo — the hardware serial printed on the device
(e.g. 866903013700011).
Machine acquisition costs and depreciation are recorded separately from the location and operating details on this page.
Use the address review and installation workflow
for a reviewed numbered address and an explicit cabinet-location record. It
applies equally to ZHZN, Reyeah and VC EL fleet records. Legacy labels, GPS
pins and geocodeStatus: resolved do not establish a reviewed address,
postal deliverability or physical installation.
Register a machine
POST /api/v1/machines/register — scope machines:write
The cloud half of physical installation — and installation order doesn't matter:
- Register first, power on later — the serial is provisioned as
offlinewith ten empty aisles ready to price and refill; it flips toonlinewhen the firmware first calls in. - Power on first, register later — a never-seen board that calls
/apk/getEquipmentis auto-provisioned into the unclaimed pool (fully stocked, so the kiosk boots to a working grid). Registering the serial then claims it: the machine and any vends it already took transfer into your fleet, and its Nayax terminal is re-bound to your merchant account.
| Body field | Type | Notes |
|---|---|---|
machineNo |
string | required — board serial, 8–32 alphanumeric chars |
name |
string | optional display name |
location |
string | optional |
lat / lng |
string | optional |
operatorEmail |
string | admin keys only: which fleet to register into |
curl -X POST "https://api.kiosk-x.ai/api/v1/machines/register" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"machineNo":"866903013700099","name":"Gym Lobby","location":"24h Fitness, Queens"}'
409 is returned only if the serial already belongs to another operator. The
response includes the machine's Nayax terminal binding, created automatically
under your merchant account.
List machines
GET /api/v1/machines — scope machines:read
| Query param | Type | Notes |
|---|---|---|
operatorEmail |
string | Admin keys only: filter by operator |
status |
string | online or offline |
page |
int | 1-based, default 1 |
size |
int | default 20, max 100 |
curl "https://api.kiosk-x.ai/api/v1/machines?status=online" \
-H "X-API-Key: vtm_e08725ea09876af65f431c6a3742d7c0"
{
"code": 200, "message": "success",
"data": {
"page": 1, "size": 20, "total": 1,
"records": [{
"id": "8b7df143d91c716ecfa5fc1730022f6b",
"machineNo": "866903013700011",
"name": "Ryan Test",
"status": "online",
"lastSeen": "2026-08-10T11:49:00.000+0000",
"location": "Warehouse A",
"lat": "40.7128", "lng": "-74.0060",
"geo": {"lat": 40.7128, "lng": -74.0060, "address": "Warehouse A", "placeId": null},
"notes": null,
"expireDate": "2027-08-10T12:00:00.000+0000",
"rentState": 1
}]
}
}
geo is the structured map pin for fleet-map clients (Operator X): parsed
float coordinates, the location label as address, and an OSM-derived
placeId (osm:<type><id>) when the pin was set by server-side geocoding. The flat location/lat/lng
strings stay for existing clients.
Admin keys see every operator's machines and each record includes operatorEmail.
Get machine by number
GET /api/v1/machines/{machineNo} — scope machines:read
Returns a single machine including last heartbeat (lastSeen). A machine that
doesn't exist or is not owned by your account returns 404 Machine not found.
Update machine
PATCH /api/v1/machines/{machineNo} — scope machines:write (PUT is an
alias with identical semantics)
Partial update: only the fields you provide change.
| Body field | Type | Constraint |
|---|---|---|
name |
string | ≤ 100 chars |
notes |
string | ≤ 255 chars |
location |
string | ≤ 255 chars — display label (venue name / address) |
lat, lng |
string | decimal degrees, validated ±90/±180 |
address |
string | ≤ 255 chars — geocoded server-side into lat/lng + placeId |
placeId |
string | ≤ 128 chars — place identifier (the geocoder sets osm:<type><id>) |
timeZone |
string | ≤ 64 chars — IANA zone of the venue, e.g. America/Chicago. Refused with 400 if the tz database does not know it |
curl -X PATCH https://api.kiosk-x.ai/api/v1/machines/866903013700011 \
-H "X-API-Key: vtm_e08725ea09876af65f431c6a3742d7c0" \
-H "Content-Type: application/json" \
-d '{"name": "Lobby Machine", "location": "Building 4 lobby"}'
Setting the fleet-map pin
Two paths, both through this endpoint:
Both coordinates are required together; nonfinite/out-of-range values and
0,0 are refused. Sending address and coordinates together is refused.
Changing a location label clears the previous pin. Changing GPS coordinates
clears the previous address label and geocoder evidence. Both invalidate
address and installation confirmation. A delayed geocoder result cannot
overwrite a newer placement or a different owner's record.
- Explicit coordinates (Operator X "use my GPS", or any client that
already knows the point): send
lat+lng. No geocoding happens. - Street address: send
addresswithoutlat/lng. The backend resolves it with the platform's self-hosted Photon geocoder (OSM data, no API keys), stores the canonical coordinates +placeId, and (unless you also sentlocation) sets the machine'slocationlabel to the geocoder's formatted address.
The address path needs the deployment's geocoder URL — KIOSKX_GEOCODER_URL
(the in-cluster kiosk-x-photon Service in production). Explicit lat/lng
always works with no geocoder. See the Geo platform guide for the
full self-hosted stack (reverse geocoding, POI search, tiles).
An address that did not resolve still returns 200. Read geo.geocodeStatus,
not the status code. The write is never failed over a geocoding problem: the
operator has the cabinet in the van either way, and a venue that cannot be
recorded at all is worse than one recorded without coordinates. So the machine
is saved with the address as typed, lat/lng/placeId set to null — never
0,0 and never a stale pin from wherever it used to be — and the outcome
stamped on the record (app/machine_geocode.py:120-170).
geo.geocodeStatus |
What happened | Whose problem |
|---|---|---|
resolved |
The address is on the map, with canonical coordinates and a placeId |
Nobody's |
not_found |
The geocoder answered and does not know this address | The operator's — fix the address, then POST /api/v1/machines/{machineNo}/geocode/retry with a corrected address |
provider_error |
The geocoder was unreachable, timed out, or replied with something unusable | Ours. The address may be fine; retryable unattended |
skipped |
This deployment has no geocoder configured at all. geocodeDetail reads geocoding is not configured on this deployment |
Deployment config — nothing about the address is wrong, and nothing on site can fix it |
| absent | Never geocoded: a machine pinned from device GPS, or one predating this field. Provenance is not guessed from the mere presence of coordinates | Nobody's |
geo.geocodeRetryable is true for provider_error and skipped only —
both mean the address was never actually judged, so a later attempt can still
succeed. Re-asking the same geocoder the same question after a not_found
cannot produce a different answer, so that retry is refused with 409 unless
you pass a corrected address (app/machine_geocode.py:36-49).
The consequence worth knowing before you automate against this: a 200 with
geocodeStatus: "skipped" is a machine that was not placed on the map. If your
client treats the status code as the verdict, an unconfigured geocoder looks
exactly like a successful placement, and a whole fleet lands with no pins and no
error anywhere. Assert on geocodeStatus == "resolved" when the pin matters.
Setting the venue's time zone
timeZone is only needed where the placement cannot settle it on its own — a
state that straddles a boundary, or a country with more than one zone. It
outranks every inference the backend makes, so a typo is the one way to put a
cabinet's on-screen clock confidently in the wrong hour; unknown names are
refused rather than resolved to UTC. See the
cabinet clock guide for how the zone is otherwise derived and
why a cabinet sometimes shows no clock at all.
Every write is recorded in the audit log (GET /sandbox/audit, admin only).