Errors

The API uses standard HTTP status codes. The error body has one of three shapes, depending on the endpoint family.

EndpointsError format
/api/v1/*RFC 7807 Problem Details
/api/oauth/*RFC 6749 OAuth errors
/api/devices/*Legacy { error, code }

/api/v1/*: Problem Details

Errors from the v1 API are RFC 7807 Problem Details with Content-Type: application/problem+json. The members are:

  • type: a stable URL, https://api.aldursvottun.is/errors/<slug>. Branch on this (or on status), not on the text.
  • title: usually the HTTP reason phrase, such as "Not Found". Some types have their own, such as "Organization Not Active" for tenant-inactive.
  • status: the HTTP status code.
  • detail: a human-readable explanation. Left out on 500 and 503.
  • instance: the request path, on errors raised before the endpoint runs (authentication and validation). Not always present.
  • traceId: our id for this request, the same value as the X-Trace-Id response header. On every Problem Details error.
  • Extension members on some errors: errors on 422; retryAfterSeconds and reason on 429; retryAfterSeconds on 503.
{
  "type": "https://api.aldursvottun.is/errors/unauthenticated",
  "title": "Unauthorized",
  "status": 401,
  "detail": "Invalid or expired access token",
  "instance": "/api/v1/devices",
  "traceId": "3f1c9a52-7d4e-4b0a-9c1e-2a6f8d0b5e17"
}

Trace ID

Every /api/v1/* response, success or error, carries an X-Trace-Id header (a UUID; browsers can read it, it is in Access-Control-Expose-Headers). Problem Details bodies repeat it as traceId. We generate it for each request; an X-Trace-Id or X-Request-Id header you send is ignored.

Log it with your own request. When you contact support about a failed request, especially a 500 or 503, quote the traceId: it is how we find that request in our logs.

Validation errors (422)

A 422 lists every failing field in errors, each with field (a dotted path, or (root)) and message. Query parameters are validated the same way.

{
  "type": "https://api.aldursvottun.is/errors/validation-error",
  "title": "Unprocessable Entity",
  "status": 422,
  "detail": "Request body failed validation",
  "instance": "/api/v1/verifications",
  "errors": [
    { "field": "requiredAge", "message": "Number must be less than or equal to 150" }
  ]
}

Order of checks. The body is parsed and validated before the token is checked, then the query parameters, then authentication. A malformed body gets 400 or 422 even without a valid token, so a 422 does not mean your credentials were accepted.

Status codes

The slug is the last part of type.

Statustype slugWhen
400bad-requestThe body is not valid JSON; an ID used as a query filter (deviceId, verifiedBy, locationId) is malformed; a locationId or locationIds entry in a body is not a live location in your tenant; a verification's deviceId names a device that is not an active api-origin device; or a verification's Idempotency-Key header is not 1 to 255 printable ASCII characters.
401unauthenticatedNo Authorization header, or an invalid, expired or revoked token. Also: a scheme the endpoint doesn't accept (Bearer on a device-only endpoint such as GET /api/v1/devices/me, DPoP on an endpoint devices can't call), a device token sent as Bearer, a bad or reused DPoP proof, or a disabled or revoked device or service client.
403forbiddenThe token lacks the scope the endpoint needs, the caller type is not allowed on this endpoint (for example a user token on GET /api/v1/employees/code), or the resource belongs to another tenant.
403tenant-inactiveYour organization is suspended or not active. Title "Organization Not Active", the same for device and service-client callers. Nothing is done; retrying won't help until the organization is active again.
403verifier-not-enrolledPOST /api/v1/verifications: the employee in verifiedBy has not completed Employee Enrollment. Enroll them with POST /api/v1/employees/{id}/enrollment and try again. Nothing is recorded and the pass is not used.
404not-foundThe ID in the path is unknown, malformed or already deleted. Deleting the same resource twice gives 404 the second time. An unknown employeeId in a manual verification, or an unknown deviceId in a verification body, is also 404.
409conflictA duplicate: an employee with the same SSN or access code already exists in your tenant (deleted ones included), or a live employee, location or device already has this externalId (deleting one frees its externalId). Also: starting enrollment for an employee who is already enrolled.
409idempotency-key-in-flightPOST /api/v1/verifications with method eid: an eID start with this Idempotency-Key is still in progress. It is not run, so the customer's phone is not prompted twice. Retry shortly with the same key to get the original response.
409enrollment-identity-in-usePOST /api/v1/employees/{id}/enrollment: the employee was entered with a kennitala whose person is already another employee of your tenant. One person is at most one employee. No eID request is sent.
422validation-errorThe body or the query parameters failed validation. The errors array names each field.
422idempotency-key-reusedPOST /api/v1/verifications: the Idempotency-Key was already used by this client in the last 24 hours with a different body. Nothing is recorded. Use a new key for a new verification.
429rate-limitedOver a rate limit. Wait Retry-After seconds. A pass verification over its own limits carries livemode like every verification answer. The per-client limit on all v1 endpoints carries "limit": "client" and no livemode, since it is checked before the endpoint runs. A device refused for a missing scope is counted too, so it gets this 429 instead of 403 once over the limit.
500internal-errorA problem on our side, or an upstream failure such as the eID Provider being unavailable. The body carries no detail. Retry with backoff.
503service-unavailableWe could not check your organization's status just now. A temporary problem on our side; nothing is done. Retry after the Retry-After header (seconds, also in retryAfterSeconds).

A verification that runs but doesn't pass is not an error: it returns 200 with "result": "fail". See Verification. The 429 body is shown on Rate limits.

/api/oauth/*: OAuth errors

POST /api/oauth/token and POST /api/oauth/device_authorization answer in the OAuth 2.0 format (RFC 6749 §5.2), with Content-Type: application/json. error is always present; error_description is sometimes present. Device code polls leave out error_description, and authorization_pending carries the poll interval in seconds.

{
  "error": "invalid_request",
  "error_description": "Missing grant_type (the body must be application/x-www-form-urlencoded)"
}
StatuserrorWhen
400invalid_requestA required parameter is missing, or the body has the wrong format: the token endpoint takes application/x-www-form-urlencoded, device_authorization takes JSON.
400unsupported_grant_typegrant_type is not client_credentials, refresh_token or urn:ietf:params:oauth:grant-type:device_code.
400invalid_scopeclient_credentials: a requested scope is not registered for the client. Device flow: a requested scope isn't an API scope (wildcards and dashboard permissions are refused).
401invalid_clientclient_credentials: unknown client, wrong secret, or client not active (sent with WWW-Authenticate). Device flow: client_id is not abyrg_device.
401unauthorized_clientclient_credentials: the client's tenant is not active.
429slow_downclient_credentials: more than 30 requests a minute for one client_id. Wait Retry-After seconds.
400authorization_pendingDevice code poll: no admin has approved the device yet. Keep polling every interval seconds.
400expired_tokenDevice code poll: the device code is older than 10 minutes. Start the flow again.
400access_deniedDevice code poll: an admin denied the request.
400 / 401invalid_grantThe device code or refresh token is unknown, already used, expired, or its device is no longer active. 401 on a device code poll, 400 on a refresh.
400 / 401invalid_dpop_proofThe DPoP header is missing (400) or the proof fails verification: 400 on device_authorization and refresh, 401 on a device code poll.
400 / 401invalid_dpop_bindingThe proof is signed with a different key from the one the device code or refresh token is bound to. 401 on a device code poll, 400 on a refresh.
500server_errorA problem on our side. Retry with backoff.

The token grants are described in Authentication and Device authentication.

/api/devices/*: legacy format

The device endpoints outside /api/v1 return { error, code }. error is a human-readable message; branch on code. /api/devices/validate adds "valid": false.

{
  "valid": false,
  "error": "Organization is suspended",
  "code": "TENANT_SUSPENDED"
}
StatuscodeWhen
400INVALID_REQUESTPOST /api/devices/employees: accessCode is not exactly 6 digits. The body also carries a details array.
400PIM_NOT_ENABLEDGET /api/devices/{id}/pim/key: PIM is off for this device or tenant.
400NO_API_KEYGET /api/devices/{id}/pim/key: the tenant has no PIM key stored.
401UNAUTHORIZEDMissing or invalid DPoP credentials, a reused proof, or a device that is no longer active.
403FORBIDDENThe device token lacks the scope the endpoint needs.
403TENANT_INACTIVEThe device's organization is suspended or not active.
403DEVICE_MISMATCHThe {id} in the path is not the calling device's own ID.
403TENANT_SUSPENDEDGET|POST /api/devices/validate: the tenant is suspended.
404DEVICE_NOT_FOUND / TENANT_NOT_FOUNDGET|POST /api/devices/validate: the device or its tenant no longer exists.
500SERVER_ERRORA problem on our side. Retry with backoff.

Removed and unknown endpoints

  • /api/devices/link and /api/devices/link/claim return 410 Gone with { "error": "legacy_endpoint_removed", "message": "..." }. Use the device flow instead.
  • A path under /api/ that doesn't exist returns 404 with { "error": "Not found" }.