The Verification Session Lifecycle: Create, Capture, Status, Webhook, Erase
A backend guide to the five stages of a remote identity verification session: what each call does, which status to trust, how to receive callbacks, and how to design erasure and data minimisation.
You are about to wire a remote identity verification flow into a product, and you want to know what your backend has to do, in what order, and what can go wrong between the first call and the day the personal data is destroyed. This guide follows one verification session through five stages: create, capture, status, webhook and erase. It is written for the backend developer who owns the integration, and it answers one question: how should a server-side integration be built so that it reads results correctly, stays secure and keeps no more personal data than it needs?
The guide describes the NeuroVision session model as documented in the public documentation and in the REST API description, and it draws general practice from the HTTP specification, the OWASP API security material, the browser media capture documentation, the General Data Protection Regulation (GDPR) and European and UK regulator guidance. It gives no address, no key and no value that belongs to a real account. Samples use placeholders in angle brackets, which you replace with the values provided with your access.
The lifecycle at a glance
A verification session is one verification of one user, created from a scenario. The scenario is the recipe: its steps, documents, countries, thresholds and rules, built in a no-code editor, and each scenario has an identifier. The session has a status and a list of results, one per step. The user sees the widget, a branded web screen that runs in desktop and mobile browsers in 29 interface languages.
The documentation describes the request flow in four moves. Your backend creates a session through the REST API, your page opens the widget for that session, the user completes the steps, and your backend reads the result or receives a webhook. Erasure is the fifth stage, and it is far easier to design while the integration is being built than after it ships. The quick start lists the six operations of the lifecycle: create session, session status, sessions filter, change status, erase personal data and erasure log. All calls are POST requests under the API address that is provided with your access.
Stage one: create
Authenticate on the server
A session is created with a server-to-server call.
You create a permanent API token in the dashboard and send it in the JSON body of the request as token.
The token belongs on your server only: never in browser code, a mobile app or a repository.
A second token type, the user token, is issued when a dashboard user logs in, goes in the token header and serves tools that act for a signed-in dashboard user.
A permanent API token sent in the header is refused.
Two controls narrow the damage of a leaked token, and each has a boundary.
IP binding applies only to groups with strict IP binding, whose tokens carry the address they were issued to, and only to the token header; a bound token used from another address is refused with 401 and the code IP_MISMATCH.
A permanent token sent in the request body, as above, is therefore not covered by that check.
A request from an address outside your IP allowlist is refused with 403 and the message Access Denied.
The allowlist has exceptions: an ADMIN token is exempt however it was sent, and an OWNER token is exempt only when it came in the token header and not in the request body.
An empty allowlist means requests are accepted from any address, so fill it in.
When you create a token through the token methods you can also give it an expiry.
The OWASP list of API security risks lists broken authentication as its second risk and observes that compromising a system's ability to identify the client or user compromises API security overall.
The create call
The create call takes the token and the identifier of the scenario. This is the sample from the quick start, with placeholders.
curl -X POST "<API_BASE_URL>/kyc/session/create" \
-H "Content-Type: application/json" \
-d '{
"token": "<your permanent API token>",
"schemaId": "<your scenario id>"
}'
A successful call returns the session. The sample response in the quick start has this shape.
{
"sessionId": "00000000-0000-0000-0000-000000000000",
"status": "idle",
"schemaId": "<your scenario id>",
"secondsToLive": 3600,
"createdAt": "2026-01-01T10:00:00.000Z",
"errors": [],
"results": [
{ "type": "document", "status": "idle" },
{ "type": "selfie", "status": "idle" },
{ "type": "liveness", "status": "idle" },
{ "type": "faces", "status": "idle" }
]
}
Keep sessionId.
The user's uploads go against it, and the status call reads the result back with it.
The REST API description says that every other operation of the verification takes this identifier.
Read three fields of the response before you open the widget.
The status is the session status, not a success flag, and idle means the session is ready.
The results list already names the tasks that the scenario asks for, each of them still idle.
The secondsToLive field reports the seconds left before the session expires, and at zero an idle or processing session is reported as expired.
Identifiers you choose
The response also echoes two identifiers that are yours: clientKey, your own identifier for the person, and clientUser, your own identifier for the operator.
This is the first place where data minimisation enters the design.
The erase operation says in terms that clientKey and clientUser stay on the session and in the journal after the destruction of personal data, so they must not carry personal data; send an opaque identifier of your own.
Retrying a create call
A create call is not a safe thing to repeat blindly.
The HTTP specification defines an idempotent method as one whose intended effect on the server, for multiple identical requests, is the same as for a single request, and it lists PUT, DELETE and the safe methods as idempotent, which leaves POST outside the list.
So if a create call times out, you do not know whether a session exists.
Record the intent in your own database before the call, keep the returned sessionId against it once the call succeeds, and let an unanswered intent lapse, because an unused session expires on its own when its time to live runs out.
Stage two: capture
What your page does
Capture is the one stage in which your server is not on the critical path. Your page opens the widget for the session, and the user shows a document, takes a selfie and follows a short liveness step. The platform reads the document, checks it for fraud, matches the face and confirms a live person. You set the allowed domains for your widget in the dashboard when you create the API token. Your backend therefore has little to do during capture, and the main design question is what it must not do: it must not assume that a session which has been created will be finished.
Browser rules for the camera
The widget runs in a browser, and browsers apply their own rules to camera access.
The browser documentation for getUserMedia explains the mechanism.
The method prompts the user for permission to use a media input, and it returns a promise that is rejected with NotAllowedError if the user denies permission and with NotFoundError if matching media is not available.
Two consequences follow for the page that hosts the widget. First, host it over HTTPS, because in an insecure context the media devices object is undefined and the camera cannot be reached at all. Second, if the capture content sits inside a frame, check the permission policy of the page.
Sessions that nobody finishes
A user can close the tab, lose the connection or ignore the prompt.
Your backend learns about this only by the clock: the session reaches secondsToLive of zero and reports expired.
Treat expired as a normal outcome and offer the user a fresh session.
Your onboarding logic should move an applicant to "approved" only from a state in which you have read a session status of success from the platform, and never because a browser told your page that the widget closed.
Stage three: status
Read the right field
The status call reads a session back.
It takes the identifier of the session, sessionId, or your own clientKey, and one of the two is required.
The clientKey is accepted for regular sessions only and is limited to 36 characters.
For the outcome of a verification session, the documentation is firm about which field carries the verdict.
The status field of a session is the session status.
It is not the ok or error of a response envelope.
You branch on this field and never on the text of a message.
The REST API description repeats the point: a failure normally arrives as an HTTP 200 with an error status in the body, and the message text is reworded without notice, so it is illustrative and never a contract.
The HTTP specification says that a 200 response means the request has succeeded, and that for POST the content is the status of, or the results obtained from, the action.
The seven statuses
The session and each step can hold one of seven statuses.
idle: the session is created and ready, or a step has not started.processing: the session or a step is still being processed.success: the session or step completed successfully.failed: the session or step failed, and a step is forced to failed when it has errors.exception: processing ended with an exception.expired: the time to live of the session ran out.suspicious: the platform flagged the result as suspicious.
Of these, the REST API description names success, failed and suspicious as the statuses that a session first reaches as a final status, and the spent field reports the seconds from creation to that moment.
Model your own state machine on that split: idle and processing are waiting states, success, failed and suspicious are decisions, and expired and exception are outcomes that need a policy of their own, such as a new session for the user or a manual review.
It is a flag that the platform sets, and what your business does with it, for example sending it to a human reviewer, is a decision of your risk policy.
Steps, questionnaires and checks
The results list holds one entry per step, such as document, selfie, liveness and faces, and each entry carries its own status from the same list.
A questionnaire record is either idle, while the form is unfinished, or success, once it is filled in and committed, and it is not a verdict.
The status call accepts a questionnaire record too: you pass the questionnaire session identifier that the completion webhook carried, and only that identifier addresses it.
Verdicts of database checks, such as AML screening, are not part of the session body.
You read them from the database checks method after the DATABASE_CHECKS_COMPLETE event.
The REST API description explains why: checks run asynchronously and finish after the session itself, so you poll the method until checksStatus is complete, with an interval of 5 to 10 seconds described as reasonable, or you wait for the webhook that pushes the same results.
An abandoned session never reaches complete, because its stored status stays idle, so stop polling when the session TTL that you configured has passed.
A questionnaire record has no time to live at all, so poll it against the completion webhook and not to a deadline.
A scenario that configured no checks answers complete with an empty result list, which keeps "nothing was checked" distinguishable from an unknown identifier.
Manual decisions and listing
A reviewer can change a session status manually in the dashboard, or through the status change method. Store the platform's status and your own decision in separate fields, so that a later manual change never overwrites what the platform said.
Stage four: webhook
The four events
A webhook lets the platform tell your backend when something completes. There are four events.
SESSION_COMPLETE: a verification session completes.QUESTIONNAIRE_COMPLETE: a questionnaire step is filled in and committed.DATABASE_CHECKS_COMPLETE: the database checks of a session, such as AML screening, complete.PD_ERASURE_COMPLETE: the destruction of personal data requested through the erase call finishes.
You enter the callback URL of your backend in the callback settings of the scenario. Four options sit beside it: encrypt the body with the secret key of the scenario, omit identity fields, notify only when the session is finalized, and send database checks as they become ready.
Treat the event as a signal
The documentation gives the receiving rule in one sentence. Treat the event as a signal and fetch the result by the session identifier through the REST API. There are two reasons to follow it.
The first is trust. The OWASP list of API risks names unsafe consumption of APIs, and its description says that developers tend to trust data received from third-party APIs more than user input and so adopt weaker security standards. An endpoint that is open to the internet and accepts a POST from the platform is also open to anyone else who learns its address. Fetching the result over your own authenticated call is a good start, but it does not make a forged event harmless. The same OWASP list names broken object level authorization as its first risk: endpoints that handle object identifiers create a wide attack surface, and object level authorization checks should be considered in every function that accesses a data source using an ID from the user. A session identifier in a callback body is such an identifier, supplied by whoever posted the request. Before the handler fetches or applies anything, it should therefore match the identifier to a session that your own system created, for the applicant and the workflow it expects, and ignore events for any other identifier. The list also names unrestricted resource consumption: serving requests takes network, processor and memory resources, some resources such as biometrics validation are paid for per request, and successful attacks can lead to denial of service or higher operational costs. A flood of forged events, each triggering a fetch, costs requests, so deduplicate events, limit how many fetches one source can trigger, and cap the work done per event. Replay is the third concern, because an old and genuine event can be posted again; the idempotent receiver described below answers it.
The second is minimisation.
With the option to omit identity fields, the callback changes per event.
On SESSION_COMPLETE, names, document data and images stay out of the callback, and the session identifier stays in, which links to the session.
Your system fetches the result by that identifier after the event arrives.
On QUESTIONNAIRE_COMPLETE, the entered values (full name, date of birth, email, phone and the values of your own fields) are dropped, while the identifiers, the key and the title of each field stay.
The check results of a questionnaire, when the scenario has checks, are sent in full whatever the option, and so are the results in DATABASE_CHECKS_COMPLETE, because without the verdict a skipped check would read as clean even on a match.
The option therefore does not remove check results from a callback, and the endpoint and the logs in front of it are a place where such results can appear.
Encrypt or omit
The two protective options solve different problems. Encryption protects the content of a body that you do decide to receive: you decrypt it with the secret key of the scenario. The option to omit identity fields reduces the identity fields that are sent, and it leaves the check results in the callback. Use both when your endpoint passes through shared infrastructure, because encryption is the option that also covers the check results, and use omission by default when your backend can fetch what it needs.
Build an idempotent receiver
The documentation does not tell you how often an event may be delivered, and you should not assume a number. Build the receiver so that the count does not matter. The HTTP specification defines the property you want: the effect of multiple identical requests is the same as the effect of one. In practice that means keying your processing on the session identifier and the event name, so that a duplicate delivery changes nothing. The database check event needs more care, because the REST API description says that it carries the full result set every time and is re-sent whenever the session returns to a fully terminal state, so it is to be treated as idempotent by session identifier and not as a one-shot event. A duplicate delivery and a later re-sent result set look alike on arrival, and a permanent flag that the event was handled would discard a legitimate update. On each arrival, reconcile or refetch the current results for the session identifier, and guard the downstream actions, for example by acting only when the outcome has changed, so that a repeated set does not repeat a payment, a message or a block. The REST API description does this itself for the erasure event: it says to treat the event as idempotent by session identifier, and it notes that the event is sent once per session and not delivered again, so a lost delivery is recovered by calling the erase method, which answers with the same erasure time.
A few more habits make the receiver safe. Answer quickly and move the real work to a queue. Serve the endpoint over HTTPS only, and keep tokens and keys out of its URL, because the OWASP cheat sheet notes that credentials in a URL can be captured in web server logs. Log the event name, the session identifier and the time, and do not log decrypted bodies.
Stage five: erase
What the call does
The erase call destroys the personal data of a session on request.
The data covered are document data, contacts, images and video, and the person record that the session created, unless another session or a manual entry still holds it.
You send either sessionId or clientUser, exactly one of them: with clientUser the call destroys the personal data of every session of your group with that value.
The clientUser is from 1 to 36 characters once the edge spaces are cut.
The caller needs the kycSessionDelete permission, and the session itself stays, with its verdict and without the personal data.
You can state the cause of the destruction, which is written to the destruction journal.
The values are subject_request, consent_withdrawn, purpose_achieved, unlawful_processing and operator_request, and the last is recorded when you give none.
The erasure log method exports the journal for a period: what was destroyed, when, why and by whom.
Two answers, one rule
The response is where careless integrations fail.
The REST API description says to branch on result in the body, not on the HTTP code or on status.
erased is the only outcome that means the destruction is finished, and status is ok while the work is still going on, in which case result is in_progress.
The call waits about 20 seconds, and if part of the destruction could not be confirmed in that time, for example because the media storage did not answer, it starts the destruction and reports what it has.
The system finishes the work itself and sends the PD_ERASURE_COMPLETE webhook, the caller has nothing to repeat, and a repeated call meanwhile returns the current state.
The body also reports each kind of data.
The parts object gives the state of the text data and of the media: erased, kept, unconfirmed or absent.
A kept state carries a reason: another session of the same person still holds the record, the record was entered by hand, or the record is on a blacklist.
Your integration should store the parts object with the erasure time, and it should show the kept reasons to whoever answers a data subject's request, because "erased, except for the record that another session holds" is a different answer from "erased".
After the destruction
A session that the user is still passing is not refused: the call closes it and destroys its data, the widget shows the session as finished, and further uploads to it are refused.
Repeating the call, also after the session was deleted from the cabinet, answers the same, with the same erasure time.
Creating or opening a session with the same clientKey gives back that finished session and not a new one, so to verify the person again you create a session with a new clientKey.
A session object whose destruction has begun carries pdErased set to true, keeps its identifiers, statuses, errors and verdicts, and carries no recognised value, contact, device value or media link.
Mirror that in your own database: a flag, a timestamp and no copy of the data that was destroyed.
Why the erase stage is a design stage
The GDPR sets the frame in which erasure is requested.
Article 17 gives the data subject the right to obtain from the controller the erasure of personal data without undue delay, and it lists grounds that include data that are no longer necessary for the purposes for which they were collected, withdrawn consent where there is no other legal ground for the processing, and unlawful processing.
The right does not apply to the extent that processing is necessary, among other cases, for compliance with a legal obligation that requires the processing or for the establishment, exercise or defence of legal claims.
The grounds line up with the reasons that the erase call accepts: purpose_achieved for the first, consent_withdrawn for the second and unlawful_processing for the third.
Map them deliberately, because the second applies only where no other legal ground supports the processing, and a legal obligation or a legal claim can justify keeping data that a request would otherwise reach.
The UK regulator's guidance on the right to erasure adds a clock: the individual must be informed without undue delay and within one month of receipt of the request.
It also addresses backups, which are the part of erasure that engineers forget: the key issue is to put the backup data beyond use, even if it cannot be immediately overwritten, and to be clear with individuals about what will happen to their data.
The erase call destroys what the platform holds. It cannot reach the copies that you made. Your erasure procedure must therefore cover your own database, your logs, your queues, your analytics exports and your backups, in the order and with the evidence that your retention policy states.
Minimisation as a property of the integration
Article 5 of the GDPR states the principles in a form that an integration can be checked against. Personal data shall be adequate, relevant and limited to what is necessary in relation to the purposes for which they are processed, which is data minimisation.
The European Data Protection Board guidelines on data protection by design and by default turn this into design elements that map to the lifecycle. Its list of key minimisation elements includes data avoidance, limiting the amount collected, access limitation, pseudonymisation as soon as direct identification is no longer necessary, deletion where data is no longer necessary, and a data flow that does not create more copies than necessary. Its storage limitation elements include clear internal procedures and functionalities for deletion, automation of deletion, a justification for the storage period, enforcement and testing of retention policies, and attention to backups and logs.
Applied to the five stages, the elements give a short list of rules.
- Create: send an opaque
clientKeyandclientUser, and keep the mapping to a person in your own system, not in the identifier. - Capture: collect only the steps that the scenario needs for the purpose, because an extra step is extra personal data.
- Status: store the session identifier, the status and your decision, and fetch the details only when a person with a reason to see them asks.
- Webhook: omit identity fields from the callback and fetch on demand, and encrypt the body when check results must not appear in the logs of intermediaries.
- Erase: automate the call from your retention schedule, record the answer, and clean your own copies in the same job.
The retention period itself is a scenario setting. The guidelines say that if personal data is no longer necessary for the purpose of the processing, then it shall by default be deleted or anonymized, and that the controller should be able to justify why the period of storage is necessary.
A failure checklist for the integration
Use these questions in a code review before launch.
- For the outcome of a verification session, does the code decide from the
statusfield of the body only, never from message text or the HTTP code alone (the erase call has its own rule, in the erasure item below)? - Is the permanent API token kept on the server, with the IP allowlist filled in?
- Does a create call that times out leave an intent record and not a second session?
- Is the page that hosts the widget served over HTTPS, with camera permission granted to any frame that needs it?
- Does the application approve only after a server-side read of a
successstatus, with a policy forfailed,suspicious,exceptionandexpired? - Is the webhook handler idempotent by session identifier and event name, does it reconcile a re-sent database check result set instead of discarding it, does it match the identifier to a session your own system created before it fetches or applies anything, and does it fetch the result instead of trusting the body?
- Does the erasure job branch on
result, wait for the webhook onin_progress, and store the answer? - Are the identifiers sent to the platform opaque, and does your own erasure cover your database, logs and backups?
Where this sits in a verification flow
A verification flow built on NeuroVision KYC places the steps in one session: document, selfie, liveness and face match, with the scenario assembled in an editor. The dashboard provides a session log, reports and export, manual decision, roles and a user list. Erasure endpoints, a callback option that omits names, document data and images from the session event while keeping the session identifier, which links to the session, and encryption of the callback body give the integrating team controls for deletion and data minimisation, and retention in days is a setting of each scenario. The check results of database checks stay in the callback whatever that option says. Callback bodies can be encrypted.
To see how these steps look in your own scenario, contact the team.
Sources
All sources were fetched on 27 October 2026.
- RFC 9110, HTTP Semantics, Internet Engineering Task Force, published through the RFC Editor.
- Regulation (EU) 2016/679 (General Data Protection Regulation), Official Journal text, European Union.
- MediaDevices: getUserMedia() method, MDN Web Docs.
- OWASP Top 10 API Security Risks, 2023 edition, OWASP Foundation.
- REST Security Cheat Sheet, OWASP Cheat Sheet Series.
- Guidelines 4/2019 on Article 25, Data Protection by Design and by Default, European Data Protection Board.
- Right to erasure, Information Commissioner's Office, United Kingdom.