Liveness flow
The liveness flow has four steps and runs in two different places: part of it in your backend, part of it on the user's device. That split explains almost everything here — the JWT token, the capture SDK and the identifier that ties the two API calls together.
The reference pages describe each endpoint in isolation. This page answers what they do not: in what order to call, who calls what and how the identifiers chain together.
‼️ Two traps, before anything else
Each one has already broken an integration in production, and both fail silently, with a 200 in the response.
- Call
/liveness/v2, always with the version in the path. It is the v2 envelope — with the result indata.isAlive— that the next step reads. A URL with no version does not guarantee that response, and the face match answersmatched: falseforever, with no error at all, whenever it does not finddata.isAlive. /face-match-for-livenessreturns only{"matched": true}or{"matched": false}. Noid, noversion, nodata, nometadata. It is the exception to the API's standard envelope, and a client expectingdatagetsundefined.
Overview
| # | Who calls | What | What comes back |
|---|---|---|---|
| 1 | Your backend | POST /auth-jwt | accessToken |
| 2 | Your backend → SDK | hands the token over to the device | — |
| 3 | SDK, on the device | POST /liveness/v2 with the selfie captured | session id and data.isAlive |
| 4 | Your backend | POST /face-match-for-liveness/{id}/v2 with the document | matched |
Between step 3 and step 4, the id of the liveness session has to travel back from the device to your backend — it is the only thread linking the two calls.
Your backend SDK on the device Nextcode API
│ │ │
│ 1. POST /auth-jwt │ │
────────────────────────────────────────────────────▶
│ { accessToken } │
◀────────────────────────────────────────────────────
│ │ │
│ 2. hand over the token │ │
──────────────────────────▶ │
│ │ │
│ 3. captures the selfie and calls │
│ POST /liveness/v2 │
│ ──────────────────────────▶
│ │ { id, data.isAlive } │
│ ◀──────────────────────────
│ │ │
│ the session id goes back to the backend │
◀────────────────────────── │
│ │ │
│ 4. POST /face-match-for-liveness/{id}/v2 │
│ + the photo of the document │
────────────────────────────────────────────────────▶
│ { "matched": true } │
◀────────────────────────────────────────────────────
│ │ │The dividing line matters
Everything that happens in the middle column runs in code the end user can inspect. That is why the API key never travels down there, and why step 1 exists.
Step 1 — the backend issues the token
The capture SDK runs inside your app or in the user's browser. Putting the API key there would mean handing it to anyone willing to open the browser inspector or decompile the APK.
The way out is a short-lived JWT token: your backend calls /auth-jwt with the API key and gets back an accessToken that can safely travel down to the device. It carries the same permissions as the key that issued it and expires on its own.
Full reference of the parameters and the responses: JWT token.
Three decisions that apply to this step:
- The call always starts from your backend.
/auth-jwtis the only endpoint of the API that does not accept the JWT token itself: it is the one that issues the token, and only the API key authenticates it. - Issue the token immediately before handing it to the SDK. The
ttlshould cover the capture window, not the user's entire session. A token issued at login and used ten minutes later usually reaches the SDK already expired. ttlis a string with a unit —"15m", not900and not"900". That is the most common cause of500on this endpoint, and it is spelled out on the reference page.
Step 2 — capturing with the SDK
The SDK conducts the liveness check on the device, authenticates itself with the token in the Authorization: Bearer <accessToken> header and calls /liveness/v2 directly. The selfie never goes through your backend.
| Platform | Sample repository |
|---|---|
| Android | liveness-sdk-android-sample |
| iOS | liveness-sdk-ios-sample |
| Web | liveness-sdk-web-sample |
Confirm that the version being called is v2. This is the exact point where the first trap sets in: with no version in the path, the response looks normal, and the problem only shows up at step 4 — as a matched: false that never turns into true.
Step 3 — reading the liveness result
data.isAlive answers a single question: is there a live person, present at the moment of the capture? It does not identify anyone and does not compare against any document — that is what step 4 is for.
isAlive | What happened | Usual next move |
|---|---|---|
true | Live person, legitimate capture. | Move on to step 4. |
false | The analysis ran and did not approve. | Repeating the capture usually solves it: poor light, focus or framing drag the result down just as much as a fraud attempt does. |
Full response format: Liveness.
Keep the id from the response. It is the livenessRequestId of the next step, and without it the session cannot be recovered. It also comes in the Nextid-ReqId header of every response.
Step 4 — comparing against the document
Here the question changes: we already know there is a live person; what is left is whether it is the person on the document.
The endpoint is Face Match for Liveness, and the chaining happens through the URL:
curl -i -X POST 'https://api-homolog.nxcd.app/face-match-for-liveness/13cfec7c-b238-4820-a4d1-5173e4c1418e/v2' \
--header 'Authorization: ApiKey YOUR_API_KEY' \
--form 'documento=@./documento-frente.jpg'The 13cfec7c-… in the path is the id that came from step 3. You send the document only — the selfie is already stored in the liveness session, and sending it again would be comparing the image against itself.
The response is the shortest body in the whole API:
{ "matched": true }No envelope, no id, no confidence. The identifier of this request is in the Nextid-ReqId header.
Only call this step when isAlive is true
A rejected session returns matched: false without running any comparison — and a 200 response is billed all the same. Checking the result of step 3 first saves a call and avoids reading as "different faces" what was merely "there was nothing to compare".
What if the selfie did not come from a liveness session?
Then the endpoint is a different one: Face Match takes both images in the same request and returns the full envelope, with confidence. Use /face-match-for-liveness when the selfie was already captured by the SDK, and /face-match when you have both files in hand.
Common errors in the flow
| Symptom | Likely cause | What to do |
|---|---|---|
401 in the SDK, in the middle of the capture | Expired token. The ttl is too short, or the token was issued long before the capture started. | Issue the token right before handing it to the SDK and size the ttl by the capture window. |
500 on /auth-jwt | ttl sent as a number (900) or as a string with no unit ("900", read as 900 milliseconds). | Send "15m", "1h", "30s". |
401 on /auth-jwt | API key missing, invalid, or without permission to issue tokens. | Check the key and the environment. The token inherits the permissions of the key: if the key does not have the product, neither will the token. |
matched: false always, even with the right person | The liveness session did not return data.isAlive — almost always by calling /liveness with no version in the path. | Check the version field of the step 3 response. If it is not v2, fix the SDK's URL. |
matched: false right after an isAlive: false | Expected behavior: with no approved session, nothing is compared. | Do not read it as "different faces". Repeat the capture. |
matched: false with an approved session and the right person | Wrong livenessRequestId — the id of another request, the Nextid-ReqId id of the wrong call — or no file sent. | A livenessRequestId that does not exist does not return 404: it returns 200 with matched: false. Check the id and the file. |
404 on /face-match-for-liveness | A version that does not exist in the URL, or the image of the session is no longer available. | Calling /face-match-for-liveness/{id}/v2 solves the first case: v2 is the only version. |
The full list of codes and the format of the error responses are in HTTP response codes.
The identifiers, end to end
| Identifier | Born in | Goes to |
|---|---|---|
accessToken | the response of POST /auth-jwt | the Authorization: Bearer header of the calls made by the SDK |
session id | the response of POST /liveness/v2 | the path of POST /face-match-for-liveness/{id}/v2 |
Nextid-ReqId | the header of every response | your logs — it is how support locates a call |
Next steps
- JWT token — parameters,
ttland issuing errors - Liveness — full response, errors and versions
- Face Match for Liveness — every case that leads to
matched: false - OCR + biometrics flow — the other side of onboarding: classify, extract and cross-check the data on the document