Get started

Docs · API reference

Devices API

A credential for hardware you cannot lock in a rack: a till or tablet pairs itself with a typed code, then holds a short-lived token of its own that is narrower than any API key and revocable on its own.

Why not just use an API key?

An API key belongs on a server. A tablet on a bar counter is not a server — it is hardware in a public room that can be picked up — and a key on it would carry your whole account: every template, every session anyone has ever run, and the authority to erase them.

A device credential is the alternative. It is issued per tablet, holds a fixed and deliberately small set of powers, expires in minutes rather than never, and can be killed on its own without rotating anything else.

API keyDevice
LivesUntil revoked or expired15 minutes, refreshed
TemplatesEvery template you haveOnly those named at registration
Inline DCQLAllowedRefused
SeesEvery session in the accountOnly the sessions it created
Result detailClaims and issuer-signed credentialsClaims only
ErasureWith verifications:deleteNever

Pairing a device

Three steps, and only the first touches the back office.

POST/v1/devicesdevices:manage

Request
curl -X POST https://api.valitel.eu/v1/devices \
  -H "authorization: Bearer $VALITEL_API_KEY" \
  -H "content-type: application/json" \
  -d '{"name":"Bar 2 till","allowed_templates":["age_over_18"]}'
FieldTypeNotes
namestring, 1–64 charsThe label you will recognise on the device list, and the actor name in your audit trail.
allowed_templatesarray, 1–20 keysThe only templates this device may verify with. Non-empty: a device with no template could do nothing.
pairing_ttl_secondsinteger, 60–86400How long the pairing code stays claimable. Default 900 — raise it for a tablet you ship before anyone pairs it.
201 Created
{
  "device": { "id": "0f2b6d1c-…", "status": "pending", "…": "…" },
  "pairing_code": "VD-7K4M-QP2X"
}

Someone then types the code into the tablet, which claims it. This call carries no Authorization header, because the code is the credential:

POST/v1/devices/enrollno auth

Request
curl -X POST https://api.valitel.eu/v1/devices/enroll \
  -H "content-type: application/json" \
  -d '{"pairing_code":"vd7k4mqp2x"}'
200 OK
{
  "access_token": "vd_qN2xhVX0mA7Kd1jsPu4RbTfZ8cwLyE6g",
  "refresh_token": "vdr_5tBm9zKqW3nHxRc7Yv1UpLdA2fJs4Geo",
  "token_type": "Bearer",
  "expires_in": 900,
  "refresh_expires_in": 604800,
  "device": {
    "id": "0f2b6d1c-8a44-4d0e-9f61-3c2e5b7a1d90",
    "name": "Bar 2 till",
    "status": "active",
    "allowed_templates": ["age_over_18"],
    "created_at": "2026-08-21T18:02:11.004Z",
    "enrolled_at": "2026-08-21T18:04:37.512Z",
    "last_seen_at": "2026-08-21T18:04:37.512Z"
  }
}

Lowercase and missing dashes are forgiving — a human is retyping this off a screen. A misread character is not: the alphabet excludes 0 1 I L O U V so that confusing 0 with O produces a code that cannot exist rather than somebody else's.

Errors400 invalid_pairing_code 429 rate_limit_exceeded 500 internal_error

Pair a till with the free Valitel staff app

You do not have to build the tablet side yourself. The Valitel staff app for iOS and Android is free for Valitel customers and makes the device-side calls on this page for you: it claims a pairing code through POST /v1/devices/enroll, keeps its tokens fresh under the rules in Refreshing, and runs one check — the Age 18+ preset.

  1. Register the till with age_over_18 among its allowed templates, in either of two ways:
    • On the dashboard's Devices page, no API key needed: an admin or owner names the till, ticks age_over_18 and chooses Create pairing code. The dashboard asks for the password again first, and shows the code once.
    • From your own backend, with POST /v1/devices as above, using an API key minted with devices:manage on the dashboard's API keys page. The scope is never pre-ticked, so a key minted with the defaults cannot register a device.
  2. Open the staff app on the till and type the pairing code while it is still claimable — 15 minutes by default.

Search “Valitel” in the App Store or Google Play once a listing is live — the direct links are on the product page's staff app section for anyone browsing signed out. A signed-in reader of these docs will not see that section on /: it renders your live verification log instead.

Verifying from the device

From here the tablet is an ordinary API client: put its access_token in the same bearer header and call the verifications API as normal. The wallet cannot tell the difference, because there is none — a device's session is an ordinary session.

What differs is what the device may ask for, and what it gets back:

AttemptResult
A template outside allowed_templates403 template_not_allowed
An inline query403 raw_query_not_allowed
Reading a session it did not create404
Listing devices, or erasing a verification403

An allowlist over template keys would be worth nothing if the same request could be inlined as raw DCQL, which is why the second row exists.

A completed result read by a device carries claims and no credentials array:

200 OK
{
  "id": "vrf_G3mgaU2NnqZU-vNBd_jFAw",
  "status": "completed",
  "claims": { "age_over_18": true }
}

The till gets the decision, not the issuer-signed evidence: the per-credential block carries issuer identity, trust findings and proof metadata — a compliance surface for your backend, and pure excess disclosure on a screen in a bar. Read the same session with your API key and it is all there. A device seeing less is not data loss; it is the point.

Refreshing, and the one way to brick a till

POST/v1/devices/tokenno auth

Access tokens last 15 minutes. The refresh token lasts a week and is single-use — every exchange returns a whole new pair.

Request
curl -X POST https://api.valitel.eu/v1/devices/token \
  -H "content-type: application/json" \
  -d '{"refresh_token":"vdr_…"}'

Two rules follow, and they belong in your client rather than your runbook:

  1. Persist the new pair before discarding the old one. A crash between the two loses the device.
  2. Never blindly retry a request to this endpoint that timed out. You do not know whether the server rotated; retrying with the same token is exactly the replay that kills it. Treat an unseen response as "possibly rotated".

The previous access token is deliberately left alive until it expires on its own, so a device that refreshes early does not kill a request still in flight.

On a 401, branch on the code: device_token_expired means refresh and retry once; anything else means the credential is gone and the tablet needs re-pairing.

Errors400 invalid_refresh_token 429 rate_limit_exceeded 500 internal_error

Listing and revoking

GET/v1/devicesdevices:manage

Every device you have registered, { "data": [ … ] }, pending and revoked included. A device token gets 403 here — a tablet cannot enumerate its siblings.

DELETE/v1/devices/:iddevices:manage

Revokes a device. Its live access and refresh tokens stop working immediately — including a refresh it had already queued — and an unclaimed pairing code is destroyed with them. Nothing it verified is affected.

Returns 200 with the device row, and does so again on a second call, so a lost response is safe to retry. Revocation is terminal: a device that needs to come back is a new registration and a new pairing code.

Errors401 invalid_api_key 403 insufficient_scope 404 device_not_found 429 rate_limit_exceeded

Rate limits for a fleet

Two budgets apply, and neither is your API key's — see Rate limits for the general shape.

  • Per device. Each device gets its own bucket, so one busy till cannot spend your backend's allowance or the allowance of the till beside it.
  • Per address, shared. Enrolment and refresh share a tighter per-IP budget of their own — 20 a minute by default — because they are exactly what a brute force against the pairing-code alphabet would target.