Authentication

Service client authentication with the OAuth 2.0 client_credentials grant. Your server exchanges its client credentials for a short-lived Bearer token.

Which caller are you?

  • Service client (a server-side integration): this page. Sends Authorization: Bearer <token>.
  • Device (a scanner, tablet or till app): use the OAuth device flow with DPoP, described in Device authentication. Sends Authorization: DPoP <token> plus a DPoP proof header. A device token sent as Bearer is rejected with 401.

Each endpoint page lists which of the two it accepts and the scope it needs.

How it works

  1. Register a service client in the admin dashboard and receive a client_id and client_secret
  2. Get an access token — POST your client_id and client_secret to /api/oauth/token, then call any API endpoint using the token as a Bearer token

Register a service client

A tenant admin registers service clients through the admin dashboard. Navigate to Integrations → API Clients and click Create Client.

During registration you provide:

  • Name — a human-readable label for this integration
  • Description — optional description of the integration
  • Permissions — the scopes this client may request (see Available permissions)

The system generates a client_id (format: svc_<24 hex chars>) and a client_secret (format: scs_<48 hex chars>). Save the client secret immediately — it is a single-line string shown only once and cannot be retrieved again. The server stores only a SHA-256 hash of the secret.

Step 1 — Get an access token

POST your client_id and client_secret to the token endpoint to receive a short-lived access token. Use application/x-www-form-urlencoded encoding. A JSON body is rejected with 400 invalid_request.

scope is optional. Leave it out to get every scope the client is registered for, or pass a space-separated subset to get a narrower token.

Store your client secret as an environment variable — it is a single-line string that is natively .env-friendly:

CLIENT_ID='svc_3f9a1c7e5b2d8046a1e9c3b7'
CLIENT_SECRET='scs_a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6'

Avoid committing secrets to source control.

curl -s -X POST https://abyrgverslun.is/api/oauth/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=client_credentials\
&client_id=svc_3f9a1c7e5b2d8046a1e9c3b7\
&client_secret=scs_a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6\
&scope=devices:read transactions:read" | jq .

Response

POST/api/oauth/token

Exchange client credentials for an access token.

Headers

Content-TypestringRequired

application/x-www-form-urlencoded

Request Body

grant_typestringRequired

Must be client_credentials

client_idstringRequired

Your service client ID (svc_…)

client_secretstringRequired

Your client secret (scs_…)

scopestringOptional

Space-separated scopes to request; must be a subset of the client's registered scopes. Omit it to get all of them.

Response

{
  "access_token": "eyJhbGciOiJFUzI1NiIsInR5cCI6ImF0K2p3dCJ9...",
  "token_type": "Bearer",
  "expires_in": 300,
  "scope": "devices:read transactions:read"
}

Token errors

The token endpoint answers errors in the OAuth format (RFC 6749), not Problem Details. See Errors for all formats.

{
  "error": "invalid_scope",
  "error_description": "Requested scopes exceed registered: employees:write"
}
StatuserrorWhen
400invalid_requestgrant_type, client_id or client_secret is missing, or the body is not application/x-www-form-urlencoded (a JSON body reads as a missing grant_type)
400unsupported_grant_typegrant_type is not client_credentials, refresh_token or urn:ietf:params:oauth:grant-type:device_code
400invalid_scopescope asks for a scope the client is not registered for; error_description lists them
401invalid_clientunknown client_id, wrong secret, or the client is disabled or revoked. Sent with WWW-Authenticate and no error_description
401unauthorized_clientthe client's tenant is not active
429slow_downmore than 30 token requests in a minute for this client_id. Wait for the Retry-After header (seconds)
500server_errora problem on our side; retry with backoff

The 30-per-minute limit counts every request for a client_id, including failed ones. Cache your token and reuse it until it expires instead of requesting one per API call.

Step 2 — Use the access token

Include the token in the Authorization header of every API request. Tokens expire in 5 minutes (expires_in: 300). No refresh tokens are issued to service clients: when the token expires, request a new one with your client credentials.

The example below runs a pass verification. For the full request and response, see Verify Pass.

POST/api/v1/verifications

Example authenticated request: a pass verification. Needs the transactions:write scope. Both outcomes return HTTP 200 with result pass or fail.

Headers

AuthorizationstringRequired

Bearer <access_token>

Content-TypestringRequired

application/json

Request Body

methodstringRequired

"pass"

passDatastringRequired

The Pass Token read from the customer's wallet pass: 53 characters, the digit 2 followed by 52 Crockford Base32 characters

requiredAgeintegerRequired

Minimum age to check, 0-150 (for example, 18)

Examples

cURL

ACCESS_TOKEN="eyJhbGciOiJFUzI1NiIsInR5cCI6ImF0K2p3dCJ9..."

curl -s -X POST https://abyrgverslun.is/api/v1/verifications \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "method": "pass",
    "passData": "2QWPSZMFXGAGCH1NAJB0RMH298XB106N5E2D7WTW5M2H4XEHYZZ4H",
    "requiredAge": 18
  }' | jq .

Response

{
  "result": "pass",
  "transactionId": "k57a3m9qz4x8c1n2v6b0d5f7gh1j2k3l"
}

Available permissions

Request only the scopes your integration needs. A token without the scope an endpoint needs gets 403.

  • A :write scope covers create, update and delete, and the record a create or update returns. Listing or fetching records needs the :read scope.
  • Verifications (POST /api/v1/verifications and polling GET /api/v1/verifications/{id}) need transactions:write. Reading the transaction history needs transactions:read.
PermissionDescription
devices:readList and read device details
devices:writeCreate, update and delete devices
locations:readList and read locations
locations:writeCreate, update and delete locations
locations:deleteDelete locations, without create or update
employees:readList and read employees, look up an employee by access code
employees:writeCreate, update and delete employees
employees:deleteDelete employees, without create or update
transactions:readList and read transaction records
transactions:writeRun verifications (pass, manual, eID) and poll their result

Secret rotation

Rotate secrets without downtime using the Rotate Secret action on the client detail page in the dashboard. A new secret is generated automatically.

The old secret transitions to retiring status for a 24-hour grace window, during which both the old and new secrets are accepted. After the grace window the old secret is no longer valid. Update your CLIENT_SECRET environment variable during the grace window to avoid interruption.

Revoking tokens

An admin can invalidate all active tokens for a client with the Revoke All Tokens action in the dashboard. Every token issued before that moment is then rejected with 401. Disabling or revoking the client has the same effect on its tokens.

This is not strictly instant: each API server caches a client's status for about 5 seconds, so a revoked token can still work for a few seconds.