Skip to content

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.

  1. Call /liveness/v2, always with the version in the path. It is the v2 envelope — with the result in data.isAlive — that the next step reads. A URL with no version does not guarantee that response, and the face match answers matched: false forever, with no error at all, whenever it does not find data.isAlive.
  2. /face-match-for-liveness returns only {"matched": true} or {"matched": false}. No id, no version, no data, no metadata. It is the exception to the API's standard envelope, and a client expecting data gets undefined.

Overview

#Who callsWhatWhat comes back
1Your backendPOST /auth-jwtaccessToken
2Your backend → SDKhands the token over to the device
3SDK, on the devicePOST /liveness/v2 with the selfie capturedsession id and data.isAlive
4Your backendPOST /face-match-for-liveness/{id}/v2 with the documentmatched

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-jwt is 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 ttl should 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.
  • ttl is a string with a unit"15m", not 900 and not "900". That is the most common cause of 500 on 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.

PlatformSample repository
Androidliveness-sdk-android-sample
iOSliveness-sdk-ios-sample
Webliveness-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.

isAliveWhat happenedUsual next move
trueLive person, legitimate capture.Move on to step 4.
falseThe 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:

bash
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:

json
{ "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

SymptomLikely causeWhat to do
401 in the SDK, in the middle of the captureExpired 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-jwtttl sent as a number (900) or as a string with no unit ("900", read as 900 milliseconds).Send "15m", "1h", "30s".
401 on /auth-jwtAPI 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 personThe 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: falseExpected 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 personWrong 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-livenessA 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

IdentifierBorn inGoes to
accessTokenthe response of POST /auth-jwtthe Authorization: Bearer header of the calls made by the SDK
session idthe response of POST /liveness/v2the path of POST /face-match-for-liveness/{id}/v2
Nextid-ReqIdthe header of every responseyour logs — it is how support locates a call

Next steps

Nextcode | Identity Verification Solutions