Pass verification

Send the Pass Token read from a customer's wallet pass. The server looks the token up, checks the pass and the age requirement, records a transaction and gives the customer's wallet a fresh token.

The Pass Token

The QR code or NFC payload on the pass is an opaque Pass Token: 53 characters, the version character 2 followed by 52 Crockford Base32 characters (0-9 and A-Z without I, L, O and U). It carries no personal data and no signature to check on your side. Send it to the API exactly as read.

A token is single-use. A successful lookup consumes it, and the server pushes a fresh token to the customer's wallet. The consumed token stays valid for one more presentation during a 1-hour Overlap, which covers a phone that has not yet received the new token. A later presentation returns "reason": "consumed", and one after the Overlap ends returns "reason": "expired". A daily job also gives every pass older than 7 days a fresh token, and the old token then stays valid for 24 hours.

What the server checks

  1. The call is counted against the rate limit before the token is read, so refused and invalid tokens count too.
  2. A value that is not a Pass Token is refused as invalid. No transaction is recorded.
  3. The token's hash is looked up: the pass's current token, or its previous token within the Overlap. No match, or a pass that is revoked or not yet delivered, gives unknown.
  4. An expired pass, or a previous token past its Overlap, gives expired.
  5. A token that was already used gives consumed.
  6. Otherwise the pass's age tiers are checked against requiredAge, and the token is consumed whatever the result. Consuming the current token pushes a fresh one to the wallet.
POST/api/v1/verifications

Verify a Pass Token with "method": "pass". Both outcomes return HTTP 200 with "result": "pass" or "fail"; HTTP error codes mean the request itself was refused. Callers: a service client (Bearer) or a linked device (DPoP), with the transactions:write scope.

Headers

AuthorizationstringRequired

Bearer <access_token> (service client) or DPoP <access_token> (linked device). Requires the transactions:write scope

DPoPstringOptional

Device callers only: a DPoP proof for this request (htm POST, htu https://abyrgverslun.is/api/v1/verifications, ath). See Device authentication

Content-TypestringRequired

application/json

Idempotency-KeystringOptional

A unique key per verification, such as a UUID (1 to 255 printable ASCII characters). Send the same key when retrying, and you get the original response back instead of a second verification. See Retrying safely

Request Body

methodstringRequired

"pass"

passDatastringRequired

The Pass Token read from the pass (53 characters: 2 + 52 Crockford Base32). Must be a non-empty string; any other string is answered with reason invalid

requiredAgeintegerRequired

Minimum age to verify, 0 to 150 (for example, 20)

posReferencestringOptional

Your own reference, such as a receipt or basket ID, 1 to 20 characters. Stored on the transaction

deviceIdstringOptional

Service clients only: attribute the transaction to an active device of origin api in your tenant (created with POST /api/v1/devices). 404 if the device doesn't exist, 400 if it isn't an active api device. Ignored for device callers, which are always recorded as themselves. Also used as the device key for the rate limit

verifiedBystringOptional

The Verifier: ID of the employee performing the verification, recorded on the transaction. Must be an employee in your tenant (400 otherwise). Becomes required, and the employee must have completed Employee Enrollment (403 verifier-not-enrolled otherwise), once enrollment is enforced. See Employees

Examples

cURL (service client)

curl -X POST https://abyrgverslun.is/api/v1/verifications \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "method": "pass",
    "passData": "22V6MKRDAVZDP0Q9K0ACD7DSB5JAKZ5R1G86M60YKWPH0G2RHQJZS",
    "requiredAge": 20,
    "posReference": "till4-1234"
  }'

cURL (linked device)

# $DPOP_PROOF is a fresh proof for POST https://abyrgverslun.is/api/v1/verifications
curl -X POST https://abyrgverslun.is/api/v1/verifications \
  -H "Authorization: DPoP $ACCESS_TOKEN" \
  -H "DPoP: $DPOP_PROOF" \
  -H "Content-Type: application/json" \
  -d '{
    "method": "pass",
    "passData": "22V6MKRDAVZDP0Q9K0ACD7DSB5JAKZ5R1G86M60YKWPH0G2RHQJZS",
    "requiredAge": 20
  }'

JavaScript

const response = await fetch('https://abyrgverslun.is/api/v1/verifications', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${accessToken}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    method: 'pass',
    passData: scannedValue, // exactly as read from the QR code or NFC
    requiredAge: 20,
  }),
});

if (response.status === 429) {
  const retryAfter = Number(response.headers.get('Retry-After'));
  // Wait retryAfter seconds before trying again
} else if (response.ok) {
  const { result, reason, transactionId } = await response.json();
  // result: 'pass' | 'fail'
  // reason (fail only): 'invalid' | 'unknown' | 'expired' | 'consumed'
}

Response

// Age requirement met
{
  "result": "pass",
  "transactionId": "k1jcce4akgxtehpqa9pz2809dnxpxhfq",
  "livemode": true
}

// Valid pass, but the customer is under requiredAge (no reason)
{
  "result": "fail",
  "transactionId": "k1jcce4akgxtehpqa9pz2809dnxpxhfq",
  "livemode": true
}

// Token refused
{
  "result": "fail",
  "transactionId": "k1jcce4akgxtehpqa9pz2809dnxpxhfq",
  "reason": "consumed",
  "livemode": true
}

// Not a Pass Token: no transaction is recorded
{
  "result": "fail",
  "reason": "invalid",
  "livemode": true
}

Fail reasons

reason is only present when the token itself was refused. An underage customer gets "result": "fail" with no reason.

reasonMeaningTransaction
invalidThe value is not a Pass Token (wrong length, characters or version). Usually a misread or a different barcode.None, no transactionId
unknownNo active pass has this token: never issued, revoked, or not yet delivered. The customer may need to reinstall the pass.Recorded as fail
expiredThe pass has expired, or this is an old token past its Overlap. The customer should refresh the pass in their wallet.Recorded as fail
consumedThe token has already been used. The customer should wait for the wallet to show the new token, or use another method.Recorded as fail

Errors

Errors use Problem Details (application/problem+json). See Errors for the full format.

StatusWhen
400The body is not valid JSON, deviceId names a device that is not an active api device, verifiedBy is not an employee in your tenant (or is missing, once enrollment is enforced), or the Idempotency-Key header is empty, longer than 255 characters or not printable ASCII
401Missing, invalid, expired or revoked token, a bad DPoP proof, or a device token sent as Bearer
403The token lacks transactions:write, or it is a dashboard user token. Type verifier-not-enrolled: the verifiedBy employee has not completed Employee Enrollment; the pass is not used
404deviceId doesn't match a device in your tenant
422The body failed validation, for example a missing method, an empty passData, or a requiredAge that is not an integer from 0 to 150. Validation runs before authentication. Also idempotency-key-reused: the Idempotency-Key was already used with a different body
429The device or tenant is over its pass verification limit. No transaction is recorded

A 429 carries a Retry-After header in seconds, and the body repeats it as retryAfterSeconds with "reason": "rate_limited". The limits are on the Rate limits page.

HTTP/1.1 429 Too Many Requests
Content-Type: application/problem+json
Retry-After: 12

{
  "retryAfterSeconds": 12,
  "reason": "rate_limited",
  "livemode": true,
  "type": "https://api.aldursvottun.is/errors/rate-limited",
  "title": "Too Many Requests",
  "status": 429,
  "detail": "Rate limit exceeded, retry after 12 second(s)"
}

Retrying safely

A verification consumes the Pass Token, so retrying one whose response you lost would answer consumed. Send an Idempotency-Key header (a fresh UUID per verification) and reuse it on every retry of that verification:

  • A retry with the same key and the same body within 24 hours returns the original response with the header Idempotent-Replayed: true. Nothing is consumed or recorded again, and it does not count toward the rate limit.
  • Keys belong to the caller: your service client or device in your tenant. Another caller's key never returns your result.
  • The same key with a different body gives 422 with type idempotency-key-reused. Use a new key for a new verification.
  • Responses that recorded nothing (invalid, a 429 or another error) are not stored. A retry with the same key runs again.
  • Duplicate requests sent at the same time are safe too: one runs and the other gets its result.

Without a key, recover a lost response by looking it up with GET /api/v1/transactions?posReference=.

Looking up a result later

GET /api/v1/verifications/{transactionId} returns { "id", "status": "completed", "result", "transactionId" } for a recorded pass verification. It takes the same callers and scope as the POST. See eID verification for the endpoint.