by Intelliverse X

Vending machine client app guide

This guide is for developers building the on-device client app that runs on a kiosk / vending machine (or a device-management service acting on its behalf). It covers which endpoints the machine side uses, how to use each one, and the sync patterns that keep the device and the platform consistent.

Base URL: https://api.kiosk-x.ai

How a machine fits into the model

The machine sync loop

A typical client app runs this loop every 2–5 minutes (the "heartbeat"):

boot ──► mint token ──► GET my machine record ──► GET my inventory ──► apply config
                              ▲                                            │
                              └────────────── every 2–5 min ◄──────────────┘

1. Mint a token at boot (OAuth clients)

curl -X POST https://api.kiosk-x.ai/oauth/token \
  -d grant_type=client_credentials \
  -d client_id=kiosk-x-sandbox-superior \
  -d client_secret=kx_secret_superior_0a1b2c3d4e5f6a7b

Tokens last 3600s — re-mint when expires_in is close to elapsing, or on the first 401. Devices using a static X-API-Key skip this step.

2. Confirm identity and config

curl ".../api/v1/machines/866903013700011" -H "X-API-Key: $KEY"

Use this to verify the device is registered, read its display name, location, and rentState (1 = active lease; treat 0 as "do not vend"). lastSeen is maintained by the platform from your traffic — a device that stops calling in shows as offline to the operator.

3. Pull the planogram (inventory config)

curl ".../api/v1/inventory/machines/866903013700011" -H "X-API-Key: $KEY"

For each aisle, apply platform-side config to the device UI and vend logic:

Field Device behavior
productName, productCode, productImageUrl What to display on the touchscreen
sellingPrice Price to charge — always use the platform value, never a cached one
currentStock Reconcile: if device count differs, trust local sensors and push a correction (step 4)
maxStock Capacity for restock math
alertThreshold When to surface "low stock" in the device UI
faulted If true and the jam is cleared, push faulted: false to clear it

4. Push state changes as they happen

After each vend, decrement the aisle (requires inventory:write):

curl -X PUT ".../api/v1/inventory/machines/866903013700011/aisles/3" \
  -H "X-API-Key: $KEY" -H "Content-Type: application/json" \
  -d '{"currentStock": 6}'

When a dispense motor jams or a sensor fails, flag the aisle so the operator sees it immediately:

curl -X PUT ".../api/v1/inventory/machines/866903013700011/aisles/3" \
  -H "X-API-Key: $KEY" -H "Content-Type: application/json" \
  -d '{"faulted": true}'

When a field tech physically refills the machine and confirms on the device screen, acknowledge it in one call:

curl -X POST ".../api/v1/inventory/machines/866903013700011/restock" \
  -H "X-API-Key: $KEY"

The platform records orders from the payment flow. A device can cross-check its local vend log against the platform's view:

curl ".../api/v1/orders?machineNo=866903013700011&startTime=2026-08-10%2000:00:00&endTime=2026-08-10%2023:59:59" \
  -H "X-API-Key: $KEY"

Match on orderNumber/buyTime. payState values: 3 = shipped (paid and dispensed), 4 = refunded. If a local vend has no matching shipped order, queue it for retry-reporting; if an order shows refunded, expect a fault flag on that aisle.

Resilience rules for device firmware

  1. Offline-first: queue writes (stock decrements, fault flags) locally and replay them in order when connectivity returns. All writes are idempotent — replaying a PUT with the same body is safe.
  2. Backoff: on 5xx or network failure, retry with exponential backoff (start 5s, cap 5 min). On 429, respect Retry-After (3600s) — batch your queued writes rather than sending one call per vend during rush hour.
  3. Budget: one heartbeat (machine GET + inventory GET) every 5 minutes plus typical vend traffic stays well inside the 1,000 req/hour limit.
  4. Clock: timestamps are UTC (yyyy-MM-dd HH:mm:ss in query params, ISO-8601 in responses). Sync the device clock via NTP; don't derive buyTime locally.
  5. Envelope: every response is {code, message, data} — check code (mirrors the HTTP status), never parse message for logic.

Endpoint cheat sheet (machine side)

Device event Call
Boot / get credentials POST /oauth/token
Heartbeat: identity + config GET /api/v1/machines/{machineNo}
Heartbeat: planogram + prices GET /api/v1/inventory/machines/{machineNo}
Vend completed PUT .../aisles/{aisleNo} with currentStock
Jam / sensor fault PUT .../aisles/{aisleNo} with faulted: true
Tech confirmed refill POST .../restock
Reconcile sales log GET /api/v1/orders?machineNo=...
Platform health probe GET /health

What the machine app should not call: GET /api/v1/machines (fleet-wide, operator concern), PATCH /api/v1/machines/... (naming/location is set by the operator), and POST /api/v1/accounts (admin only).

Reyeah "JD" kiosk: the native device protocol (/apk/*)

The Partner API above is the clean, operator-facing surface. The Reyeah JD kiosk firmware (com.ruiye.jd) instead speaks its own vendor device protocol under /apk/* (envelope {code, data, msg} with code == 0 = success). Kiosk-X implements that protocol too, so an unmodified Reyeah build can run against the Intelliverse cloud with a single base-URL change.

Device call Kiosk-X behaviour
GET /apk/getEquipment Returns the calling device's provisioned record. With auto-provisioning enabled, a valid new serial enters the unclaimed pool. Any starter product grid or stock is provisional data, not physical inventory or permission to sell. The authorized operator claims the serial, maps actual products and records actual filled quantities before acceptance.
GET /apk/getGoods Planogram built live from that machine's aisles (aisle N ↔ aisle N).
GET /apk/getStyle Kiosk UI theme (fixture).
GET /apk/getAd Operator-managed screensaver ads, proxied per machine from the Intelliverse ad system (S3-hosted video); bundled defaults as fallback.
GET /apk/getUpgradeVersion null normally; serves the CI-signed S3 APK when a rollout targets this machine — see APK rollouts.
POST /apk/apkUp Firmware reports its installed build → tracked as rollout adoption. Needs a device credential once enforcement is on — see Device credentials on /apk/* below.
POST /apk/authPassword Checks the configured manager password. Obtain authorized access through this cabinet's protected handover; never use a default copied from a guide.
POST /apk/createOrder / createOrderCart Records the vend via record_sale against the calling machine → shows up in the operator app.
everything else (ordersUpdate, uploadLog, …) Accepted and acked.

Device credentials on /apk/*

This surface was built to mirror the vendor cloud, which authenticated nothing per-machine, so no handler here has ever asked the caller to prove anything. The firmware, though, has always sent a credential: MyApplication installs three OkGo common headers at splash — secret (the constant in UrlConfigString.secret), equipmentNo and apkVersion — and they ride every request the app makes, including the parameterless ones. So a credential check can be added here without touching the client.

POST /apk/apkUp is the first call to use it, because it is the one whose write steers OTA rather than a dashboard. The version it stores is what active_rollout_for_machine compares against, so a caller who can set it can declare a cabinet current and stop it ever being offered a build again — and a report at or above the target for every machine in a wave flips that wave to completed and persists it.

Two credentials are accepted, in this order (the same ladder, for the same reasons, as /zhzn/* — see app/routes/zhzn.py:_device):

  1. x-device-key: <keyId>:<secret> — a per-device key from /zhzn/enrol. The machine comes from the key, so a holder can only ever speak as the one cabinet it was issued to.
  2. secret: <fleet secret> (KIOSKX_REYEAH_DEVICE_SECRET) — the constant baked into the published APK. Shared by every cabinet, so it proves fleet membership and nothing about which machine is calling. Accepted because it is what the firmware in the field actually sends.

The cutover

KIOSKX_REYEAH_DEVICE_AUTH_ENFORCED is off by default, and enforcement additionally requires KIOSKX_REYEAH_DEVICE_SECRET to be set — a flag on its own must never start refusing the fleet, because a config mistake would then be a fleet-wide outage. Order of operations:

  1. Set KIOSKX_REYEAH_DEVICE_SECRET to the baked-in constant. Nothing changes yet; reports now simply get classified.
  2. Watch the log. Every credential-less report logs at WARNING with a stable prefix, carrying the machine, the client IP and a running count:

apkup unauthenticated: endpoint=apkUp machine=… ip=… count=… secretConfigured=True enforced=False

This line going quiet is the evidence — and the only evidence — that step 3 strands nobody. A cabinet that is unplugged this week is in nobody's column, so give it longer than feels necessary. 3. Set KIOSKX_REYEAH_DEVICE_AUTH_ENFORCED=true. Credential-less reports now get 401 and write nothing. The refusal is a real HTTP status rather than the protocol's code: 1 on a 200, so it shows up in an access log without parsing bodies; that is safe here specifically because the firmware's apkUpCallBack is an empty override and reads nothing from the envelope — unlike validVmcVersion, where a non-success answer means "flash the VMC". 4. Remove the fleet-secret arm only once cabinets present x-device-key. GET /api/v1/fleet/device-identity is where that is counted.

Rollback at any point is unsetting the flag.

One guard does not wait for any of this. A cabinet the ZHZN gateway has heard register is not writable through this endpoint at all: both fields the call would set steer that fleet's OTA (apkVersion is the number compared for both fleets, and a com.ruiye.jd versionAppId drops a machine out of every wave published for the ZHZN agent), so one call naming a ZHZN serial silenced that cabinet's /zhzn/upgrade while it still showed as a target of the wave — which reads to an operator as "the cabinet never asked" and sends somebody to the machine. A board that authenticated to /zhzn/* has already said what it is, and this endpoint has no standing to overrule it with a call that proved nothing.

No vendor-hosted URLs in device payloads

Every URL the device protocol serves is on infrastructure we control (regression-tested for getStyle chrome, getGoods product images, and getEquipment). Two legacy fields in the equipment payload deserve a note:

Device identity (multi-machine)

Every /apk/* call is keyed to the calling device. Identity is resolved from, in order: query param / body field / header (equipmentNo, machineNo, deviceNo, deviceId, sn, or X-Equipment-No), then the session cookie. getEquipment pins the resolved serial in a cookie (kx_equipment_no), so firmware that sends parameterless GETs after boot stays attributed to the right machine — the Reyeah build persists cookies across calls and reboots. Calls with no identity at all fall back to the demo machine 866903013700011 so the emulator and docs examples keep working.

Screensaver ads (operator-managed, S3)

getAd fetches the machine's approved ads from the Intelliverse ad-management system (GET /kiosk/screensaver-ads/{machineId} on the User Management API): operators upload video ads, admins approve them, the media lives on S3, and each machine gets its own list. Responses are cached for 60s. If a machine has no ads or the upstream is unreachable, the bundled default screensaver + banners are served — a kiosk never boots ad-less. Serial → Intelliverse machineId translation is configured with INTELLIVERSE_ADS_MACHINE_MAP (e.g. 866903013700011:24).

One cloud build — and migrating boards off the vendor cloud

The shipped APK (reyeah-vending-kioskx.apk, installs on the tablet as "Kiosk X") points UrlConfigString.baseUrl at api.kiosk-x.ai — our cloud implements the full device protocol, so the board boots to the product grid and every purchase flows straight into the operator app and the emulator. Registration order doesn't matter: an unregistered board is auto-provisioned on first contact and claimed later.

A board still running the factory firmware (pointed at the original vendor backend) migrates in three steps and never touches the vendor again:

  1. Installadb uninstall com.ruiye.jd && adb install -r reyeah-vending-kioskx.apk. The kiosk boots and auto-provisions itself against our cloud.
  2. Claim the board's serial — operator app → Machines → + Register (or POST /api/v1/machines/register).
  3. Verify — a test vend appears in the operator app under your fleet.

The APK is published to the downloads hub; rebuild it with scripts/build_vending_variants.sh in the vending-kiosk repo.

Payments: the Nayax reader is cloud-independent

The kiosk app never processes card data. A Nayax VPOS Touch reader sits on the machine's MDB bus: it authorizes the tap with Nayax's cloud, the VMC vends, and the reader captures the exact amount. Settlement is decided by the terminal → merchant-account binding inside Nayax, so repointing the APK at a different cloud changes nothing about who gets paid — the operator's Nayax account receives the funds either way. What the cloud receives is the transaction record (POST /api/v1/payments/nayax/webhook), which is how the sale appears in the operator app with card brand, last4, and settlement account. Full walkthrough: Payments & Nayax.