Developers
API reference
Version 1 · Base URL https://api.lumex.id/v1
- Quick start
- Authentication
- Create a verification
- Get a verification
- The verification object
- Webhooks
- Errors
Quick start
- Create a verification from your server for one of your users.
- Send the user to the
urlin the response. They scan their ID card or passport and take a short selfie in their phone browser. - We post the result to your webhook and send the user back to your
redirect_url.
All requests and responses are JSON. Call the API from your server only, never from a browser or mobile app, because your secret key must stay private.
Authentication
Send your secret key in the Authorization header. You get two keys in your dashboard:
| Key | Use |
|---|---|
lx_test_… | Test mode. Runs the full flow with a test banner. Not billed. |
lx_live_… | Live mode. Real checks, billed per completed check. |
Authorization: Bearer lx_live_…
Create a verification
| Field | Type | Description |
|---|---|---|
reference | string, required | Your own ID for the user, up to 100 characters. Returned in every response and webhook. |
document | string | id_card, passport or any. Default any lets the user choose. |
locale | string | en or ar. Default en. The user can switch language in the flow. |
redirect_url | string | HTTPS address the user returns to when they finish. We add ?verification=vrf_… to it. |
metadata | object | Up to 20 string keys and values of your own. Returned as sent. |
Send an Idempotency-Key header with a unique value to make retries safe. The same key within 24 hours returns the first result instead of creating a second verification.
curl https://api.lumex.id/v1/verifications \ -H "Authorization: Bearer lx_live_…" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: 6f1c2b7e-signup-1042" \ -d '{ "reference": "user_1042", "document": "any", "locale": "ar", "redirect_url": "https://your-site.com/verified" }'
Response 201 Created:
{
"id": "vrf_7KQ2M9",
"reference": "user_1042",
"status": "created",
"url": "https://verify.lumex.id/v/…",
"expires_at": "2026-10-09T08:00:00Z",
"created_at": "2026-10-08T08:00:00Z"
}The link works for 24 hours and for one person only. Create a new verification if it expires.
Get a verification
Returns the full verification object. Use it to double-check a webhook, or to poll if you cannot receive webhooks.
curl https://api.lumex.id/v1/verifications/vrf_7KQ2M9 \
-H "Authorization: Bearer lx_live_…"The verification object
{
"id": "vrf_7KQ2M9",
"reference": "user_1042",
"status": "approved",
"decline_reasons": [],
"document": {
"type": "id_card",
"country": "KWT",
"number": "287041501234",
"surname": "ALSALEM",
"given_names": "FAHAD",
"nationality": "KWT",
"birth_date": "1987-04-15",
"sex": "M",
"expiry_date": "2029-03-01"
},
"checks": {
"code_lines": "passed",
"expiry": "passed",
"liveness": "passed",
"face_match": { "result": "passed", "score": 0.97 }
},
"metadata": {},
"created_at": "2026-10-08T08:00:00Z",
"completed_at": "2026-10-08T08:01:04Z"
}Status moves in one direction:
| Status | Meaning |
|---|---|
created | Link made, user has not opened it yet. |
in_progress | User opened the link and is scanning. |
approved | All checks passed. Final. |
declined | One or more checks failed. See decline_reasons. Final. |
expired | The link was not finished within 24 hours. Final, not billed. |
Decline reasons: document_unreadable, document_unsupported, code_lines_invalid, document_expired, liveness_failed, face_mismatch.
Country codes are the three-letter codes printed in the document's code lines. Document and selfie images are deleted once the result is decided, so they are never returned by the API.
Webhooks
Add your webhook address in the dashboard. We send a POST with a JSON body for each event:
| Event | When |
|---|---|
verification.started | The user opened the link. |
verification.approved | The verification was approved. |
verification.declined | The verification was declined. |
verification.expired | The link expired unfinished. |
{
"id": "evt_3HZ81L",
"type": "verification.approved",
"created_at": "2026-10-08T08:01:04Z",
"data": { /* the verification object */ }
}Reply with any 2xx status within 10 seconds. If you do not, we retry with growing gaps for up to 24 hours. The same event can arrive more than once, so ignore an id you have already handled.
Check the signature on every webhook. Each request has a Lumex-Signature header like t=1791446464,v1=5257a8…. Compute an HMAC-SHA256 of t + "." + raw body with your webhook secret and compare it to v1. Reject it if t is more than 5 minutes old.
import crypto from "node:crypto"; export function verifyLumex(rawBody, header, secret) { const p = Object.fromEntries(header.split(",").map(x => x.split("="))); const t = Number(p.t); if (!t || Math.abs(Date.now() / 1000 - t) > 300) return false; const expected = crypto.createHmac("sha256", secret) .update(`${t}.${rawBody}`).digest("hex"); if (!p.v1 || p.v1.length !== expected.length) return false; return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(p.v1)); }
function verify_lumex(string $raw, string $header, string $secret): bool { parse_str(str_replace(',', '&', $header), $p); if (empty($p['t']) || abs(time() - (int)$p['t']) > 300) return false; $expected = hash_hmac('sha256', $p['t'] . '.' . $raw, $secret); return hash_equals($expected, $p['v1'] ?? ''); }
Use the raw request body exactly as received. Parsing and re-encoding the JSON first changes the bytes and breaks the signature.
Errors
Errors return a JSON body with a stable code you can check in your code, and a readable message:
{ "error": { "code": "invalid_field", "message": "redirect_url must start with https://" } }| HTTP | When |
|---|---|
400 | A field is missing or not valid. |
401 | The key is missing, wrong or revoked. |
402 | Your membership is not active. |
404 | No verification with that ID in this mode. |
409 | The same Idempotency-Key was used with a different body. |
429 | Too many requests. Wait for the seconds in Retry-After. |
5xx | Our side. Retry with the same Idempotency-Key. |
Need a hand?
Tell us about your stack through the contact form and we will help you connect your first site.