Skip to content

Passkey / WebAuthn API

The Passkey / WebAuthn API exposes FIDO2-compliant registration and authentication flows and provides admin-level credential management. All WebAuthn ceremony endpoints follow the W3C WebAuthn Level 2 specification and are compatible with platform authenticators (Face ID, Touch ID, Windows Hello) and roaming authenticators (FIDO2 security keys).


WebAuthn authentication is a two-step ceremony. Each step consists of a begin request that returns a challenge options object and a complete request that submits the authenticator’s signed response.

Registration ceremony — enroll a new passkey for a user:

  1. POST /api/auth/passkey/register/begin — server generates a challenge and returns PublicKeyCredentialCreationOptions
  2. POST /api/auth/passkey/register/complete — client submits the signed attestation; server stores the credential

Authentication ceremony — sign in with an existing passkey:

  1. POST /api/auth/passkey/authenticate/begin — server generates a challenge and returns PublicKeyCredentialRequestOptions
  2. POST /api/auth/passkey/authenticate/complete — client submits the signed assertion; server returns a session token

Admin endpoints (/api/v1/admin/passkeys/) require an admin-scoped API key:

Authorization: Bearer arb_live_your-api-key-here

Self-service endpoints (/api/auth/passkey/) require an authenticated user session. Pass the session token returned from a prior authentication:

Authorization: Bearer arb_session_...

Requests with missing or invalid credentials return 401 Unauthorized. Requests with valid credentials that lack the required scope return 403 Forbidden.


Method Path Description
POST /api/auth/passkey/register/begin Start passkey registration
POST /api/auth/passkey/register/complete Complete passkey registration
POST /api/auth/passkey/authenticate/begin Start passkey authentication
POST /api/auth/passkey/authenticate/complete Complete passkey authentication
GET /api/auth/passkey/credentials List my passkeys (self-service)
DELETE /api/auth/passkey/credentials/{credential_id} Remove own passkey (self-service)
GET /api/v1/admin/passkeys/ List all passkeys (admin)
GET /api/v1/admin/passkeys/{credential_id} Get passkey detail (admin)
DELETE /api/v1/admin/passkeys/{credential_id} Revoke a passkey (admin)

POST /api/auth/passkey/register/begin

Generates a registration challenge. The response is a standard PublicKeyCredentialCreationOptions object that must be passed directly to navigator.credentials.create() in the browser or the platform authenticator SDK.

When called by an admin on behalf of another user, include user_id in the request body. When called in a self-service context, omit the body or send an empty object — the server resolves the user from the session.

Request body

Field Type Required Description
user_id string (UUID) No Target user ID. Admin use only. Omit for self-service registration.

Request

Terminal window
curl -X POST "https://api.arbitex.ai/api/auth/passkey/register/begin" \
-H "Authorization: Bearer arb_live_your-api-key-here" \
-H "Content-Type: application/json" \
-d '{ "user_id": "usr_01HZ_ALICE" }'

Response 200 OK

{
"challenge": "dGhpcyBpcyBhIHNhbXBsZSBjaGFsbGVuZ2U",
"rp": {
"name": "Arbitex",
"id": "arbitex.ai"
},
"user": {
"id": "dXNyXzAxSFpfQUxJQ0U",
"name": "[email protected]",
"displayName": "Alice Chen"
},
"pubKeyCredParams": [
{ "type": "public-key", "alg": -7 },
{ "type": "public-key", "alg": -257 }
],
"authenticatorSelection": {
"authenticatorAttachment": "platform",
"residentKey": "required",
"userVerification": "required"
},
"timeout": 60000,
"attestation": "none"
}
Field Type Description
challenge string (base64url) Random challenge that the authenticator must sign
rp object Relying party — the application’s identity
rp.id string Relying party origin (must match the request origin)
user object User handle and display information
pubKeyCredParams array Acceptable public key credential algorithms
authenticatorSelection object Constraints on the authenticator type and user verification
timeout integer Maximum milliseconds the browser will wait for the authenticator

Error responses

Status Description
404 user_id not found (admin call only)
409 User has already registered the maximum number of passkeys

POST /api/auth/passkey/register/complete

Submits the attestation object produced by the authenticator to complete registration. The server verifies the signature against the challenge issued by register/begin and stores the credential.

Request body

Field Type Required Description
credential object Yes The PublicKeyCredential object returned by navigator.credentials.create()
credential.id string (base64url) Yes Credential identifier
credential.rawId string (base64url) Yes Raw credential identifier bytes
credential.response.attestationObject string (base64url) Yes CBOR-encoded attestation object
credential.response.clientDataJSON string (base64url) Yes Client data JSON
credential.type string Yes Must be "public-key"

Request

Terminal window
curl -X POST "https://api.arbitex.ai/api/auth/passkey/register/complete" \
-H "Authorization: Bearer arb_live_your-api-key-here" \
-H "Content-Type: application/json" \
-d '{
"credential": {
"id": "pQvKbQ9X2m4YrZ8fNcOwJA",
"rawId": "pQvKbQ9X2m4YrZ8fNcOwJA",
"response": {
"attestationObject": "o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YVik...",
"clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiZEdocGN5QnBjeUJoSUhOaGJYQmxaQ0JqYUdGc2JHVW5iUSIsIm9yaWdpbiI6Imh0dHBzOi8vYXJiaXRleC5haSJ9"
},
"type": "public-key"
}
}'

Response 201 Created

{
"credential_id": "a3f8e2b1-4c7d-4e9a-8b6f-1d2e3f4a5b6c",
"created_at": "2026-03-14T10:00:00Z",
"aaguid": "adce0002-35bc-c60a-648b-0b25f1f05503",
"transports": ["internal", "hybrid"]
}
Field Type Description
credential_id string (UUID) Arbitex-assigned credential identifier
created_at datetime ISO 8601 registration timestamp
aaguid string (UUID) Authenticator Attestation GUID identifying the authenticator model
transports array of string Reported transport hints: internal, usb, nfc, ble, hybrid

Error responses

Status Description
400 Challenge has expired (default TTL: 5 minutes) — restart the ceremony
422 Attestation verification failed — invalid signature, origin mismatch, or malformed CBOR

POST /api/auth/passkey/authenticate/begin

Generates an authentication challenge. The response is a standard PublicKeyCredentialRequestOptions object that must be passed to navigator.credentials.get().

For discoverable credentials (passkeys), omit user_id — the browser will prompt the user to select from their stored passkeys. For non-discoverable credentials (legacy security keys), supply user_id to populate allowCredentials with the user’s enrolled credential IDs.

Request body

Field Type Required Description
user_id string (UUID) No For non-discoverable flow only — populates allowCredentials. Omit for discoverable credentials.

Request

Terminal window
# Discoverable (passkey) flow — no user_id required
curl -X POST "https://api.arbitex.ai/api/auth/passkey/authenticate/begin" \
-H "Content-Type: application/json" \
-d '{}'

Response 200 OK

{
"challenge": "cmFuZG9tQ2hhbGxlbmdlQnl0ZXNIZXJl",
"rpId": "arbitex.ai",
"allowCredentials": [],
"timeout": 60000,
"userVerification": "required"
}
Field Type Description
challenge string (base64url) Random challenge the authenticator must sign
rpId string Relying party ID — must match the registration rp.id
allowCredentials array Credential descriptors to limit which credentials may respond. Empty for discoverable flow.
userVerification string required — biometric or PIN verification is mandatory

Error responses

Status Description
404 user_id not found (non-discoverable flow only)
422 user_id found but the user has no enrolled passkeys

POST /api/auth/passkey/authenticate/complete

Submits the signed assertion produced by the authenticator. The server verifies the signature, checks the sign counter for cloning detection, and returns a session token on success.

Request body — standard WebAuthn AuthenticatorAssertionResponse wrapped in a PublicKeyCredential

Field Type Required Description
credential.id string (base64url) Yes Credential identifier returned by the authenticator
credential.rawId string (base64url) Yes Raw credential identifier bytes
credential.response.authenticatorData string (base64url) Yes Authenticator data
credential.response.clientDataJSON string (base64url) Yes Client data JSON
credential.response.signature string (base64url) Yes Assertion signature
credential.response.userHandle string (base64url) No User handle (provided by discoverable credentials)
credential.type string Yes Must be "public-key"

Request

Terminal window
curl -X POST "https://api.arbitex.ai/api/auth/passkey/authenticate/complete" \
-H "Content-Type: application/json" \
-d '{
"credential": {
"id": "pQvKbQ9X2m4YrZ8fNcOwJA",
"rawId": "pQvKbQ9X2m4YrZ8fNcOwJA",
"response": {
"authenticatorData": "SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MFAAAABg",
"clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiY21GdVpHOXRRMmhoYkd4bGJtZEJlblJsY2lJLCJvcmlnaW4iOiJodHRwczovL2FyYml0ZXguYWkifQ",
"signature": "MEUCIQDktp8yRBGGzLKxV3Q9JlGJXNNq5QjLtj3P5KYzXkR4IAIgFgNM9r..."
},
"type": "public-key"
}
}'

Response 200 OK

{
"token": "arb_session_eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9...",
"user_id": "usr_01HZ_ALICE",
"credential_id": "a3f8e2b1-4c7d-4e9a-8b6f-1d2e3f4a5b6c",
"sign_count": 42
}
Field Type Description
token string Session token for subsequent API requests
user_id string Authenticated user identifier
credential_id string (UUID) Arbitex credential identifier that was used
sign_count integer Updated authenticator signature counter — used to detect cloned credentials

Error responses

Status Description
400 Challenge has expired — restart the authentication ceremony
401 Signature verification failed
409 Sign count decreased — possible credential cloning detected; credential suspended pending admin review
422 Malformed assertion data

GET /api/auth/passkey/credentials

Returns all passkeys enrolled for the authenticated user. Requires a valid user session.

Request

Terminal window
curl "https://api.arbitex.ai/api/auth/passkey/credentials" \
-H "Authorization: Bearer arb_session_eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9..."

Response 200 OK

[
{
"credential_id": "a3f8e2b1-4c7d-4e9a-8b6f-1d2e3f4a5b6c",
"aaguid": "adce0002-35bc-c60a-648b-0b25f1f05503",
"transports": ["internal", "hybrid"],
"created_at": "2026-03-14T10:00:00Z",
"last_used_at": "2026-03-14T09:48:00Z",
"sign_count": 42
},
{
"credential_id": "b4e9f3c2-5d8e-4f0b-9c7g-2e3f4a5b6c7d",
"aaguid": "f8a011f3-8c0a-4d15-8006-17111f9edc7d",
"transports": ["usb"],
"created_at": "2026-01-20T14:30:00Z",
"last_used_at": "2026-03-10T11:20:00Z",
"sign_count": 187
}
]

DELETE /api/auth/passkey/credentials/{credential_id}

Removes a passkey from the authenticated user’s account. The credential is immediately invalidated and cannot be used for future authentication.

Path parameters

Parameter Description
credential_id UUID of the credential to remove

Request

Terminal window
curl -X DELETE \
"https://api.arbitex.ai/api/auth/passkey/credentials/a3f8e2b1-4c7d-4e9a-8b6f-1d2e3f4a5b6c" \
-H "Authorization: Bearer arb_session_eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9..."

Response 204 No Content

Error responses

Status Description
403 Credential belongs to a different user
404 Credential not found
409 Cannot remove the last credential — user would be locked out

GET /api/v1/admin/passkeys/

Returns all passkeys across all users in the organization, paginated. Requires an admin-scoped API key.

Query parameters

Parameter Type Description
user_id string Filter results to a specific user’s credentials
page integer Page number (default 1)
per_page integer Results per page (default 25, max 100)

Request

Terminal window
curl "https://api.arbitex.ai/api/v1/admin/passkeys/?user_id=usr_01HZ_ALICE&page=1&per_page=25" \
-H "Authorization: Bearer arb_live_your-api-key-here"

Response 200 OK

{
"data": [
{
"credential_id": "a3f8e2b1-4c7d-4e9a-8b6f-1d2e3f4a5b6c",
"user_id": "usr_01HZ_ALICE",
"aaguid": "adce0002-35bc-c60a-648b-0b25f1f05503",
"transports": ["internal", "hybrid"],
"created_at": "2026-03-14T10:00:00Z",
"last_used_at": "2026-03-14T09:48:00Z",
"sign_count": 42
}
],
"total": 1,
"page": 1,
"per_page": 25
}
Field Type Description
credential_id string (UUID) Arbitex-assigned credential identifier
user_id string Owning user’s identifier
aaguid string (UUID) Authenticator Attestation GUID
transports array of string Transport hints reported at registration
created_at datetime ISO 8601 registration timestamp
last_used_at datetime | null ISO 8601 timestamp of the most recent successful authentication; null if never used after enrollment
sign_count integer Current authenticator signature counter

GET /api/v1/admin/passkeys/{credential_id}

Returns full metadata for a single credential.

Path parameters

Parameter Description
credential_id UUID of the credential

Request

Terminal window
curl "https://api.arbitex.ai/api/v1/admin/passkeys/a3f8e2b1-4c7d-4e9a-8b6f-1d2e3f4a5b6c" \
-H "Authorization: Bearer arb_live_your-api-key-here"

Response 200 OK

{
"credential_id": "a3f8e2b1-4c7d-4e9a-8b6f-1d2e3f4a5b6c",
"user_id": "usr_01HZ_ALICE",
"user_email": "[email protected]",
"aaguid": "adce0002-35bc-c60a-648b-0b25f1f05503",
"transports": ["internal", "hybrid"],
"backup_eligible": true,
"backup_state": true,
"created_at": "2026-03-14T10:00:00Z",
"last_used_at": "2026-03-14T09:48:00Z",
"sign_count": 42,
"status": "active"
}
Field Type Description
backup_eligible boolean Whether the credential is eligible for cloud backup (platform passkeys)
backup_state boolean Whether the credential is currently backed up
status string active, suspended (sign count anomaly), or revoked

Error responses

Status Description
404 Credential not found

DELETE /api/v1/admin/passkeys/{credential_id}

Permanently revokes a passkey. The credential is immediately invalidated — any in-flight authentication ceremony using this credential will fail. This operation cannot be undone.

Path parameters

Parameter Description
credential_id UUID of the credential to revoke

Request

Terminal window
curl -X DELETE \
"https://api.arbitex.ai/api/v1/admin/passkeys/a3f8e2b1-4c7d-4e9a-8b6f-1d2e3f4a5b6c" \
-H "Authorization: Bearer arb_live_your-api-key-here"

Response 204 No Content

Error responses

Status Description
404 Credential not found

The full passkey enrollment sequence from a web client:

Step 1 — Request creation options

Terminal window
curl -X POST "https://api.arbitex.ai/api/auth/passkey/register/begin" \
-H "Authorization: Bearer arb_session_eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9..." \
-H "Content-Type: application/json" \
-d '{}'

The server returns PublicKeyCredentialCreationOptions. Pass this directly to the browser:

const options = await response.json();
const credential = await navigator.credentials.create({ publicKey: options });

Step 2 — Submit the attestation

Terminal window
curl -X POST "https://api.arbitex.ai/api/auth/passkey/register/complete" \
-H "Authorization: Bearer arb_session_eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9..." \
-H "Content-Type: application/json" \
-d '{
"credential": {
"id": "<credential.id>",
"rawId": "<base64url(credential.rawId)>",
"response": {
"attestationObject": "<base64url(credential.response.attestationObject)>",
"clientDataJSON": "<base64url(credential.response.clientDataJSON)>"
},
"type": "public-key"
}
}'

Response — the enrolled passkey is now active:

{
"credential_id": "a3f8e2b1-4c7d-4e9a-8b6f-1d2e3f4a5b6c",
"created_at": "2026-03-14T10:00:00Z",
"aaguid": "adce0002-35bc-c60a-648b-0b25f1f05503",
"transports": ["internal", "hybrid"]
}

Step 1 — Request authentication options

Terminal window
curl -X POST "https://api.arbitex.ai/api/auth/passkey/authenticate/begin" \
-H "Content-Type: application/json" \
-d '{}'

Pass the returned PublicKeyCredentialRequestOptions to the browser:

const options = await response.json();
const assertion = await navigator.credentials.get({ publicKey: options });

Step 2 — Submit the assertion

Terminal window
curl -X POST "https://api.arbitex.ai/api/auth/passkey/authenticate/complete" \
-H "Content-Type: application/json" \
-d '{
"credential": {
"id": "<assertion.id>",
"rawId": "<base64url(assertion.rawId)>",
"response": {
"authenticatorData": "<base64url(assertion.response.authenticatorData)>",
"clientDataJSON": "<base64url(assertion.response.clientDataJSON)>",
"signature": "<base64url(assertion.response.signature)>",
"userHandle": "<base64url(assertion.response.userHandle)>"
},
"type": "public-key"
}
}'

Response — use the session token for all subsequent API calls:

{
"token": "arb_session_eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9...",
"user_id": "usr_01HZ_ALICE",
"credential_id": "a3f8e2b1-4c7d-4e9a-8b6f-1d2e3f4a5b6c",
"sign_count": 43
}

Status Description
400 Bad Request Challenge expired or required field missing
401 Unauthorized Missing session token or signature verification failed
403 Forbidden Valid credentials but insufficient scope (e.g., non-admin calling an admin endpoint)
404 Not Found User or credential not found
409 Conflict Sign count anomaly (potential cloning), last-credential removal attempt, or registration cap reached
422 Unprocessable Entity Malformed attestation or assertion data