Identity verification integration
This guide takes a backend engineer from a provisioned account to a working hosted identity verification flow. Your server creates a session, your customer completes the steps on a page hosted by Noxtica, and your server reads the outcome by a status read or a signed event. Your server never receives the customer’s answers or capture directly.
Results are automated signals. You decide what each outcome means for your customer; Noxtica hands you the evidence and an audit trail.
Key points
- Your server creates the sessionIt uses your own opaque customer reference and receives a single-use link. The server key stays on your server.
- The customer finishes on a hosted pageThe page is hosted by Noxtica and runs in the customer’s own browser. Your server never receives the customer’s answers or capture directly.
- The status read is the authorityA signed event saves you from polling. Read the session status before you act on it, and decide what each outcome means for your customer.
Availability
Identity verification has Controlled availability. A tenant needs the module provisioned by the account team, and the account team confirms what applies to your tenant. Steps described here as optional apply only when they are enabled for your organisation. See the product page for the product overview and the limits of the result.
Choose how to integrate
Three routes lead to a verification. Pick by who builds the capture experience and who sends the link.
| Route | Who creates the link | Who builds the capture experience | Guide |
|---|---|---|---|
| Hosted session from your server | Your server, by API | Noxtica hosts the page; you send the customer to it | This page |
| Invitation from Backoffice | A person in your organisation, in Backoffice | Noxtica hosts the page | Invitations and hosted verification settings |
| Direct API from your server | Not applicable | Your own application | Self-hosted identity verification (direct API) |
All three need the module provisioned for your tenant and, where an API is involved, a server key with the identity permission. Hosted sessions and invitations also need a recorded legal basis and share the settings described in Invitations and hosted verification settings.
How the flow works
- Your server creates a session with your own opaque customer reference. The response carries a
sessionIdand a single-use link. Store thesessionIdnext to your customer record. - You send the customer to the link: a full-page redirect, a new tab, an email or message, or a QR code.
- The customer completes the steps on the hosted page in their own browser.
- Noxtica processes the submission. A finished capture currently waits in the review queue in your Backoffice (
review_pending) until someone in your organisation records a decision. - Your server learns the outcome by reading the session status, or from a signed
kyc.session.completedevent. Read the status before you act on an event. - If you set a return link, the customer can go back to your site from the hosted page after a completed outcome. The return link is a convenience, not proof that verification succeeded.
Your server
Creates the session, reads the outcome
Your customer
Opens the link, completes the steps
Noxtica
Hosted page and processing
Your organisation
Records a decision in the Backoffice
Create a session
Your server creates a session with your own opaque customer reference. The response carries a sessionId and a single-use link. Store the sessionId next to your customer record.
Send the customer to the link
A full-page redirect, a new tab, an email or message, or a QR code.
The customer completes the steps
The customer works through the steps on the hosted page, in their own browser.
Processing and review
Noxtica processes the submission. A finished capture currently waits in the review queue in your Backoffice until someone in your organisation records a decision.
Your server learns the outcome
By reading the session status, or from a signed event. Read the status before you act on an event.
Before you start
Complete these once per environment:
- Module provisioned. Your account team provisions identity verification for your tenant. Until then every request below receives a plain
404. - Hosted verification page configured. In the Backoffice, open Identity Verification → KYC Invitations → Settings and fill in the business name, privacy notice link and purpose text the customer sees. The KYC pages show a readiness checklist that names anything still missing. Until the page is configured the customer sees a “Verification isn’t ready yet” message.
- Several domains (optional). If your organisation runs more than one domain, each can carry its own business name, privacy link and support link. See Multiple domains.
- Legal basis recorded by your organisation. An authorised person in your organisation records the legal basis for hosted identity verification in the same Settings page. Noxtica does not choose one for you. Until it is recorded, session creation is refused.
- A server key with the identity permission. In the Backoffice, open API & SDK → Server API Keys and create a key with the “Verify identity documents (KYC)” permission ticked. A new key defaults to a narrower permission that cannot create sessions. The secret is shown once, so save it in your server secret store. Server keys start with
sk_prod_. - Approved return hosts (optional). If you want a return link, add the host names of your return pages under “Approved return destinations” in the same Settings page. Saving that page asks for fresh step-up verification.
- An events endpoint (optional). If you do not want to poll, register an HTTPS endpoint under Integrations → Webhooks, subscribe it to the identity events you need, and save the signing secret when it is shown. The secret is shown once.
Keys and base address
A key’s prefix tells you what it is: server keys start with sk_prod_. Call https://collect.noxtica.com with it. A key that is missing, malformed, unknown or revoked is refused with 401 invalid_api_key.
- Keep each key in its own secret store entry.
- A signing secret belongs to one endpoint.
- The Identity Verification → KYC Verifications → Integration tab shows ready-to-copy samples.
Check capacity before starting a verification
This step is optional. Identity verification runs on a shared pool of inference capacity, and a verification started while the pool is busy can take longer than usual. GET /v1/kyc/load tells your server how busy the pool is right now and whether this is a good moment to start a verification, so you can hold a customer back, or tell them to expect a wait, instead of sending them into a flow that may be slow.
The answer is advice. It reserves nothing and refuses nothing: a verification you start while the advice is defer is still processed, with the limits it always had.
GET https://collect.noxtica.com/v1/kyc/load
Authorization: Bearer sk_prod_your_server_key
The route needs a server key with the identity permission, the same “Verify identity documents (KYC)” permission (write:identity) that session reads use. There is no separate key for it. It is available when identity verification is provisioned for your tenant and the capacity read is enabled for your organisation; until then it answers the same plain 404 as an unknown path. Ask your account team to enable it.
What the response contains
The status is always 200 for a request that was served, including when the load could not be read (see When the load cannot be read).
{
"ok": true,
"status": "ok",
"recommendation": "accept",
"reasons": [],
"capacity": 8,
"inUse": 5,
"queueDepth": 0,
"estimatedWaitMs": 0,
"processingMs": { "p50": 400, "p95": 1500, "samples": 256 },
"yours": { "inFlight": 1 },
"asOf": "2026-10-28T17:31:08.000Z",
"ttlSeconds": 8,
"requestId": "<request-id>"
}
| Field | Meaning |
|---|---|
status | ok when the numbers are real. unknown when Noxtica could not read its own load; then every platform number is null. |
recommendation | accept, degraded or defer. See The three recommendations. |
reasons | Why the recommendation is not accept, most serious first. Empty for accept. Ignore a value you do not recognise: the list can grow. |
capacity | Concurrent inference slots ready right now. It is not a number of sessions: see the note below. 0 when none is ready. |
inUse | Slots running a job right now, across all customers. |
queueDepth | Jobs, across all customers, waiting for a slot. |
estimatedWaitMs | How long a job that arrives now would wait before it starts, in milliseconds. It does not include the job’s own processing time. 0 when a slot is free. |
processingMs | How long recently completed jobs took, in milliseconds: p50 (median), p95, and samples (how many jobs they come from, up to 256). Each value is the upper edge of a duration band, so it is rounded up. null when there are no recent jobs. |
yours.inFlight | The number of verifications of your own organisation that are running right now. It is the only value that belongs to you. null when it could not be counted. |
asOf | The time, in ISO 8601 UTC, at which the platform numbers were taken. |
ttlSeconds | How long Noxtica keeps those platform numbers before it reads them again: 8 seconds, or 5 when status is unknown. |
requestId | Identifies the request. Quote it when you contact support. |
Capacity counts slots, not sessions. A slot runs one inference job at a time. One verification uses a slot for each of its steps, and each step typically takes from a few tens of milliseconds to a few seconds, so a slot is busy only briefly for each verification. "capacity": 8 means eight inference jobs can run at the same moment. It does not mean eight customers can be verified at the same moment, and you cannot turn it into a customer limit by dividing it by a fixed number. Read recommendation, estimatedWaitMs and queueDepth instead.
The three recommendations
recommendation | What it means | What to do |
|---|---|---|
accept | Capacity is available and nothing is queued. | Start the verification. |
degraded | Verifications are still accepted and will finish, but expect them to be slower than usual, or the margin is thin. | Start it if your journey tolerates extra time, and consider a “this can take a little longer than usual” message. Hold back only if you must be fast. |
defer | The pool is saturated or has no ready capacity. A new job is likely to wait several seconds, or to be refused as overloaded. | Do not send the customer in yet. See What to do on defer. |
The recommendation follows the reasons: any reason from the defer group gives defer; otherwise any reason from the degraded group gives degraded; otherwise accept. These are the thresholds:
| Reason | Group | It is present when |
|---|---|---|
no_ready_instance | defer | No capacity is ready to take work, so capacity is 0. |
queue_wait_high | defer | estimatedWaitMs is 6000 ms or more, or reaches the platform’s own queue-wait limit when that is lower. |
queue_full | defer | queueDepth is at least capacity. |
recent_overload | defer | A request was refused for overload, queue timeout or memory pressure within the last 30 seconds. |
high_utilization | degraded | inUse divided by capacity is 0.75 or more. |
queue_building | degraded | queueDepth is greater than 0. |
wait_elevated | degraded | estimatedWaitMs is 1500 ms or more. |
reduced_capacity | degraded | Part of the platform’s capacity is out of service, so capacity is lower than usual. |
escalation_unavailable | degraded | The extra stage that re-examines difficult cases is unavailable. Verifications continue without it. |
escalation_saturated | degraded | That extra stage is running at full capacity, so difficult cases can wait. |
vram_pressure | degraded | Accelerator memory is low on part of the capacity, which slows or refuses new jobs. |
slow_processing | degraded | processingMs.p95 is 10000 ms or more, over at least 20 samples. |
load_unavailable | degraded | status is unknown. |
The numbers are an aggregate across all customers and are sampled, not reserved for you. Several of your requests can read accept in the same second and start together, so the advice lowers the chance of a long wait but is not a promise of a free slot.
When the load cannot be read
If Noxtica cannot read its own load (a timeout, an unreachable service or an invalid answer), the route does not fail. It answers 200 with "status": "unknown", "recommendation": "degraded", "reasons": ["load_unavailable"] and null for capacity, inUse, queueDepth, estimatedWaitMs and processingMs. yours.inFlight is still reported when it can be counted, and ttlSeconds is 5.
degraded here is deliberate: a problem with the load read must not make you stop every verification. Treat unknown as “no advice”: you are still allowed to start verifications. If the verification service itself is unavailable, the verification call reports that on its own.
Polling and caching
- Noxtica caches the platform numbers. They are computed once and shared by all customers for
ttlSeconds(8 seconds, or 5 forunknown).yours.inFlightis computed on each request. The response carriesCache-Control: private, max-age=5. - Poll no faster than every 10 seconds. A faster poll returns the same snapshot.
- Never call it once per end-user page view. Poll from your server on a timer, or when a customer is about to start, keep the answer in your own cache, and share it between all your users.
- The route is rate limited per key, currently 30 requests per minute. A limited call answers
429with aRetry-Afterheader (andretryAfterin the body); wait that many seconds before the next call. The limit can change, so do not build on the number.
What to do on defer
When the recommendation is defer, do not send the customer into the verification yet:
- Queue the customer. Keep them in your own queue instead of sending the link, or before you submit their captures on the direct API. Nothing is held on Noxtica’s side, so a customer who leaves costs nothing.
- Show a wait message. Tell the customer that verification is busy and that you will continue shortly. Do not show an error: nothing has failed.
- Retry with backoff. Ask again after the larger of
estimatedWaitMsand your poll interval, and lengthen the delay on each furtherdefer(for example 10, 20, then 40 seconds, capped at a couple of minutes, with some random jitter). Use one shared poller for all queued customers, not one poll per customer. - Set a limit. If the platform stays on
deferlonger than your journey can wait, offer the customer a way back later, for example by message, rather than a spinner that never ends. - Release in small steps. When the advice returns to
acceptordegraded, release the queue gradually rather than all at once, so you do not recreate the burst.
Examples
curl -sS https://collect.noxtica.com/v1/kyc/load \
-H "Authorization: Bearer $NOXTICA_SERVER_KEY"
From your server, read it once per interval and share the answer between all your users. The sample turns the recommendation into a yes or no: accept is yes, degraded is yes when your journey tolerates extra time, defer is no.
const POLL_INTERVAL_MS = 10_000;
let cached = { at: 0, load: null };
async function readLoad() {
if (cached.load && Date.now() - cached.at < POLL_INTERVAL_MS) return cached.load;
const response = await fetch('https://collect.noxtica.com/v1/kyc/load', {
headers: { Authorization: `Bearer ${process.env.NOXTICA_SERVER_KEY}` },
});
if (!response.ok) throw new Error(`capacity read failed: ${response.status}`);
cached = { at: Date.now(), load: await response.json() };
return cached.load;
}
export async function checkCapacity({ allowDegraded = true } = {}) {
try {
const load = await readLoad();
return {
start: load.recommendation === 'accept' || (load.recommendation === 'degraded' && allowDegraded),
retryAfterMs: Math.max(load.estimatedWaitMs ?? 0, POLL_INTERVAL_MS),
};
} catch {
// The advice is optional: if the read fails (for example 404 or 429), start the verification.
return { start: true, retryAfterMs: 0 };
}
}
Errors
| Status | Error | Meaning | What to do |
|---|---|---|---|
401 | invalid_api_key | The key is missing, malformed, unknown or revoked. | Check the Authorization header. |
403 | API key does not have write:identity scope | The key lacks the identity permission. | Use a key with “Verify identity documents (KYC)” ticked. |
404 | V1 endpoint not found | The capacity read is not enabled for your organisation, or the path is wrong. | Check the path. If it is right, ask your account team. Do not retry in a loop. |
429 | Rate limit exceeded | Too many requests for this key. | Wait Retry-After seconds, then poll less often. |
503 | tenant_module_gate_unavailable | A temporary fault while checking that identity verification is enabled for you. | Retry after Retry-After seconds (2). |
A load that cannot be read is never an error: it is a 200 with status: unknown. The shared error envelope is described in Errors.
What the response reveals
The answer carries aggregated platform numbers, the same for every customer, and your own in-flight count. It contains no personal data, no other customer’s data, and nothing about how the capacity is built or where it runs. Nothing about the call is stored beyond the standard request log.
Create a session
Send the request from your server. Never place the server key in a browser or a mobile app.
POST https://collect.noxtica.com/v1/kyc/sessions
Authorization: Bearer sk_prod_your_server_key
Content-Type: application/json
Idempotency-Key: your-retry-key
Request fields
The body is a JSON object. Field names are exact: an unknown field is ignored without an error, so a misspelt name silently falls back to its default.
| Field | Type | Required | Notes |
|---|---|---|---|
subjectRef | string | Yes | Your own opaque reference for the customer, 1 to 256 characters. Use an identifier from your system, not a name or an email address. Status reads return it to you; events never carry it. |
expiresInSeconds | integer | No | How long an unopened link stays valid: 300 to 604800, default 86400. A value outside the range is rejected with a 400, not adjusted. |
returnUrl | string | No | An https address whose host exactly matches an approved return host, up to 2048 characters. Any other host is rejected with return_url_not_allowed. |
purpose | string | No | Short text shown to the customer on the hosted page, up to 500 characters. When you omit it the default purpose from your hosted page settings is used. |
idType | string | No | An optional hint for the document type. Leave it out unless your account team asks you to send it; the customer chooses on the page. |
domainId | string | No | The registered domain this link is created for. The hosted page then shows that domain’s business name, privacy link and support link. See Multiple domains. |
siteKey | string | No | The site key of a registered domain, as an alternative to domainId. Naming two different domains is rejected with domain_mismatch. |
hostedOptions | object | No | Per-session switches for optional steps. Leave it out to use your defaults. If you send it, send the complete object your account team supplies; a partial object is rejected with a 400. |
The request has no field for language, webhook address, or customer contact details. The hosted page follows the customer’s browser language, and you register webhook endpoints in the Backoffice. Without domainId and siteKey, the link uses your organisation’s hosted page configuration, as it always did; Multiple domains explains how a domain changes that.
Response
A new session returns 200 with this shape:
{
"ok": true,
"sessionId": "<uuid>",
"url": "https://verify.noxtica.com/s/<token>",
"expiresAt": 1790848845000,
"domainId": null,
"requestId": "<request-id>"
}
sessionIdis a UUID. Store it with your customer record.urlis the single-use link. It is returned once, in this response, and no later call returns it again. Treat it as a secret: do not log it and do not put it in an analytics address.expiresAtis a time in epoch milliseconds, not seconds.domainIdis the domain the link was created for, ornullwhen it uses your organisation’s configuration.requestIdidentifies the request. Quote it when you contact support.
Idempotency
Idempotency-Key is optional and must be 8 to 128 characters when present. Without it, every call creates a new session. With it, a retry that carries the same key returns the same sessionId with "replayed": true and no url. The key alone identifies the session: a different body under the same key returns the original session.
If a request times out and you never saw the link, you cannot recover it. Create a new session with a new key and let the old one expire.
Examples
curl -sS https://collect.noxtica.com/v1/kyc/sessions \
-H "Authorization: Bearer $NOXTICA_SERVER_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"subjectRef":"cust_example","returnUrl":"https://merchant.example.com/verification/return"}'
const response = await fetch('https://collect.noxtica.com/v1/kyc/sessions', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.NOXTICA_SERVER_KEY}`,
'Content-Type': 'application/json',
'Idempotency-Key': crypto.randomUUID(),
},
body: JSON.stringify({
subjectRef: 'cust_example',
returnUrl: 'https://merchant.example.com/verification/return',
}),
});
const body = await response.json();
if (!response.ok) throw new Error(`session creation failed: ${response.status} ${body.error}`);
// Store body.sessionId with your customer record, then send body.url to the customer.
import json
import os
import urllib.request
import uuid
request = urllib.request.Request(
"https://collect.noxtica.com/v1/kyc/sessions",
data=json.dumps(
{
"subjectRef": "cust_example",
"returnUrl": "https://merchant.example.com/verification/return",
}
).encode(),
headers={
"Authorization": "Bearer " + os.environ["NOXTICA_SERVER_KEY"],
"Content-Type": "application/json",
"Idempotency-Key": str(uuid.uuid4()),
},
method="POST",
)
with urllib.request.urlopen(request) as response:
body = json.load(response)
# Store body["sessionId"] with your customer record, then send body["url"] to the customer.
Send the customer to the link
Choose the delivery that fits your journey:
- Redirect. Send the customer’s browser to
urlas a full page. This is the simplest path. - New tab. Open
urlin a new tab or window from your own page. - Email or message. Send the link through your own channel.
- QR code. Show the link as a QR code so the customer can continue on a phone. The hosted page also offers a QR code and a copy-link option for moving to a phone.
What the hosted page does and does not do:
- It cannot be embedded. The hosted page refuses to load inside an iframe. Open it as a full page or in a new tab.
- It does not message your page. The page does not post a message to the window that opened it, and it does not close itself. Do not wait for a script on your side to learn the outcome.
- It follows the customer’s language. The language comes from the customer’s browser. You cannot set it, and the link accepts no parameters beyond its own path.
- Some browsers cannot be used. Some in-app browsers are not supported. The page tells the customer how to continue in their regular browser.
Return link
The return link is optional.
- Set
returnUrlwhen you create the session. Its host must be on your approved return hosts. - After a completed outcome, the hosted page shows a plain “Return to your business” link to exactly that address. There is no automatic redirect, and no query parameters are added, so the address carries no session id or status.
- The link is not shown after a review, unverified, error, cancelled or expired outcome.
- Invitations created from the Backoffice carry no return link.
Because a finished capture currently waits for a decision, most customers do not reach a return link straight away. Design your journey so that it does not depend on the customer coming back: show your own “we will let you know” state, and finish through the status read or the event. Correlate with the sessionId you stored, and never treat a visit to your return page as proof of completion.
Lifetimes
An unopened link stays valid until its own expiry, one day by default. After the customer first opens the link, the customer has 30 minutes to finish, or until the original expiry if that comes sooner. A session that waits in review_pending does not expire while it waits for a decision. KYC Hosted Sessions explains expiry and the 410 responses the hosted page itself returns.
Get the outcome
Two routes lead to the outcome: a status read, and a signed event. Use both. The event saves you from polling, and the status read is the authority.
Read the session status
GET https://collect.noxtica.com/v1/kyc/sessions/{sessionId}
Authorization: Bearer sk_prod_your_server_key
A session you created always returns 200, including an expired one, which reads "status": "expired". A session id that does not exist, or that belongs to another organisation, returns 404.
{
"ok": true,
"session": {
"id": "<uuid>",
"status": "review_pending",
"subjectId": "cust_example",
"domainId": null,
"expiresAt": 1790763900000,
"completedAt": 1790762445000,
"verificationId": "<verification-id>",
"reasonCodes": ["<reason-code>"],
"returnUrl": "https://merchant.example.com/verification/return",
"decision": {
"outcome": "review_pending",
"reasonCodes": ["<reason-code>"],
"decidedAt": 1790762445000
}
},
"requestId": "<request-id>"
}
- Everything sits under
session.session.idis thesessionIdfrom creation, andsubjectIdis thesubjectRefyou sent. - Timestamps are epoch milliseconds, or
nullwhen the moment has not happened. decisionappears once the session has an outcome. Itsoutcomeisapproved,rejected,review_pendingorerror.verificationIdidentifies the verification case, so you can find it in the KYC pages of the Backoffice.domainIdis the domain the session was created for, ornullfor your organisation’s configuration.- When Noxtica Verification IDs are enabled for your organisation, the session also carries
nvid. - The response can carry more fields than this page lists. Ignore fields you do not use and do not fail on unknown ones.
Poll with a delay that grows between reads and stop at a final status. Do not poll in a tight loop; rate limits apply.
Statuses
| Status | Meaning | Event |
|---|---|---|
pending | Created. The customer has not opened the link. | None |
opened | The customer opened the link and is working through the steps. | None |
verifying | The customer submitted and processing is running. | None |
review_pending | Processing finished and a decision is needed in your Backoffice review queue. Not a rejection, and not an approval. | None yet. kyc.session.completed follows when a decision is recorded. |
completed | A decision approved the session. | kyc.session.completed with status completed |
unverified | Processing finished with a negative outcome. | kyc.session.completed with status unverified |
error | Processing could not finish, because of a fault or a refusal. | kyc.session.completed with status error |
expired | The link or the capture window lapsed before a decision. | None. Read the status to observe expiry. |
cancelled_by_user | The customer chose to cancel on the hosted page. This applies when cancelling is enabled for your organisation. | kyc.session.cancelled |
Only completed is an approval. Treat every other status as not approved until you have read a later one.
Status read
Your server asks for the session. This is the authority.
How it behaves
- A session you created always answers, including an expired one, which reads expired.
- Poll with a delay that grows between reads, and stop at a final status.
- A scheduled read covers sessions that send no event.
Signed event
Noxtica sends your endpoint a notification. It saves you from polling, and it is not the decision.
When one is sent
- kyc.session.completed, when a session reaches completed, unverified or error, including the moment a reviewer decision resolves a session in review.
- kyc.session.cancelled, when the customer cancels and cancelling is enabled for your organisation.
- An expired session sends no event: read the status to observe expiry.
In your handler
- Verify the signature over the raw body, deduplicate on the delivery id, and read the session status before you act.
What counts as approved
Treat every status as not approved until you have read the one that approves.
The rule
- A decision that approved the session reads completed. Nothing else is an approval.
- Review pending is neither an approval nor a rejection: a decision is still needed in your Backoffice review queue.
- A person in your organisation reviews any adverse decision before it affects a customer.
Receive events
Events are notifications. They save you from polling, and they are not the decision.
| Event | When it is sent |
|---|---|
kyc.session.completed | A session reaches completed, unverified or error. This includes the moment a reviewer decision resolves a review_pending session. |
kyc.session.cancelled | The customer cancelled on the hosted page. Subscribe to it explicitly. It applies when cancelling is enabled for your organisation. |
Other identity event types exist. This page covers the two that describe a hosted session; Identity verification events and webhooks lists every identity event, its payload and when it is sent.
Review decisions are relayed on a periodic schedule, so allow a few minutes between a decision and its event. An expired session sends no event: use the status read to observe expiry.
The Backoffice “Send test” button sends a sample of a different event type. It cannot produce an identity event, so test your handler with a signed sample of your own.
Every delivery is an HTTPS POST with these headers:
| Header | Value |
|---|---|
X-Noxtica-Signature | sha256= followed by the lowercase hex signature |
X-Noxtica-Timestamp | The time of this attempt, in epoch milliseconds |
X-Noxtica-Event | The event type |
X-Noxtica-Delivery | The delivery id, identical to the id in the body |
The body is a JSON envelope with id, type, created_at, tenant_id and data. schemaVersion sits inside data, not on the envelope.
{
"id": "<sessionId>:<webhookId>",
"type": "kyc.session.completed",
"created_at": 1790762445000,
"tenant_id": "<tenant-id>",
"data": {
"schemaVersion": "1",
"sessionId": "<sessionId>",
"status": "completed",
"verificationId": "<verification-id>",
"reasonCodes": ["<reason-code>"],
"occurredAt": 1790762445000,
"deliveryId": "<sessionId>:<webhookId>"
}
}
{
"id": "<sessionId>:<webhookId>",
"type": "kyc.session.cancelled",
"created_at": 1790762300000,
"tenant_id": "<tenant-id>",
"data": {
"schemaVersion": "1",
"sessionId": "<sessionId>",
"status": "cancelled_by_user",
"cancelReason": "not_now",
"cancelledAt": 1790762300000,
"occurredAt": 1790762300000,
"deliveryId": "<sessionId>:<webhookId>"
}
}
cancelReasonisnullor one ofnot_now,privacy_concern,no_document,technical_problemandother.- When Noxtica Verification IDs are enabled for your organisation, a completed event also carries
data.nvid. Other optional steps can add fields todata, so ignore fields you do not recognise. - Events never carry your
subjectRef. Match an event to your customer bysessionId.
Verify the signature
The signature is an HMAC-SHA256 with your endpoint’s signing secret over the timestamp, a dot, and the raw request body: ${timestamp}.${rawBody}. Verify it before you parse or trust anything in the body.
- Use the body exactly as received. Do not parse and re-serialise it.
- Compare in constant time.
- Check the timestamp header, not
created_at. Every retry is signed again with a fresh timestamp. - Noxtica does not enforce a timestamp window on its side, so enforce your own. Five minutes is a sensible tolerance.
import { createHmac, timingSafeEqual } from 'node:crypto';
const TOLERANCE_MS = 5 * 60 * 1000;
export function verifyNoxticaWebhook({ secret, timestamp, signature, rawBody, now = Date.now() }) {
const sentAt = Number(timestamp);
if (!Number.isSafeInteger(sentAt) || Math.abs(now - sentAt) > TOLERANCE_MS) return false;
const expected = createHmac('sha256', secret).update(`${timestamp}.${rawBody}`).digest('hex');
const provided = String(signature).replace(/^sha256=/, '').toLowerCase();
const expectedBytes = Buffer.from(expected);
const providedBytes = Buffer.from(provided);
return expectedBytes.length === providedBytes.length && timingSafeEqual(expectedBytes, providedBytes);
}
import hashlib
import hmac
import time
TOLERANCE_MS = 5 * 60 * 1000
def verify_noxtica_webhook(secret: str, timestamp: str, signature: str, raw_body: bytes) -> bool:
if not (timestamp.isascii() and timestamp.isdigit()):
return False
if abs(time.time() * 1000 - int(timestamp)) > TOLERANCE_MS:
return False
signed = timestamp.encode() + b"." + raw_body
expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
provided = signature.removeprefix("sha256=").lower()
return hmac.compare_digest(expected, provided)
Delivery, retries and duplicates
- A
2xxresponse marks the delivery as delivered. - Any
4xxresponse is final. The delivery is not retried, including408and429. Return a4xxonly when you mean it. - A redirect, a
5xxresponse, a timeout or a network error is retried, up to five more attempts. After that the delivery is marked failed and is not sent again. The retry spacing is not published, so do not build on it. - Each attempt waits up to five seconds for your response, and redirects are not followed. Acknowledge quickly and process afterwards.
- Delivery is at least once. Deduplicate on
X-Noxtica-Delivery, which stays the same across retries. - Order is not fixed. Two events for the same session can arrive in either order.
A notification is not authority
When an event arrives, verify it, record the delivery id, and then read GET /v1/kyc/sessions/{sessionId}. Act on the status that read returns, not on the event body. This keeps your state correct when events are late, repeated or out of order, and it means a forged or replayed event cannot approve anyone.
Noxtica Verification ID
When Noxtica Verification IDs are enabled for your organisation, a session that completes or waits for review is given an ID in the form NVID-XXXX-XXXX-XXXX-XXXX. The customer sees it on the final page, and you read it from session.nvid or data.nvid. The ID is not a credential and does not sign anyone in. A person who has it can check its status on the verification site. That public lookup accepts requests only from the verification site itself, so your server cannot call it: read the ID from the session status or the event instead.
Errors
Every error body is JSON and includes a requestId. Quote it when you contact support.
| Status | Error | Meaning | What to do |
|---|---|---|---|
400 | invalid_request | Malformed JSON, a missing or over-long subjectRef, a bad expiresInSeconds, purpose or Idempotency-Key. | Fix the request. Check field names and limits. |
400 | invalid_hosted_options | hostedOptions is not a complete, valid object. | Send the complete object or leave it out. |
400 | invalid_id_type | idType is not accepted for this session. | Leave idType out. |
400 | return_url_not_allowed | returnUrl is not https or its host is not an approved return host. | Add the host in the hosted page settings or leave returnUrl out. |
400 or 403 | an error ending in _not_enabled, or email_verification_unavailable | You asked for an optional step that is not enabled for your organisation. | Leave that step out of hostedOptions, or ask your account team. |
401 | invalid_api_key | The key is missing, malformed, unknown or revoked. | Check the Authorization header, and that the key matches the environment you call. |
403 | API key does not have write:identity scope | The key lacks the identity permission. | Create a key with “Verify identity documents (KYC)” ticked. |
403 | kyc_legal_basis_receipt_required | Your organisation has not recorded the legal basis for hosted identity verification. | Record it in the Backoffice, then retry. |
404 | V1 endpoint not found | The module is not provisioned for your tenant, or the path is wrong. A status read also returns it for an unknown session id. | Check the path and session id. If both are right, ask your account team. Do not retry in a loop. |
413 | body_too_large | The request body is too large. | Send a small JSON object. |
415 | invalid_request | The Content-Type is not JSON. | Send Content-Type: application/json. |
429 | Rate limit exceeded | Too many requests. | Back off and retry. Honour retryAfter or Retry-After when present. |
500 | server_misconfigured | A server-side configuration problem. | Contact support with the requestId. |
503 | tenant_module_gate_unavailable, kyc_legal_basis_receipt_unavailable, invalid_request with a reason | A temporary fault. | Retry with a growing delay, and honour Retry-After when present. |
503 | hosted_verify_not_configured | The hosted verification service is not set up for this environment. | Contact your account team. |
A status read can also return 400 with Invalid sessionId when the id cannot be decoded.
The shared error envelope, rate limits and key handling are described in Errors and Authentication.
Who it fits
Use identity verification where your policy requires identity evidence and a person on your team owns the review.
- Onboarding and account opening. Collect supported identity evidence through a hosted flow, without building your own capture screens.
- Exceptions and review queues. Separate finished, expired, cancelled, and needs-review cases (see Statuses). Keep the exception path visible to your operators.
- Deciding when to ask. Pair verification with Device Intelligence and Risk Scoring to decide which journeys call for identity evidence. Common questions explains how they differ.
How teams connect
Identity verification has three surfaces: the server API, the hosted page, and the console with events. Choose how to integrate shows which route fits your team. Operators set up the hosted page in Invitations and hosted verification settings, and your endpoints subscribe to session events as described in Identity verification events and webhooks.
Limits
Read these boundaries before you design the flow.
- Controlled availability. What applies depends on an eligible identity configuration, your jurisdiction, and your contract. Confirm with your account team before you plan around it. See Availability.
- Not legal identity proof. No result is legal identity proof outside the scope of the configured provider and contract. Results are probabilistic and can include false positives and false negatives. They are not statements of fact. Read the disclaimer.
- You decide. A qualified person on your team should review adverse decisions. Noxtica is not a consumer reporting agency. Do not use output for credit, insurance, employment, or housing eligibility.
- Your responsibilities. You own the notices, consents, and legal basis for collecting identity evidence from your customers in each jurisdiction you serve. Plan a fallback for sessions that expire, are cancelled, or end in an error. The Privacy Policy and the Security page describe how Noxtica handles personal data; no module-specific data-scope statement for identity verification is published yet.
Common questions
Answers about statuses, review, links, keys, events and the Verification ID are in Identity verification FAQ and troubleshooting. Two product questions are not covered there.
Can I turn identity verification on myself?
No. It is provisioned per tenant under Controlled availability; see Availability. Contact your account team to discuss your use case and jurisdiction.
How does it relate to Device Intelligence and Risk Scoring?
They answer different questions. Device and risk evidence describe a browser context and an interaction. Identity verification gathers evidence about a person for your decision. Device risk can support identity evidence, but neither replaces the other.
Go-live checklist
- The readiness checklist in the Backoffice KYC pages shows no missing item, and a test session runs end to end.
- The legal basis is recorded by an authorised person in your organisation, and the notices and consents you owe your customers are in place.
- The server key has the identity permission, lives in a server secret store, and is never sent to a browser or an app.
- Your server creates every session, sends an
Idempotency-Key, stores thesessionIdwith the customer, and never logs the link. - Your return page makes no assumption about the outcome, and your journey works when the customer never comes back.
- Your events endpoint uses HTTPS, verifies the signature over the raw body, enforces a timestamp tolerance, deduplicates on the delivery id, answers quickly and reads the session status before it acts.
- A scheduled status read covers sessions that send no event, such as an expired session or one in
review_pending. - If you use the capacity read, one server-side poller shares the answer between your users, polls no faster than every 10 seconds, treats
status: unknownas “start anyway”, and has a wait message and a backoff fordefer. - Your code handles every status. Only
completedis an approval, andreview_pendingis neither an approval nor a rejection. - A person in your organisation reviews any adverse decision before it affects a customer. Results can be wrong in either direction and are not legal proof of identity.
- Noxtica does not delete verification details or evidence automatically and sets no retention date for them: they stay until a tenant administrator deletes them. Verification data: storage, access and deletion explains how an administrator deletes a verification in Backoffice.
- The Privacy Policy and the Security page describe how Noxtica handles personal data. This page does not.
Related reading
- Invitations and hosted verification settings — the Backoffice settings every hosted link uses, and invitations without code.
- Multiple domains — a business name, privacy link and support link per domain, and
domainIdon session creation. - Self-hosted identity verification (direct API) — the direct API as it works today.
- Identity verification events and webhooks — every identity event and its payload.
- Noxtica Verification ID (NVID) — the shareable reference for a verification.
- Identity verification FAQ and troubleshooting — answers to the questions integrators ask most.
- KYC Hosted Sessions — session lifetime, expiry and the
410responses. - Backend Integration — keep the credential on your server and handle explicit response states.
- Identity verification — the product overview, availability and limits.