Link Identity Platform · v1
Getting started
A working integration in about thirty minutes: get a token, register a scanner, run an enrollment, run a verification, and wire up webhooks for the asynchronous results. This guide intentionally skips edge cases — for the full surface, see the API reference.
What this is#
Link Identity is biometric identification at the edge. A tenant owns one or more palm scanners. Users enroll once on any scanner in the tenant, and any other scanner in that tenant can verify them in roughly a second. Enrollment and verification are asynchronous — you create a challenge, the scanner captures the biometric, and the platform delivers the result over a webhook.
The REST surface is small on purpose: one token endpoint, three challenge endpoints (devices, enrollment, verification), and webhook management. The rest of this page walks each of them in order.
Prerequisites#
You will need:
- A tenant in the Link Identity console and an admin or operator login.
- An OAuth client. Create one at
Integrations → OAuth Clients → New client. Copy theclient_idandclient_secret— the secret is shown once. - An HTTPS endpoint that can receive POST deliveries. The examples below use
https://your-app.example.com/webhooks/identity— substitute your own. - A paired scanner if you intend to run a real enrollment. You need its
hardware_idonce provisioning is complete.
Authenticate#
Exchange the client credentials for a Bearer token. This is the only unauthenticated call in the API.
curl -X POST https://link-identity-sandbox-427874453693.me-central1.run.app/v1/auth/token \
-H "Content-Type: application/json" \
-d '{
"grant_type": "client_credentials",
"client_id": "06a4dc18-b06c-4e1c-ad25-204d44bf0c71",
"client_secret": "cs_live_xxx"
}'{
"success": true,
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 3600
}Tokens live for expires_inseconds (one hour). There is no refresh token in the client-credentials flow — request a new one when the current is within ~60 seconds of expiry. Cache it in memory for the duration; don't fetch a new token per request.
Warning
client_secret is shown once at creation. If you lose it, rotate the client from the console. Rotating creates a new secret; tokens issued from the old secret remain valid until they expire naturally.Register a device#
Register a scanner in the console first. The response carries a one-time pairing_code (valid for expires_in seconds) that you enter on the physical scanner to complete provisioning.
curl -X POST https://link-identity-sandbox-427874453693.me-central1.run.app/v1/devices \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"device_name": "Scanner-04",
"location": "Main Entrance"
}'{
"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."
}Once paired, keep the scanner's hardware_id — every enrollment and verification call needs it. Pairing completion is reported via the device.paired webhook (see Listen for events).
Full contract: Register Device.
Run an enrollment#
Create a both-hands enrollment challenge for a user against a specific paired scanner. The challenge tells the scanner to capture both palms; the platform stores the biometric templates against your external_user_id.
curl -X POST https://link-identity-sandbox-427874453693.me-central1.run.app/v1/enroll/both \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"external_user_id": "{{external_user_id}}",
"hardware_id": "hardware_local_003",
"metadata": { "employee_id": "EMP-4421" }
}'A 201 means the challenge is pending, not that the user is enrolled. The biometric capture happens on the scanner over the next few seconds, and the completion result arrives over the enrollment.completewebhook. Don't poll — wait for the event.
Full contract: Create Both Hands Enrollment Challenge.
Run a verification#
Verification is a 1:1 palm match against a previously enrolled user. Same asynchronous shape as enrollment — create a challenge, wait for the webhook.
curl -X POST https://link-identity-sandbox-427874453693.me-central1.run.app/v1/verify \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"external_user_id": "{{external_user_id}}",
"hardware_id": "{{hardware_id}}",
"metadata": {
"document_id": "doc_123",
"document_hash": "sha256"
}
}'The result arrives over the verification.complete webhook with a matched boolean, score arrays, and timing details.
Full contract: Create Verification Challenge.
Listen for events#
The three events you'll handle: device.paired, enrollment.complete, and verification.complete. Register your endpoint once; the platform delivers a JSON payload to it whenever a subscribed event fires.
curl -X POST https://link-identity-sandbox-427874453693.me-central1.run.app/v1/webhooks \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Production Events",
"events": ["device.paired", "enrollment.complete", "verification.complete"],
"endpoint": "https://your-app.example.com/webhooks/identity",
"api_key": "whsec_your_secret_key_here"
}'Sample delivery for a successful verification:
POST https://your-app.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
{
"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"
}
}
}Warning
Full contract: Create Webhook, Payload Format.
Production checklist#
Before you ship the integration:
- Rotate client secrets on a schedule — quarterly is a reasonable cadence. Rotation issues a new secret without invalidating tokens already in flight; switch over, then revoke the old one. Treat a leaked secret as an incident — delete the OAuth client entirely to kill tokens immediately.
- Verify delivery headers on every webhook — check
X-API-Key(constant-time compare),X-Link-Event, andX-Link-Deliverybefore you trust the payload. - Acknowledge fast, process async — return 200 within a second or two, then enqueue the work. Anything slower invites retry storms during traffic spikes.
- Handle 401, 403, and 422 distinctly — 401 means the token is wrong or expired (refresh and retry once), 403 means the OAuth client lacks the scope for that endpoint (a permissions problem on your side), and 422 means the request body didn't validate. Lumping them together makes failures invisible.
- Test against a staging tenant first — create a parallel tenant in the console, point staging there, run the full enrollment → verification loop end-to-end before pointing production traffic at the real one.
Where to next#
You now have the shape of every call. Bookmark the API reference for the full parameter lists, status tables, and per-endpoint notes. SDKs in Node, Python, and Kotlin for the Android scanner builds are tracked on the SDKs page.