eID verification

Verify a customer's age with their Icelandic electronic ID. Start a request, show the returned code to the customer, and poll until they have confirmed in their eID app or on their phone's SIM.

Flow

  1. POST /api/v1/verifications with "method": "eid". The eID Provider sends the customer a sign-in request, and the response gives you an id and a short code.
  2. Show the code to the customer. They should see the same code on their phone before they confirm.
  3. Poll GET /api/v1/verifications/{id} every 2 to 3 seconds until the status is success, rejected or timeout.

A request expires 5 minutes after it starts. The age check and the transaction happen on the poll that sees the customer's confirmation, so keep polling until you get a final status.

Both calls accept a service client (Bearer) or a linked device (DPoP plus a DPoP proof header, see Device authentication), with the transactions:write scope. The poll needs transactions:write too.

Note: The request is sent to the customer before deviceId and the caller type are checked. A bad deviceId (404 or 400) or a dashboard user token (403) is refused only after the customer has already been prompted. Make sure these are right before you start a request.

Step 1: start the request

POST/api/v1/verifications

Start an eID verification with "method": "eid". Returns the id to poll and the code to show the customer. Requires 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)

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 (the same id and code) back instead of a second prompt on the customer's phone. See Retrying safely

Request Body

methodstringRequired

"eid"

loginTypestringRequired

"app" (the customer confirms in their eID app) or "sim" (the customer confirms with the eID on their phone's SIM)

identifierstringRequired

Who to prompt: ASCII digits with optional spaces, "-", ".", "(", ")" and a leading "+" or "(+"; an Icelandic phone number (7 digits, optionally after 354, 00354 or +354) or a kennitala with a valid checksum; at most 20 characters (trimmed; at most 256 as sent; invisible characters are dropped, and Unicode spaces and dashes are read as " " and "-"). See Identifier format below. For loginType "app", the customer's kennitala (for example "1505901249"). For loginType "sim", their phone number (for example "7771234")

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. 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

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. Checked before the customer's phone is prompted

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": "eid",
    "loginType": "sim",
    "identifier": "7771234",
    "requiredAge": 20,
    "posReference": "till4-1234"
  }'

Response

{
  "id": "kcb4mk3xdgmxdndafcjn7k50xweckqrb",
  "status": "pending",
  "authId": "<the eID Provider's reference>",
  "code": "4821",
  "livemode": true
}

id is what you poll. code is shown to the customer. authId is the eID Provider's own reference for the sign-in; you don't need it to poll.

Identifier format

identifier is an Icelandic phone number or a kennitala. Read on its digits only, it must be one of:

  • A phone number: exactly 7 digits, optionally after the country code 354, 00354 or +354. Foreign numbers are refused.
  • A kennitala: exactly 10 digits with a valid check digit.

Send it as typed or pasted; formatting is allowed, as long as it follows these rules:

  • Digits are ASCII 0 to 9 only. Fullwidth digits (8885001) and Arabic-Indic digits (٨٨٨٥٠٠١) are refused.
  • Separators: space, -, ., ( and ), anywhere.
  • An optional + or (+ at the start, as in +354 888 5001 or (+354) 888 5001, only before 354. A + anywhere else, or before any other digits (+8885001), is refused.
  • At most 20 characters, after leading and trailing spaces are trimmed.
  • At most 256 characters as sent, before invisible characters are dropped.
  • Invisible characters that copied numbers often carry are dropped before the check: text-direction marks (U+061C, U+200E, U+200F, U+202A to U+202E, U+2066 to U+2069), zero-width space, non-joiner and joiner (U+200B to U+200D) and word joiner (U+2060).
  • Non-breaking and other Unicode spaces are read as a space, and Unicode dashes and the minus sign as -.

Any other character, such as / or ,, is refused. An identifier that breaks these rules gets 422 with type validation-error, and the customer is not prompted. For example, 888-5001, +354 888 5001, 3548885001, 003548885001, 010130-2989 and 010130.2989 are accepted; 12345678 (8 digits), 08885001, +8885001, +44 7911 123456, 1234567890 (bad check digit), 010130/2989, 888,5001 and 8885001+ are refused. The test eID identifiers are accepted too.

Step 2: poll for the result

GET/api/v1/verifications/{id}

Get a verification by id. For an eID request, each call checks with the eID Provider while the request is pending and returns its current status. For a transactionId from a pass or manual verification, it returns the recorded result. Callers: service client (Bearer) or 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 GET)

Query Parameters

id (path)stringRequired

The id from step 1, or a transactionId. 404 if it doesn't match a verification in your tenant, including a malformed id

Examples

cURL

curl https://abyrgverslun.is/api/v1/verifications/kcb4mk3xdgmxdndafcjn7k50xweckqrb \
  -H "Authorization: Bearer $ACCESS_TOKEN"

JavaScript: start and poll

const BASE = 'https://abyrgverslun.is';
const headers = {
  'Authorization': `Bearer ${accessToken}`,
  'Content-Type': 'application/json',
};

const start = await fetch(`${BASE}/api/v1/verifications`, {
  method: 'POST',
  headers,
  body: JSON.stringify({
    method: 'eid',
    loginType: 'sim',
    identifier: '7771234',
    requiredAge: 20,
  }),
});
if (!start.ok) throw new Error(`eID start failed: ${start.status}`);
const { id, code } = await start.json();

showCodeToCustomer(code);

const FINAL = ['success', 'rejected', 'timeout'];
let verification;
do {
  await new Promise((resolve) => setTimeout(resolve, 2500));
  const poll = await fetch(`${BASE}/api/v1/verifications/${id}`, { headers });
  if (!poll.ok) throw new Error(`eID poll failed: ${poll.status}`);
  verification = await poll.json();
} while (!FINAL.includes(verification.status));

if (verification.status === 'success' && verification.result === 'pass') {
  // Customer is at least requiredAge
} else {
  // Underage (success + fail), declined (rejected) or expired (timeout)
}

Response

// Customer has not answered yet: keep polling
{ "id": "kcb4mk3xdgmxdndafcjn7k50xweckqrb", "status": "pending", "livemode": true }

// Customer signed in and meets requiredAge
{
  "id": "kcb4mk3xdgmxdndafcjn7k50xweckqrb",
  "status": "success",
  "result": "pass",
  "transactionId": "k1jcce4akgxtehpqa9pz2809dnxpxhfq",
  "livemode": true
}

// Customer signed in but is under requiredAge
{
  "id": "kcb4mk3xdgmxdndafcjn7k50xweckqrb",
  "status": "success",
  "result": "fail",
  "transactionId": "k1jcce4akgxtehpqa9pz2809dnxpxhfq",
  "livemode": true
}

// Customer declined, or the sign-in failed: no transaction
{ "id": "kcb4mk3xdgmxdndafcjn7k50xweckqrb", "status": "rejected", "livemode": true }

// First poll after the 5-minute expiry: no transaction
{ "id": "kcb4mk3xdgmxdndafcjn7k50xweckqrb", "status": "timeout", "livemode": true }

// A pass or manual verification, looked up by transactionId
{
  "id": "k1jcce4akgxtehpqa9pz2809dnxpxhfq",
  "status": "completed",
  "result": "pass",
  "transactionId": "k1jcce4akgxtehpqa9pz2809dnxpxhfq",
  "livemode": true
}

Statuses

statusMeaningTransaction
pendingThe customer hasn't answered yet. Keep polling.Not yet
successThe customer signed in. This only means they authenticated: check result, which is fail when they are under requiredAge.Recorded, transactionId included
rejectedThe customer declined, or the eID Provider did not authorize the sign-in. Also returned on every poll after a timeout.None
timeoutThe request passed its 5-minute expiry while pending. Only the first poll after expiry returns timeout; later polls return rejected.None
failedClosed because your tenant was switched into or out of test mode while the request was pending. Start a new request.None
completedNot an eID status: returned when the id is the transactionId of a pass or manual verification.Already recorded

Once an eID request has a final status, later polls answer from the stored result without contacting the eID Provider again. The one change is expiry: the first poll after it returns timeout, and every poll after that returns rejected.

Retrying safely

Starting a request prompts the customer's phone, so retrying a start whose response you lost would prompt them again. Send an Idempotency-Key header (a fresh UUID per verification) and reuse it on every retry of that start:

  • A retry with the same key and the same body within 24 hours returns the original response (the same id and code) with the header Idempotent-Replayed: true. The phone is not prompted again. Poll the id for the current status.
  • A retry sent while the first request is still being started gets 409 with type idempotency-key-in-flight and is not run. Wait a moment and retry with the same key.
  • The same key with a different body gives 422 with type idempotency-key-reused.
  • A start that failed (for example a 500 from the eID Provider, or a 400) is not stored: a retry with the same key runs again.
  • Keys belong to the caller: your service client or device in your tenant. They work the same on a test tenant.

Errors

Both calls return Problem Details (application/problem+json). See Errors.

StatusWhen
400The body is not valid JSON, the Idempotency-Key header is not 1 to 255 printable ASCII characters, deviceId names a device that is not an active api device, or on a test tenant identifier is not a test eID identifier (see Test mode)
401Missing, invalid, expired or revoked token, or a bad DPoP proof
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 customer is not prompted
404Poll: the id doesn't match a verification in your tenant. Start: deviceId doesn't match a device in your tenant
409Type idempotency-key-in-flight: a start with this Idempotency-Key is still in progress. Retry shortly with the same key
422The body failed validation, for example a missing loginType or an identifier that breaks the Identifier format. Type idempotency-key-reused: the Idempotency-Key was already used with a different body
500The eID Provider could not be reached or refused the call, when starting or when polling. A failed start creates no request; a failed poll leaves the request pending, so you can poll again

Changes

  • 2026-10-14: identifier must be an Icelandic phone number or a kennitala (see Identifier format). 8–9 digit values, other lengths, a leading + without 354, and 10-digit values that fail the kennitala checksum are now refused with 400/422 before any eID prompt. Before, they were sent on to the eID Provider, which failed them after the customer had waited.
  • 2026-10-12: Invisible characters in identifier are now removed instead of refused: the Arabic letter mark (U+061C), zero-width space, non-joiner and joiner (U+200B to U+200D) and word joiner (U+2060). A number pasted with any of them is accepted, so you don't need to strip them yourself.
  • 2026-10-08: identifier must follow the Identifier format. Before, any 1 to 20 characters were sent on to the eID Provider. Now an identifier such as 010130/2989, 888,5001, one with a trailing + (8885001+), one with non-ASCII digits (8885001), letters, or fewer than 7 digits is refused with 422 validation-error, and the customer is not prompted. Remove these characters before you send the identifier.