Device API

Endpoints for linked devices: scanners, tablets and till apps that verify customers' age at the point of sale.

Before you start

  1. A tenant account with at least one location.
  2. A linked device: the device runs the OAuth device flow and a tenant admin approves it. See Device authentication.

Every endpoint on this page takes a DPoP-bound device token: Authorization: DPoP <token> plus a DPoP proof header. The store scope set (devices:read employees:read transactions:write), the only scopes a device can hold and its default, covers all of them.

The /api/v1/* endpoints return Problem Details errors. The /api/devices/* endpoints use the legacy { error, code } shape.

Verify age

Devices use the same verification endpoint as service clients. Transactions are attributed to the calling device; a deviceId in the body is ignored.

The device itself

GET/api/v1/devices/me

Returns the calling device. Device tokens only: a Bearer token gets 401. Needs devices:read. Use data.id wherever an endpoint asks for the device's own {id}.

Headers

AuthorizationstringRequired

DPoP <access_token>

DPoPstringRequired

A fresh DPoP proof for this method and URL, with ath

Response

{
  "data": {
    "id": "k57a3m9qz4x8c1n2v6b0d5f7gh1j2k3l",
    "createdAt": "2026-09-29T10:15:00.000Z",
    "name": "Till 1, Laugavegur",
    "status": "active",
    "origin": "link",
    "lastActiveAt": 1790676900000,
    "updatedAt": 1790676900000
  }
}
GET/api/devices/validate

Checks that the device is still linked and active and returns it with its tenant. Call it when the app starts. POST works the same. Needs devices:read.

Headers

AuthorizationstringRequired

DPoP <access_token>

DPoPstringRequired

A fresh DPoP proof for this method and URL, with ath

Response

{
  "valid": true,
  "device": {
    "id": "k57a3m9qz4x8c1n2v6b0d5f7gh1j2k3l",
    "name": "Till 1, Laugavegur",
    "status": "active",
    "lastActiveAt": 1790676900000
  },
  "tenant": {
    "id": "jd7f2k9m4n6p8q1r3s5t7v9w2x4y6z8a",
    "name": "Verslunin ehf.",
    "slug": "verslunin",
    "status": "active"
  }
}

lastActiveAt is epoch milliseconds. Unset fields are left out; older devices may also carry locationName and createdAt (here also epoch milliseconds, unlike the v1 API). Errors come as { "valid": false, "error", "code" }: 401 UNAUTHORIZED, 403 FORBIDDEN, 403 TENANT_SUSPENDED, 404 DEVICE_NOT_FOUND, 404 TENANT_NOT_FOUND, 500 SERVER_ERROR. A device that has been disabled or revoked gets 401.

GET/api/devices/{id}/locations

Returns the locations the device is assigned to. {id} must be the calling device's own ID (403 DEVICE_MISMATCH otherwise). Needs devices:read.

Headers

AuthorizationstringRequired

DPoP <access_token>

DPoPstringRequired

A fresh DPoP proof for this method and URL, with ath

Response

{
  "locations": [
    {
      "id": "m29c4x7b1n5v8z3q6w0e2r4t6y8u1i3o",
      "name": "Laugavegur",
      "address": "Laugavegur 1",
      "city": "Reykjavík",
      "postcode": "101"
    }
  ]
}

address2 is included when set.

Employee lookup

For a manual verification, the employee enters their 6-digit access code on the device. Look it up to get the employeeId the verification needs.

POST/api/devices/employees

Finds an active employee in the device's tenant by access code. Needs employees:read. An unknown code is not an error: it returns 200 with found: false.

Headers

AuthorizationstringRequired

DPoP <access_token>

DPoPstringRequired

A fresh DPoP proof for this method and URL, with ath

Content-TypestringRequired

application/json

Request Body

accessCodestringRequired

Exactly 6 digits. Anything else gets 400 INVALID_REQUEST.

Examples

Not found

{
  "found": false
}

Response

{
  "found": true,
  "employee": {
    "id": "n83b5v2c7x9z1a4s6d8f0g3h5j7k9l2q",
    "name": "Anna Jónsdóttir"
  }
}

More endpoints for devices

Devices can also call these, with the scopes their token holds:

Removed: POST /api/devices/link

The old claim-code linking endpoints (/api/devices/link and /api/devices/link/claim) return 410 legacy_endpoint_removed. Link devices with the OAuth device flow instead.