Errors
The API uses standard HTTP status codes. The error body has one of three shapes, depending on the endpoint family.
| Endpoints | Error 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 onstatus), not on the text.title: usually the HTTP reason phrase, such as"Not Found". Some types have their own, such as"Organization Not Active"fortenant-inactive.status: the HTTP status code.detail: a human-readable explanation. Left out on500and503.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 theX-Trace-Idresponse header. On every Problem Details error.- Extension members on some errors:
errorson422;retryAfterSecondsandreasonon429;retryAfterSecondson503.
{
"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.
| Status | type slug | When |
|---|---|---|
| 400 | bad-request | The 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. |
| 401 | unauthenticated | No 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. |
| 403 | forbidden | The 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. |
| 403 | tenant-inactive | Your 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. |
| 403 | verifier-not-enrolled | POST /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. |
| 404 | not-found | The 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. |
| 409 | conflict | A 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. |
| 409 | idempotency-key-in-flight | POST /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. |
| 409 | enrollment-identity-in-use | POST /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. |
| 422 | validation-error | The body or the query parameters failed validation. The errors array names each field. |
| 422 | idempotency-key-reused | POST /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. |
| 429 | rate-limited | Over 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. |
| 500 | internal-error | A problem on our side, or an upstream failure such as the eID Provider being unavailable. The body carries no detail. Retry with backoff. |
| 503 | service-unavailable | We 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)"
}| Status | error | When |
|---|---|---|
| 400 | invalid_request | A required parameter is missing, or the body has the wrong format: the token endpoint takes application/x-www-form-urlencoded, device_authorization takes JSON. |
| 400 | unsupported_grant_type | grant_type is not client_credentials, refresh_token or urn:ietf:params:oauth:grant-type:device_code. |
| 400 | invalid_scope | client_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). |
| 401 | invalid_client | client_credentials: unknown client, wrong secret, or client not active (sent with WWW-Authenticate). Device flow: client_id is not abyrg_device. |
| 401 | unauthorized_client | client_credentials: the client's tenant is not active. |
| 429 | slow_down | client_credentials: more than 30 requests a minute for one client_id. Wait Retry-After seconds. |
| 400 | authorization_pending | Device code poll: no admin has approved the device yet. Keep polling every interval seconds. |
| 400 | expired_token | Device code poll: the device code is older than 10 minutes. Start the flow again. |
| 400 | access_denied | Device code poll: an admin denied the request. |
| 400 / 401 | invalid_grant | The 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 / 401 | invalid_dpop_proof | The DPoP header is missing (400) or the proof fails verification: 400 on device_authorization and refresh, 401 on a device code poll. |
| 400 / 401 | invalid_dpop_binding | The 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. |
| 500 | server_error | A 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"
}| Status | code | When |
|---|---|---|
| 400 | INVALID_REQUEST | POST /api/devices/employees: accessCode is not exactly 6 digits. The body also carries a details array. |
| 400 | PIM_NOT_ENABLED | GET /api/devices/{id}/pim/key: PIM is off for this device or tenant. |
| 400 | NO_API_KEY | GET /api/devices/{id}/pim/key: the tenant has no PIM key stored. |
| 401 | UNAUTHORIZED | Missing or invalid DPoP credentials, a reused proof, or a device that is no longer active. |
| 403 | FORBIDDEN | The device token lacks the scope the endpoint needs. |
| 403 | TENANT_INACTIVE | The device's organization is suspended or not active. |
| 403 | DEVICE_MISMATCH | The {id} in the path is not the calling device's own ID. |
| 403 | TENANT_SUSPENDED | GET|POST /api/devices/validate: the tenant is suspended. |
| 404 | DEVICE_NOT_FOUND / TENANT_NOT_FOUND | GET|POST /api/devices/validate: the device or its tenant no longer exists. |
| 500 | SERVER_ERROR | A problem on our side. Retry with backoff. |
Removed and unknown endpoints
/api/devices/linkand/api/devices/link/claimreturn410 Gonewith{ "error": "legacy_endpoint_removed", "message": "..." }. Use the device flow instead.- A path under
/api/that doesn't exist returns404with{ "error": "Not found" }.