Authentication
Service Client Authentication using OAuth 2.0 Client Credentials with client_secret. Authenticate by exchanging your client credentials for a short-lived access token.
How it works
- Register a service client in the admin dashboard and receive a
client_idandclient_secret - Get an access token — POST your
client_idandclient_secretto/api/oauth/token, then call any API endpoint using the token as a Bearer token
Register a service client
A tenant admin registers service clients through the admin dashboard. Navigate to Integrations → API Clients and click Create Client.
During registration you provide:
- Name — a human-readable label for this integration
- Description — optional description of the integration
- Permissions — the scopes this client may request
The system generates a client_id (format: svc_<hex>) and a client_secret (format: scs_<48 hex chars>). Save the client secret immediately — it is a single-line string shown only once and cannot be retrieved again. The server stores only a SHA-256 hash of the secret.
Step 1 — Get an access token
POST your client_id and client_secret to the token endpoint to receive a short-lived access token. Use application/x-www-form-urlencoded encoding (not JSON).
Store your client secret as an environment variable — it is a single-line string that is natively .env-friendly:
CLIENT_ID='svc_a1b2c3d4e5f6' CLIENT_SECRET='scs_a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6'
Avoid committing secrets to source control.
curl -s -X POST https://abyrgverslun.is/api/oauth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials\
&client_id=svc_a1b2c3d4e5f6\
&client_secret=scs_a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6\
&scope=devices:read transactions:read" | jq .const CLIENT_ID = process.env.CLIENT_ID!;
const CLIENT_SECRET = process.env.CLIENT_SECRET!;
const TOKEN_ENDPOINT = "https://abyrgverslun.is/api/oauth/token";
async function getAccessToken(): Promise<string> {
const res = await fetch(TOKEN_ENDPOINT, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
grant_type: "client_credentials",
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET,
scope: "devices:read transactions:read",
}),
});
if (!res.ok) {
const error = await res.json();
throw new Error(`Token exchange failed: ${error.error}`);
}
const { access_token } = await res.json();
return access_token;
}
// Usage
const token = await getAccessToken();
const devices = await fetch("https://abyrgverslun.is/api/v1/devices", {
headers: { Authorization: `Bearer ${token}` },
}).then((r) => r.json());import os
import requests
CLIENT_ID = os.environ["CLIENT_ID"]
CLIENT_SECRET = os.environ["CLIENT_SECRET"]
TOKEN_ENDPOINT = "https://abyrgverslun.is/api/oauth/token"
def get_access_token() -> str:
res = requests.post(
TOKEN_ENDPOINT,
data={
"grant_type": "client_credentials",
"client_id": CLIENT_ID,
"client_secret": CLIENT_SECRET,
"scope": "devices:read transactions:read",
},
)
res.raise_for_status()
return res.json()["access_token"]
# Usage
token = get_access_token()
devices = requests.get(
"https://abyrgverslun.is/api/v1/devices",
headers={"Authorization": f"Bearer {token}"},
).json()var clientId = Environment.GetEnvironmentVariable("CLIENT_ID")!;
var clientSecret = Environment.GetEnvironmentVariable("CLIENT_SECRET")!;
const string TokenEndpoint = "https://abyrgverslun.is/api/oauth/token";
async Task<string> GetAccessTokenAsync()
{
using var http = new HttpClient();
var res = await http.PostAsync(TokenEndpoint, new FormUrlEncodedContent(new Dictionary<string, string>
{
["grant_type"] = "client_credentials",
["client_id"] = clientId,
["client_secret"] = clientSecret,
["scope"] = "devices:read transactions:read",
}));
res.EnsureSuccessStatusCode();
var json = await res.Content.ReadFromJsonAsync<JsonElement>();
return json.GetProperty("access_token").GetString()!;
}
// Usage
var token = await GetAccessTokenAsync();
using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization =
new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
var devices = await http.GetFromJsonAsync<JsonElement>(
"https://abyrgverslun.is/api/v1/devices");Response
/api/oauth/tokenExchange client credentials for an access token.
Headers
| Name | Type | Required | Description |
|---|---|---|---|
| Content-Type | string | Required | application/x-www-form-urlencoded |
Content-TypestringRequiredapplication/x-www-form-urlencoded
Request Body
| Name | Type | Required | Description |
|---|---|---|---|
| grant_type | string | Required | Must be client_credentials |
| client_id | string | Required | Your service client ID (svc_xxx) |
| client_secret | string | Required | Your client secret (scs_xxx) |
| scope | string | Optional | Space-separated list of requested scopes — must be a subset of registered scopes |
grant_typestringRequiredMust be client_credentials
client_idstringRequiredYour service client ID (svc_xxx)
client_secretstringRequiredYour client secret (scs_xxx)
scopestringOptionalSpace-separated list of requested scopes — must be a subset of registered scopes
Response
{
"access_token": "eyJhbGciOiJFUzI1NiIsInR5cCI6ImF0K2p3dCJ9...",
"token_type": "Bearer",
"expires_in": 300,
"scope": "devices:read transactions:read"
}Step 2 — Use the access token
Include the token in the Authorization header of every API request. Tokens expire in 5 minutes. No refresh tokens are issued — when it expires, request a fresh token with your client credentials.
/api/verification/passExample authenticated request for age verification. Verification outcomes return HTTP 200 with result pass/fail.
Headers
| Name | Type | Required | Description |
|---|---|---|---|
| Authorization | string | Required | Bearer <access_token> |
| Content-Type | string | Required | application/json |
AuthorizationstringRequiredBearer <access_token>
Content-TypestringRequiredapplication/json
Request Body
| Name | Type | Required | Description |
|---|---|---|---|
| passData | string | Required | Base64-encoded 64-byte pass (legacy 64-char Latin-1 also accepted) |
| requiredAge | number | Required | Minimum age threshold to check (for example, 18) |
passDatastringRequiredBase64-encoded 64-byte pass (legacy 64-char Latin-1 also accepted)
requiredAgenumberRequiredMinimum age threshold to check (for example, 18)
Examples
cURL
ACCESS_TOKEN="eyJhbGciOiJFUzI1NiIsInR5cCI6ImF0K2p3dCJ9..."
curl -s -X POST https://abyrgverslun.is/api/verification/pass \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"passData": "<base64-or-latin1-pass>",
"requiredAge": 18
}' | jq .Response
{
"result": "pass",
"transactionId": "j57b2mNkR4e9..."
}Available permissions
Request only the scopes your integration needs. All resources support both read and write operations.
| Permission | Description |
|---|---|
| devices:read | List and read device details |
| devices:write | Create, update, and delete devices |
| locations:read | Read location information |
| locations:write | Create, update, and delete locations |
| employees:read | Read employee records |
| employees:write | Create, update, and delete employee records |
| transactions:read | Read verification transaction records (read-only) |
| transactions:write | Create verification transactions (age verification) |
Secret rotation
Rotate secrets without downtime using the Rotate Secret action on the client detail page in the dashboard. A new secret is generated automatically.
The old secret transitions to retiring status for a 24-hour grace window, during which both the old and new secrets are accepted. After the grace window the old secret is no longer valid. Update your CLIENT_SECRET environment variable during the grace window to avoid interruption.
Instant revocation
An admin can instantly invalidate all active tokens for a client via the Revoke All Tokens action in the dashboard. This sets tokensInvalidBefore to the current timestamp. Any token issued before that timestamp is immediately rejected on next use — no token blacklist required.