Link Identity Platform · v1

API Reference

The Link Identity API exposes device registration, biometric enrollment, verification, and real-time event delivery over a single REST surface. All protected endpoints use OAuth 2.0 Client Credentials and require a Bearer token.

Base URLhttps://link-identity-sandbox-427874453693.me-central1.run.app

Overview#

Four steps take you from credentials to a verified palm scan.

  1. Create OAuth client credentials in Integrations → OAuth Clients.
  2. Request a token from POST /v1/auth/token.
  3. Send Authorization: Bearer <access_token> on protected endpoints.
  4. Refresh the token before expiry using the expires_in field — request a new one ~60 seconds early.

Note

All requests and responses are JSON. Errors return { success: false, message } — see Error Format for the full shape.

Authentication#

Exchange client credentials for a short-lived Bearer token, then attach it to every protected call.

POST/v1/auth/tokenPublic

Generate an access token. This endpoint itself is public — Bearer auth is not required to call it.

Field
Type
Required
Description
grant_type
string
Required
Must be client_credentials.
client_id
UUID
Required
Client ID from the Link Identity console.
client_secret
string
Required
Secret shown once during credential creation.
200Token issued successfully.
400Invalid body or unsupported grant type.
401Invalid client_id or client_secret.

Note

There is no refresh token in client-credentials flow — request a new token when the current one expires.
Request
POST https://link-identity-sandbox-427874453693.me-central1.run.app/v1/auth/token
Content-Type: application/json

{
  "grant_type": "client_credentials",
  "client_id": "06a4dc18-b06c-4e1c-ad25-204d44bf0c71",
  "client_secret": "cs_live_xxx"
}
Response 200
{
  "success": true,
  "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
  "token_type": "Bearer",
  "expires_in": 3600
}

Credential Rotation#

How rotation and revocation affect tokens already in flight.

RotateCreates a new client_secret. Tokens issued from the old secret remain valid until they expire on their own.
DeleteImmediately invalidates all active tokens for that OAuth client.
ExpiryTokens expire after expires_in seconds and must be reissued.

Warning

For emergency revocation (suspected credential leak), delete the OAuth client to invalidate tokens without a grace period.

Endpoints

Register Device#

Create a scanner record and receive a one-time pairing code. The hardware_id is assigned by the physical device during pairing — not returned here.

POST/v1/devicesBearer required

Registers a new scanner in the console and returns a pairing code. Enter that code on the physical scanner to complete provisioning.

Field
Type
Required
Description
device_name
string
Required
Human-readable label. 1-255 characters.
location
string
Required
Physical location details. 1-255 characters.
201Device record created and one-time pairing code returned.
400Validation or business-rule failure.
401Missing or invalid Bearer token.
Request body
{
  "device_name": "Scanner-04",
  "location": "Main Entrance"
}
Response 201
{
  "success": true,
  "data": {
    "device_id": "cced6e19-7e3b-4013-bc04-8bd2efc57e22",
    "device_name": "Scanner-04",
    "location": "Main Entrance",
    "created_at": "2026-06-08T14:08:43.151246Z",
    "pairing_code": "mdxglt2xg",
    "expires_in": 1800
  },
  "message": "Enter this pairing code on the physical scanner to complete provisioning."
}

Create Both Hands Enrollment Challenge#

Initiate a both-hands biometric enrollment for a user against a specific paired scanner.

POST/v1/enroll/bothBearer required

Creates a both-hands enrollment challenge. The device captures both palms; the result is delivered asynchronously via webhook.

Field
Type
Required
Description
external_user_id
string
Required
Tenant-scoped external user identifier.
hardware_id
string
Required
The hardware identifier provided during device pairing.
metadata
object | null
Optional
Opaque key-value data echoed back on the challenge record and response.
201Challenge created; device should capture both palms.
400Bad request or business-rule failure.
401Missing or invalid Bearer token.
403Token lacks the required tenant permissions.
404User or hardware_id not found.
409User already enrolled for this device context.
422Request body validation failed.

Tip

A successful response means the challenge is pending with palm_type: both. The completion result is delivered asynchronously via the enrollment.complete webhook.
Request body
{
  "external_user_id": "{{external_user_id}}",
  "hardware_id": "hardware_local_002",
  "metadata": { "employee_id": "EMP-4421", "department": "Engineering" }
}
Response 201
{
  "success": true,
  "data": {
    "challenge_id": "c4a5dbe7-ae49-4c28-9cdb-9f4558156160",
    "type": "enroll",
    "external_user_id": "{{external_user_id}}",
    "hardware_id": "hardware_local_002",
    "palm_type": "both",
    "status": "pending",
    "expires_at": "2026-06-08T15:08:43.151246Z",
    "metadata": { "employee_id": "EMP-4421", "department": "Engineering" }
  },
  "message": "Enrollment challenge created."
}

Create Verification Challenge#

Run a 1:1 palm match against a previously enrolled user.

POST/v1/verifyBearer required

Creates a verification challenge. The match result is emitted asynchronously over a webhook.

Field
Type
Required
Description
external_user_id
string
Required
Your tenant-scoped user identifier for the enrolled user.
hardware_id
string
Required
Paired scanner hardware ID.
metadata
object | null
Optional
Custom context echoed in the verification.complete webhook payload.
201Challenge created (not yet verified).
401Missing or expired Bearer token.
422Schema validation failure.

Tip

The biometric result arrives via the verification.complete webhook with a matched boolean and score details.
Request body
{
  "external_user_id": "{{external_user_id}}",
  "hardware_id": "{{hardware_id}}",
  "metadata": {
    "document_id": "doc_123",
    "document_hash": "sha256"
  }
}

Webhooks

Create Webhook#

Register an HTTPS endpoint to receive event deliveries.

POST/v1/webhooksBearer required

Subscribes a callback URL to one or more event keys. Deliveries POST a JSON payload signed with the optional API key.

Field
Type
Required
Description
name
string
Required
Display name for the webhook.
events
string[]
Required
One or more event keys to subscribe to.
endpoint
URL
Required
HTTPS callback URL that receives POST payloads.
api_key
string | null
Optional
If provided, sent as the X-API-Key header on every delivery.
201Webhook created and activated.
400Invalid endpoint, events, or body format.
403Insufficient permissions for webhook management.
409Duplicate webhook endpoint or name conflict.
Request body
{
  "name": "Production Events",
  "events": ["device.paired", "enrollment.complete", "verification.complete"],
  "endpoint": "https://yourapp.example.com/webhooks/identity",
  "api_key": "whsec_your_secret_key_here"
}

Event Types#

The three event keys you can subscribe to today.

device.pairedDevice pairing completed.
enrollment.completeBiometric enrollment completed.
verification.completeVerification match result with scores and thresholds.

Payload Format#

Every delivery is a POST with standard headers and a JSON body using event_type, tenant_id, timestamp, and a payload-specific data block.

Delivery headers
POST https://yourapp.example.com/webhooks/identity
Content-Type: application/json
X-Link-Event: verification.complete
X-Link-Delivery: del_a1b2c3d4-e5f6-7890-abcd-ef1234567890
User-Agent: LinkIdentity-Webhook/1.0
X-API-Key: whsec_your_secret_key_here
Field
Type
Required
Description
event_id
string
Required
Unique delivery event identifier.
event_type
string
Required
Event key, e.g. device.paired, enrollment.complete, verification.complete.
tenant_id
UUID
Required
Tenant that owns the resource.
timestamp
string (ISO 8601)
Required
When the event was emitted.
data
object
Required
Event-specific payload — see examples below.
device.paired
{
  "event_id": "evt_a1b2c3d4e5f6789012345678abcdef01",
  "event_type": "device.paired",
  "tenant_id": "d1e2f3a4-b5c6-7890-abcd-ef1234567890",
  "timestamp": "2026-06-08T14:32:11.123456Z",
  "data": {
    "hardware_id": "hardware_local_002",
    "device_name": "Lobby Scanner",
    "location": "Lobby",
    "status": "paired",
    "paired_at": "2026-06-08T14:32:10.987654Z",
    "cert_valid_until": "2026-09-06T14:32:10.987654Z"
  }
}
enrollment.complete
{
  "event_id": "evt_b2c3d4e5f6789012345678abcdef012345",
  "event_type": "enrollment.complete",
  "tenant_id": "d1e2f3a4-b5c6-7890-abcd-ef1234567890",
  "timestamp": "2026-06-08T14:35:22.456789Z",
  "data": {
    "user_id": "7b9957e2-c85f-4f9e-abe1-8aca9b6de70d",
    "feature_id": "fid-fixture-001",
    "palm_type": "right",
    "hardware_id": "hardware_local_002",
    "challenge_id": "c4a5dbe7-ae49-4c28-9cdb-9f4558156160",
    "enrolled_at": "2026-06-08T14:35:21.912955Z",
    "vendor": "mock",
    "metadata": {
      "source": "mobile-app"
    }
  }
}
verification.complete
{
  "event_id": "evt_c3d4e5f6789012345678abcdef01234567",
  "event_type": "verification.complete",
  "tenant_id": "d1e2f3a4-b5c6-7890-abcd-ef1234567890",
  "timestamp": "2026-06-08T14:40:33.789012Z",
  "data": {
    "challenge_id": "ea1d1e69-f63f-401d-a5b6-6429534bff60",
    "user_id": "7b9957e2-c85f-4f9e-abe1-8aca9b6de70d",
    "matched": true,
    "scores": [0.99, 0.98, 0.97, 0.96, 0.95, 0.94, 0.93, 0.92, 0.91, 0.90],
    "thresholds": [0.7072, 0.7253, 0.7018, 0.7211, 0.7426, 0.6929, 0.6850, 0.7100, 0.7526, 0.7004],
    "match_policy": "all_thresholds",
    "latency_ms": 180,
    "vendor": "mock",
    "verified_at": "2026-06-08T14:40:32.500000Z",
    "metadata": {
      "session_id": "sess_0099",
      "access_point": "turnstile-7"
    }
  }
}

Note

Respond with 2xx within 10 seconds to acknowledge delivery. Non-2xx responses are retried with exponential backoff.

Reference

Error Format#

Every non-2xx response uses the same JSON shape.

Error body
{
  "success": false,
  "message": "Human-readable description of the error"
}

HTTP Status Codes#

The status codes you should expect across the API.

400Bad Request — invalid fields or business-rule violations.
401Unauthorized — missing or invalid token or credentials.
403Forbidden — token valid but lacks the required permission.
404Not Found — referenced resource does not exist.
409Conflict — duplicate or conflicting resource state.
422Unprocessable Entity — schema validation failed.
500Internal Server Error — unexpected server error.