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
- The call is counted against the rate limit before the token is read, so refused and invalid tokens count too.
- A value that is not a Pass Token is refused as
invalid. No transaction is recorded. - 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. - An expired pass, or a previous token past its Overlap, gives
expired. - A token that was already used gives
consumed. - 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.
/api/v1/verificationsVerify 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
| Name | Type | Required | Description |
|---|---|---|---|
| Authorization | string | Required | Bearer <access_token> (service client) or DPoP <access_token> (linked device). Requires the transactions:write scope |
| DPoP | string | Optional | Device callers only: a DPoP proof for this request (htm POST, htu https://abyrgverslun.is/api/v1/verifications, ath). See Device authentication |
| Content-Type | string | Required | application/json |
| Idempotency-Key | string | Optional | 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 |
AuthorizationstringRequiredBearer <access_token> (service client) or DPoP <access_token> (linked device). Requires the transactions:write scope
DPoPstringOptionalDevice callers only: a DPoP proof for this request (htm POST, htu https://abyrgverslun.is/api/v1/verifications, ath). See Device authentication
Content-TypestringRequiredapplication/json
Idempotency-KeystringOptionalA 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
| Name | Type | Required | Description |
|---|---|---|---|
| method | string | Required | "pass" |
| passData | string | Required | 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 |
| requiredAge | integer | Required | Minimum age to verify, 0 to 150 (for example, 20) |
| posReference | string | Optional | Your own reference, such as a receipt or basket ID, 1 to 20 characters. Stored on the transaction |
| deviceId | string | Optional | 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 |
| verifiedBy | string | Optional | 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 |
methodstringRequired"pass"
passDatastringRequiredThe 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
requiredAgeintegerRequiredMinimum age to verify, 0 to 150 (for example, 20)
posReferencestringOptionalYour own reference, such as a receipt or basket ID, 1 to 20 characters. Stored on the transaction
deviceIdstringOptionalService 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
verifiedBystringOptionalThe 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.
| reason | Meaning | Transaction |
|---|---|---|
| invalid | The value is not a Pass Token (wrong length, characters or version). Usually a misread or a different barcode. | None, no transactionId |
| unknown | No active pass has this token: never issued, revoked, or not yet delivered. The customer may need to reinstall the pass. | Recorded as fail |
| expired | The pass has expired, or this is an old token past its Overlap. The customer should refresh the pass in their wallet. | Recorded as fail |
| consumed | The 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.
| Status | When |
|---|---|
| 400 | The 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 |
| 401 | Missing, invalid, expired or revoked token, a bad DPoP proof, or a device token sent as Bearer |
| 403 | The 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 |
| 404 | deviceId doesn't match a device in your tenant |
| 422 | The 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 |
| 429 | The 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
422with typeidempotency-key-reused. Use a new key for a new verification. - Responses that recorded nothing (
invalid, a429or 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.