Identity verification API
Verify identities, run hosted verification sessions, batch and re-verify, store evidence, read extracted identity details, and receive signed KYC, KYB and KYM webhooks.
| Environment | Base URL |
|---|---|
| Production | https://collect.noxtica.com |
Credential: ApiKeyAuth, SessionTokenAuth. See Authentication.
Verify an identity
POST /v1/kyc/verify
Two branches, selected by the body’s action field (verify is the implicit default when action is omitted — every pre-existing caller is unaffected):
action:'verify' admits, screens, policy-enforces, and finalizes a single identity verification against the tenant’s configured provider. The admission → spend-reservation → provider-call → finalize pipeline is shared byte-for-byte with the hosted-session submit path (POST /v1/kyc/session-submit) and the batch processor — there is exactly one verify pipeline in this system. It requires a nonce obtained from a prior action:'mint' call to the SAME endpoint (the only route that legitimately issues one) or a valid hosted-session-derived nonce.
action:'mint' issues that admission nonce: a fresh, tenant-scoped, single-use nonce bound in KV to the calling tenant (TTL 300s). It is the ONLY way a server-to-server caller can obtain a nonce a subsequent action:'verify' call will accept — without it, evidence-handle/cross-match/reverify are unreachable S2S, since every one of those requires an admitted verificationId a real verify call produces. Mint runs the SAME auth/module-gate/legal-basis preflight as verify, but NEVER meters, NEVER reserves spend, and makes ZERO provider calls.
Your own capture screens. If your application captures the document and selfie itself, mint with evidenceGroup: true. The response also carries an evidenceGroupId. Upload each capture into that group with POST /v1/kyc/evidence/handle (send evidenceGroupId instead of verificationId) and POST /v1/kyc/evidence/finalize, then verify with payload: {evidenceGroupId} and a fresh nonce. Noxtica resolves the uploaded evidence from the group — you never name evidence object ids — and the verificationId of the result equals the evidenceGroupId. A group accepts up to 16 evidence objects, closes after one hour, and serves exactly one verification (a retake needs a new group). A verify that names a group is refused before anything is admitted when the group is unknown (404 evidence_group_not_found, also for another organisation’s group), just expired (410 evidence_group_expired), incomplete (409 evidence_incomplete, with missingEvidence), or malformed (400); no nonce is spent and nothing is billed.
Authentication: Bearer token (ApiKeyAuth), scope write:identity.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
Idempotency-Key | header | string | Yes | A replayed key returns the byte-identical stored response — no second provider call, no double charge. At least 8 characters, at most 128 characters. |
Request body
Content type application/json, required.
The body of POST /v1/kyc/verify — either branch selected by action (verify is the implicit default when action is omitted). Named as its own component (rather than an inline oneOf) so openapi/generate.sh’s csharp/generichost codegen produces a distinct composed wrapper type instead of colliding with either member schema’s own generated name.
Variant KycVerifyMintRequest
| Field | Type | Required | Description |
|---|---|---|---|
action | string | Yes | Selects the mint branch of this endpoint. Allowed values: mint. |
evidenceGroup | boolean | No | When true, the response also carries an evidenceGroupId you upload your own captures into before verifying (see POST /v1/kyc/evidence/handle). Needs evidence storage enabled for your organisation; otherwise the call is a 404. Omitted or false returns the plain nonce. Default false. |
Variant KycVerifyVerifyRequest
| Field | Type | Required | Description |
|---|---|---|---|
action | string | No | Optional; verify is the implicit default when this field is omitted — every pre-existing caller is unaffected. Allowed values: verify. Default verify. |
nonce | string | Yes | A fresh, tenant-scoped single-use admission nonce (minted by your own prior client-side flow / SDK step). |
id_type | string | Yes | Document/verification type hint for the resolved provider adapter (aliases: idType). |
purpose | string | No | One of the closed evidence-purpose catalog values. Default identity_verification. |
audience | string | No | Defaults to the caller’s own tenant id. |
deviceContext | boolean | No | When true and device-intel is armed for the tenant, the response carries an ADVISORY deviceTrust block. Never influences the KYC verdict itself. |
payload | object | No | Provider-specific verification payload (never logged or persisted verbatim). verifiedDateOfBirth and minimumAge keys are always stripped server-side before reaching any adapter — supplying them has no effect (defense-in-depth against self-attested age/DOB). With evidenceGroupId the uploaded evidence is resolved from the group; supplying evidenceObjectIds next to it is a 400. |
payload.evidenceGroupId | string (uuid) | No | The evidenceGroupId returned by action:'mint' with evidenceGroup:true, after the document and selfie images were uploaded into it. The result’s verificationId equals this id. |
payload.jurisdiction | string | No | Optional with evidenceGroupId; must equal your organisation’s evidence residency. Allowed values: TR, EU, US. |
payload.operation | string | No | Optional with evidenceGroupId; only identity is available through a group. Allowed values: identity. |
Responses
| Status | Meaning | Error codes | Content |
|---|---|---|---|
200 | action:'mint': a freshly minted admission nonce (KycVerifyMintResponse) — zero provider calls, zero spend. action:'verify': a resolved verify attempt (verified, unverified, or an infra error verdict), a nonce-replay of a prior identical request, or an admission-level refusal (KycVerifyResponse). verdict:'error' and every nonce-fault outcome (e.g. nonce_unknown for a never-minted or wrong-tenant nonce, nonce_expired for one past its 300s TTL) are HTTP 200 by design (FR-6) — a happy-path 5xx is never returned for action:'verify'; mint’s own KV-write fault is the one 5xx exception (503, below). | application/json | |
400 | The request failed validation. error is a route-specific code such as invalid_request; read the operation description for the exact values it can return. | invalid_request | application/json ErrorEnvelope |
401 | The Authorization: Bearer sk_... header is missing or malformed, or the key is unknown or revoked. The body is deliberately identical for every one of those causes. | invalid_api_key | application/json KycSessionError |
402 | spend_cap_reached — action:'verify' only. The request was admitted, but no active pricing is configured for your organisation, so the verification was refused before any provider call. Never returned for action:'mint' (a mint never reserves spend). See the 403 response for the other refusal of this kind. | spend_cap_reached | application/json |
403 | Either the presented key lacks the required scope (ScopeForbidden shape), or — action:'verify' only — tenant_inactive: your organisation became inactive between nonce admission and the spend check, so the verification was refused. No provider call is made either way. | API key does not have write:identity scope | application/json |
404 | The byte-identical “V1 endpoint not found” response returned when the route’s module is not enabled for your organisation — indistinguishable from a route that does not exist (anti-enumeration by design; do not treat this as proof the path is wrong). | V1 endpoint not found, evidence_group_not_found | application/json |
409 | action:'verify' only: a conflicting in-flight or idempotency-key-bound request, or — when payload.evidenceGroupId names a group that does not yet hold every required capture — evidence_incomplete with the missing roles in missingEvidence (front, back, and/or selfie). The second form is refused before admission: the nonce is not spent, so retry with the same nonce once the evidence is uploaded. | evidence_incomplete | application/json ConflictError |
410 | action:'verify' with payload.evidenceGroupId: the evidence group has just expired (evidence_group_expired). Groups close after one hour; once the group record is gone the answer is the same 404 as for an unknown group. Mint a new group and upload again. | evidence_group_expired | application/json KycEvidenceGroupRefusal |
413 | body_too_large — the request exceeded this route’s byte cap. | body_too_large | application/json ErrorEnvelope |
429 | Rate limit exceeded. Wait Retry-After seconds (also retryAfter in the body) before retrying; the response carries no other rate-limit headers. | Rate limit exceeded | application/json |
503 | action:'mint': the nonce-issuing KV write failed (fail-safe: no partially-written or unusable nonce is ever returned to the caller). action:'verify': kyc_verify_unavailable — a transient fault while resolving pricing or reserving spend. Retryable; Retry-After carries the suggested backoff in seconds. Distinct from spend_cap_reached (402, a real business-state refusal, never an infrastructure fault); the request is not terminally denied. | invalid_request | application/json ErrorEnvelope |
503 | Noxtica could not read the key’s record. This is a temporary fault on Noxtica’s side and says nothing about your key. Retry after the Retry-After delay; do not treat it as a revoked key. | api_key_auth_unavailable | application/json |
Response headers
| Status | Header | Type | Description |
|---|---|---|---|
429 | Retry-After | integer | Seconds until the bucket allows another request. |
503 | Retry-After | integer | Seconds until a retry is likely to succeed (action:'verify' faults only). |
Response fields (200, application/json)
Variant KycVerifyMintResponse
| Field | Type | Always returned | Description |
|---|---|---|---|
ok | boolean | Yes | Allowed values: true. |
nonce | string (uuid) | Yes | Pass back verbatim as nonce on the follow-up action:'verify' (or omitted-action) call, from the SAME tenant, before it expires. |
ttlSeconds | integer | Yes | Absolute KV expiry from mint time (300s as of 2026-09-13, KYC_NONCE_TTL_SECONDS). A verify attempt after this window elapses returns reasonCodes:[nonce_expired]; an unminted, already-consumed, or wrong-tenant nonce returns reasonCodes:[nonce_unknown]. |
evidenceGroupId | string (uuid) | No | Present only when the request set evidenceGroup:true. Pass it as evidenceGroupId to POST /v1/kyc/evidence/handle, then as payload.evidenceGroupId on the verify call. |
evidenceGroupExpiresAt | integer | No | Present only with evidenceGroupId. Unix milliseconds at which the group closes. |
evidenceGroupTtlSeconds | integer | No | Present only with evidenceGroupId. Group lifetime from mint (3600). |
Variant KycVerifyResponse
| Field | Type | Always returned | Description |
|---|---|---|---|
ok | boolean | Yes | Allowed values: true. |
verdict | string | Yes | error is an infra/provider fault, never a statement about the subject — always HTTP 200 (FR-6). Allowed values: verified, unverified, error. |
verificationLevel | string | Yes | Allowed values: verified, partial, unverified, inconclusive. |
reasonCodes | array of string | Yes | |
verificationId | string (uuid) | Yes | |
requestId | string | Yes | |
legalBasisRef | string | No | Present only when this request’s tenant is running under classified legal-basis receipt gating. |
nfcAssurance | object | No | Present only for eMRTD/passport-chip verification paths. |
deviceTrust | object | No | Present only when the request set deviceContext:true and device-intel evidence is available. ADVISORY ONLY — never fed back into verdict/reasonCodes. |
Response example (200)
{
"ok": true,
"nonce": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"ttlSeconds": 300
}
Example request
curl
curl -X POST "https://collect.noxtica.com/v1/kyc/verify" \
-H "Authorization: Bearer $NOXTICA_API_KEY" \
-H "Idempotency-Key: example-Idempotency-Key" \
-H "Content-Type: application/json" \
-d '{
"action": "mint"
}'
Node.js (global fetch, Node 18 or later)
const response = await fetch('https://collect.noxtica.com/v1/kyc/verify', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.NOXTICA_API_KEY}`,
'Idempotency-Key': 'example-Idempotency-Key',
'Content-Type': 'application/json',
},
body: JSON.stringify({
action: 'mint',
}),
});
console.log(response.status, await response.text());
Python (standard library)
import json
import os
import urllib.request
request = urllib.request.Request(
"https://collect.noxtica.com/v1/kyc/verify",
data=json.dumps({
"action": "mint",
}).encode(),
headers={
"Authorization": f"Bearer {os.environ['NOXTICA_API_KEY']}",
"Idempotency-Key": "example-Idempotency-Key",
"Content-Type": "application/json",
},
method="POST",
)
with urllib.request.urlopen(request) as response:
print(response.status, response.read().decode())
Cross-match a self-uploaded proof-of-address document
POST /v1/kyc/verify/{verificationId}/address-document
Re-OCRs a proof-of-address document (already uploaded as KYC evidence) and the target verification’s already-captured ID-front document, then runs a deterministic name/address/freshness cross-match. This is the only cross-validation capability Noxtica offers today — it is a self-uploaded-document cross-match, never an authoritative registry lookup (USPS/Loqate/Experian), as the response’s own disclosure field states. There is no generic OCR↔MRZ↔barcode consistency check.
Authentication: Bearer token (ApiKeyAuth), scope write:identity.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
verificationId | path | string (uuid) | Yes |
Request body
Content type application/json, required.
| Field | Type | Required | Description |
|---|---|---|---|
evidenceObjectId | string | Yes | The objectId of an already-finalized (usable) evidence object of kind proof_of_address, minted via POST /v1/kyc/evidence/handle + POST /v1/kyc/evidence/finalize against this same verificationId. |
Responses
| Status | Meaning | Error codes | Content |
|---|---|---|---|
200 | A resolved cross-match verdict. | application/json | |
400 | The request failed validation. error is a route-specific code such as invalid_request; read the operation description for the exact values it can return. | invalid_request | application/json ErrorEnvelope |
401 | The Authorization: Bearer sk_... header is missing or malformed, or the key is unknown or revoked. The body is deliberately identical for every one of those causes. | invalid_api_key | application/json KycSessionError |
403 | The presented key lacks the required scope. | API key does not have write:identity scope | application/json |
404 | The byte-identical “V1 endpoint not found” response returned when the route’s module is not enabled for your organisation — indistinguishable from a route that does not exist (anti-enumeration by design; do not treat this as proof the path is wrong). | V1 endpoint not found | application/json |
409 | The verification has no captured front-document evidence to cross-match against. | invalid_request | application/json ErrorEnvelope |
413 | body_too_large — the request exceeded this route’s byte cap. | body_too_large | application/json ErrorEnvelope |
429 | Rate limit exceeded. Wait Retry-After seconds (also retryAfter in the body) before retrying; the response carries no other rate-limit headers. | Rate limit exceeded | application/json |
503 | Evidence unavailable, or the owned OCR inference service could not be reached. | invalid_request | application/json ErrorEnvelope |
503 | Noxtica could not read the key’s record. This is a temporary fault on Noxtica’s side and says nothing about your key. Retry after the Retry-After delay; do not treat it as a revoked key. | api_key_auth_unavailable | application/json |
Response headers
| Status | Header | Type | Description |
|---|---|---|---|
429 | Retry-After | integer | Seconds until the bucket allows another request. |
503 | Retry-After | integer | Seconds to wait before retrying. |
Response fields (200, application/json)
| Field | Type | Always returned | Description |
|---|---|---|---|
ok | boolean | Yes | Allowed values: true. |
verdict | string | Yes | Allowed values: pass, review, reject. |
disclosure | string | Yes | The fixed, always-identical non-authoritative-result disclosure string. |
requestId | string | Yes |
Response example (200)
{
"ok": true,
"verdict": "pass",
"disclosure": "example-disclosure",
"requestId": "req-test"
}
Example request
curl
curl -X POST "https://collect.noxtica.com/v1/kyc/verify/00000000-0000-4000-8000-000000000000/address-document" \
-H "Authorization: Bearer $NOXTICA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"evidenceObjectId": "obj-front"
}'
Node.js (global fetch, Node 18 or later)
const response = await fetch('https://collect.noxtica.com/v1/kyc/verify/00000000-0000-4000-8000-000000000000/address-document', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.NOXTICA_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
evidenceObjectId: 'obj-front',
}),
});
console.log(response.status, await response.text());
Python (standard library)
import json
import os
import urllib.request
request = urllib.request.Request(
"https://collect.noxtica.com/v1/kyc/verify/00000000-0000-4000-8000-000000000000/address-document",
data=json.dumps({
"evidenceObjectId": "obj-front",
}).encode(),
headers={
"Authorization": f"Bearer {os.environ['NOXTICA_API_KEY']}",
"Content-Type": "application/json",
},
method="POST",
)
with urllib.request.urlopen(request) as response:
print(response.status, response.read().decode())
Submit a batch verification manifest
POST /v1/kyc/verify/batch
Submits a CSV (text/csv) or JSON (application/json, {"items":[...]}) manifest of up to 500 rows. Each well-formed row is admitted asynchronously, off the response path, through the identical POST /v1/kyc/verify admission entry point — this endpoint does not verify anything itself, it only records per-item outcomes.
Authentication: Bearer token (ApiKeyAuth), scope write:identity.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
Idempotency-Key | header | string | Yes | A replayed key returns the byte-identical stored response — no second provider call, no double charge. At least 8 characters, at most 128 characters. |
Request body
Content type application/json, required.
| Field | Type | Required | Description |
|---|---|---|---|
items | array of object | Yes | At most 500 items. |
items[] | object | No | |
items[].id_type | string | Yes | |
items[].purpose | string | No | Default identity_verification. |
items[].payload | object | No |
Content type text/csv, required.
Type: string.
Responses
| Status | Meaning | Error codes | Content |
|---|---|---|---|
200 | A replayed idempotency key — the original batch job, unchanged. | application/json KycBatchSubmitResponse | |
202 | Accepted; items begin admitting asynchronously. | application/json KycBatchSubmitResponse | |
400 | The request failed validation. error is a route-specific code such as invalid_request; read the operation description for the exact values it can return. | invalid_request | application/json ErrorEnvelope |
401 | The Authorization: Bearer sk_... header is missing or malformed, or the key is unknown or revoked. The body is deliberately identical for every one of those causes. | invalid_api_key | application/json KycSessionError |
403 | The presented key lacks the required scope. | API key does not have write:identity scope | application/json |
404 | The byte-identical “V1 endpoint not found” response returned when the route’s module is not enabled for your organisation — indistinguishable from a route that does not exist (anti-enumeration by design; do not treat this as proof the path is wrong). | V1 endpoint not found | application/json |
413 | body_too_large — the request exceeded this route’s byte cap. | body_too_large | application/json ErrorEnvelope |
415 | Unrecognized content type. | invalid_request | application/json ErrorEnvelope |
429 | Rate limit exceeded. Wait Retry-After seconds (also retryAfter in the body) before retrying; the response carries no other rate-limit headers. | Rate limit exceeded | application/json |
503 | Batch admission is temporarily unavailable (digest secret unset). | invalid_request | application/json ErrorEnvelope |
503 | Noxtica could not read the key’s record. This is a temporary fault on Noxtica’s side and says nothing about your key. Retry after the Retry-After delay; do not treat it as a revoked key. | api_key_auth_unavailable | application/json |
Response headers
| Status | Header | Type | Description |
|---|---|---|---|
429 | Retry-After | integer | Seconds until the bucket allows another request. |
503 | Retry-After | integer | Seconds to wait before retrying. |
Response fields (200, application/json)
| Field | Type | Always returned | Description |
|---|---|---|---|
ok | boolean | No | |
batchId | string | No | |
status | string | No | Allowed values: processing, completed, cancelled. |
itemCount | integer | No | |
items | array of KycBatchSubmittedItem | No | |
items[] | object (KycBatchSubmittedItem) | No | |
items[].rowIndex | integer | No | |
items[].status | string | No | Allowed values: pending, admitted, evaluating, completed, rejected, cancelled. |
items[].errorCode | one of KycBatchItemErrorCode or any, nullable | No | |
requestId | string | No |
Response example (200)
{
"ok": true,
"batchId": "batch_5a1f9c3e7b2d4a6f8091c2d3e4f5a6b7",
"status": "processing",
"itemCount": 1,
"items": [
{
"rowIndex": 0,
"status": "pending",
"errorCode": "missing_id_type"
}
],
"requestId": "req-test"
}
Response fields (202, application/json)
| Field | Type | Always returned | Description |
|---|---|---|---|
ok | boolean | No | |
batchId | string | No | |
status | string | No | Allowed values: processing, completed, cancelled. |
itemCount | integer | No | |
items | array of KycBatchSubmittedItem | No | |
items[] | object (KycBatchSubmittedItem) | No | |
items[].rowIndex | integer | No | |
items[].status | string | No | Allowed values: pending, admitted, evaluating, completed, rejected, cancelled. |
items[].errorCode | one of KycBatchItemErrorCode or any, nullable | No | |
requestId | string | No |
Response example (202)
{
"ok": true,
"batchId": "batch_5a1f9c3e7b2d4a6f8091c2d3e4f5a6b7",
"status": "processing",
"itemCount": 1,
"items": [
{
"rowIndex": 0,
"status": "pending",
"errorCode": "missing_id_type"
}
],
"requestId": "req-test"
}
Example request
curl
curl -X POST "https://collect.noxtica.com/v1/kyc/verify/batch" \
-H "Authorization: Bearer $NOXTICA_API_KEY" \
-H "Idempotency-Key: example-Idempotency-Key" \
-H "Content-Type: application/json" \
-d '{
"items": [
{
"id_type": "passport",
"purpose": "identity_verification",
"payload": {}
}
]
}'
Node.js (global fetch, Node 18 or later)
const response = await fetch('https://collect.noxtica.com/v1/kyc/verify/batch', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.NOXTICA_API_KEY}`,
'Idempotency-Key': 'example-Idempotency-Key',
'Content-Type': 'application/json',
},
body: JSON.stringify({
items: [
{
id_type: 'passport',
purpose: 'identity_verification',
payload: {},
},
],
}),
});
console.log(response.status, await response.text());
Python (standard library)
import json
import os
import urllib.request
request = urllib.request.Request(
"https://collect.noxtica.com/v1/kyc/verify/batch",
data=json.dumps({
"items": [
{
"id_type": "passport",
"purpose": "identity_verification",
"payload": {},
},
],
}).encode(),
headers={
"Authorization": f"Bearer {os.environ['NOXTICA_API_KEY']}",
"Idempotency-Key": "example-Idempotency-Key",
"Content-Type": "application/json",
},
method="POST",
)
with urllib.request.urlopen(request) as response:
print(response.status, response.read().decode())
Read a batch job’s aggregate status
GET /v1/kyc/verify/batch/{batchId}/status
Authentication: Bearer token (ApiKeyAuth), scope write:identity.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
batchId | path | string | Yes |
Responses
| Status | Meaning | Error codes | Content |
|---|---|---|---|
200 | The job-level aggregate. | application/json KycBatchStatusResponse | |
401 | The Authorization: Bearer sk_... header is missing or malformed, or the key is unknown or revoked. The body is deliberately identical for every one of those causes. | invalid_api_key | application/json KycSessionError |
403 | The presented key lacks the required scope. | API key does not have write:identity scope | application/json |
404 | The byte-identical “V1 endpoint not found” response returned when the route’s module is not enabled for your organisation — indistinguishable from a route that does not exist (anti-enumeration by design; do not treat this as proof the path is wrong). | V1 endpoint not found | application/json |
503 | Noxtica could not read the key’s record. This is a temporary fault on Noxtica’s side and says nothing about your key. Retry after the Retry-After delay; do not treat it as a revoked key. | api_key_auth_unavailable | application/json |
Response headers
| Status | Header | Type | Description |
|---|---|---|---|
503 | Retry-After | integer | Seconds to wait before retrying. |
Response fields (200, application/json)
| Field | Type | Always returned | Description |
|---|---|---|---|
ok | boolean | No | |
batchId | string | No | |
status | string | No | Allowed values: processing, completed, cancelled. |
itemCount | integer | No | |
pendingCount | integer | No | |
admittedCount | integer | No | |
completedCount | integer | No | |
rejectedCount | integer | No | |
cancelledCount | integer | No | |
requestId | string | No |
Response example (200)
{
"ok": true,
"batchId": "batch_5a1f9c3e7b2d4a6f8091c2d3e4f5a6b7",
"status": "processing",
"itemCount": 1,
"pendingCount": 0,
"admittedCount": 1,
"completedCount": 0,
"rejectedCount": 0,
"cancelledCount": 0,
"requestId": "req-test"
}
Example request
curl
curl "https://collect.noxtica.com/v1/kyc/verify/batch/example-batchId/status" \
-H "Authorization: Bearer $NOXTICA_API_KEY"
Node.js (global fetch, Node 18 or later)
const response = await fetch('https://collect.noxtica.com/v1/kyc/verify/batch/example-batchId/status', {
method: 'GET',
headers: {
Authorization: `Bearer ${process.env.NOXTICA_API_KEY}`,
},
});
console.log(response.status, await response.text());
Python (standard library)
import os
import urllib.request
request = urllib.request.Request(
"https://collect.noxtica.com/v1/kyc/verify/batch/example-batchId/status",
headers={
"Authorization": f"Bearer {os.environ['NOXTICA_API_KEY']}",
},
method="GET",
)
with urllib.request.urlopen(request) as response:
print(response.status, response.read().decode())
Read a batch job’s per-item outcomes
GET /v1/kyc/verify/batch/{batchId}/results
Authentication: Bearer token (ApiKeyAuth), scope write:identity.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
batchId | path | string | Yes |
Responses
| Status | Meaning | Error codes | Content |
|---|---|---|---|
200 | Every row’s outcome, malformed or not — never silently dropped. | application/json KycBatchResultsResponse | |
401 | The Authorization: Bearer sk_... header is missing or malformed, or the key is unknown or revoked. The body is deliberately identical for every one of those causes. | invalid_api_key | application/json KycSessionError |
403 | The presented key lacks the required scope. | API key does not have write:identity scope | application/json |
404 | The byte-identical “V1 endpoint not found” response returned when the route’s module is not enabled for your organisation — indistinguishable from a route that does not exist (anti-enumeration by design; do not treat this as proof the path is wrong). | V1 endpoint not found | application/json |
503 | Noxtica could not read the key’s record. This is a temporary fault on Noxtica’s side and says nothing about your key. Retry after the Retry-After delay; do not treat it as a revoked key. | api_key_auth_unavailable | application/json |
Response headers
| Status | Header | Type | Description |
|---|---|---|---|
503 | Retry-After | integer | Seconds to wait before retrying. |
Response fields (200, application/json)
| Field | Type | Always returned | Description |
|---|---|---|---|
ok | boolean | No | |
batchId | string | No | |
items | array of object | No | |
items[] | object | No | |
items[].rowIndex | integer | No | |
items[].status | string | No | Allowed values: pending, admitted, evaluating, completed, rejected, cancelled. |
items[].verificationId | string, nullable | No | |
items[].verdict | string, nullable | No | Allowed values: verified, unverified, error. |
items[].errorCode | one of KycBatchItemErrorCode or any, nullable | No | |
requestId | string | No |
Response example (200)
{
"ok": true,
"batchId": "batch_5a1f9c3e7b2d4a6f8091c2d3e4f5a6b7",
"items": [
{
"rowIndex": 0,
"status": "pending",
"verificationId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"verdict": "verified",
"errorCode": "missing_id_type"
}
],
"requestId": "req-test"
}
Example request
curl
curl "https://collect.noxtica.com/v1/kyc/verify/batch/example-batchId/results" \
-H "Authorization: Bearer $NOXTICA_API_KEY"
Node.js (global fetch, Node 18 or later)
const response = await fetch('https://collect.noxtica.com/v1/kyc/verify/batch/example-batchId/results', {
method: 'GET',
headers: {
Authorization: `Bearer ${process.env.NOXTICA_API_KEY}`,
},
});
console.log(response.status, await response.text());
Python (standard library)
import os
import urllib.request
request = urllib.request.Request(
"https://collect.noxtica.com/v1/kyc/verify/batch/example-batchId/results",
headers={
"Authorization": f"Bearer {os.environ['NOXTICA_API_KEY']}",
},
method="GET",
)
with urllib.request.urlopen(request) as response:
print(response.status, response.read().decode())
Cancel every still-pending item of a batch
POST /v1/kyc/verify/batch/{batchId}/cancel
A compare-and-swap UPDATE ... WHERE status = 'pending'. An item already admitted/evaluating/completed already holds (or held) a real verification claim and is never retroactively un-admitted.
Authentication: Bearer token (ApiKeyAuth), scope write:identity.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
batchId | path | string | Yes |
Responses
| Status | Meaning | Error codes | Content |
|---|---|---|---|
200 | Cancellation applied to every still-pending row. | application/json | |
401 | The Authorization: Bearer sk_... header is missing or malformed, or the key is unknown or revoked. The body is deliberately identical for every one of those causes. | invalid_api_key | application/json KycSessionError |
403 | The presented key lacks the required scope. | API key does not have write:identity scope | application/json |
404 | The byte-identical “V1 endpoint not found” response returned when the route’s module is not enabled for your organisation — indistinguishable from a route that does not exist (anti-enumeration by design; do not treat this as proof the path is wrong). | V1 endpoint not found | application/json |
503 | Noxtica could not read the key’s record. This is a temporary fault on Noxtica’s side and says nothing about your key. Retry after the Retry-After delay; do not treat it as a revoked key. | api_key_auth_unavailable | application/json |
Response headers
| Status | Header | Type | Description |
|---|---|---|---|
503 | Retry-After | integer | Seconds to wait before retrying. |
Response fields (200, application/json)
| Field | Type | Always returned | Description |
|---|---|---|---|
ok | boolean | No | |
batchId | string | No | |
cancelledCount | integer | No | |
requestId | string | No |
Response example (200)
{
"ok": true,
"batchId": "batch_5a1f9c3e7b2d4a6f8091c2d3e4f5a6b7",
"cancelledCount": 0,
"requestId": "req-test"
}
Example request
curl
curl -X POST "https://collect.noxtica.com/v1/kyc/verify/batch/example-batchId/cancel" \
-H "Authorization: Bearer $NOXTICA_API_KEY"
Node.js (global fetch, Node 18 or later)
const response = await fetch('https://collect.noxtica.com/v1/kyc/verify/batch/example-batchId/cancel', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.NOXTICA_API_KEY}`,
},
});
console.log(response.status, await response.text());
Python (standard library)
import os
import urllib.request
request = urllib.request.Request(
"https://collect.noxtica.com/v1/kyc/verify/batch/example-batchId/cancel",
headers={
"Authorization": f"Bearer {os.environ['NOXTICA_API_KEY']}",
},
method="POST",
)
with urllib.request.urlopen(request) as response:
print(response.status, response.read().decode())
Re-check an already-established identity’s device/verification level
POST /v1/kyc/reverify
A metered, read-shaped check against the authoritative device registry — it does not re-run document/biometric capture. Requires the device-registry module to be enabled for your organisation in addition to the KYC module; when it is not, the route answers the GatedNotFound 404.
Authentication: Bearer token (ApiKeyAuth), scope write:identity.
Request body
Content type application/json, required.
| Field | Type | Required | Description |
|---|---|---|---|
subjectId | string | Yes | |
requiredLevel | string | No | Allowed values: verified, partial, unverified, inconclusive. |
stepUpReference | string | No | Optional biometric step-up claim reference (single-use). At most 128 characters. Pattern ^[a-zA-Z0-9][a-zA-Z0-9._:-]{0,127}$. |
Responses
| Status | Meaning | Error codes | Content |
|---|---|---|---|
200 | Always 200 (FR-6 fail-closed discipline) — a subject-not-found or infra fault is a verified:false body, never a 5xx. | application/json | |
401 | The Authorization: Bearer sk_... header is missing or malformed, or the key is unknown or revoked. The body is deliberately identical for every one of those causes. | invalid_api_key | application/json KycSessionError |
403 | The presented key lacks the required scope. | API key does not have write:identity scope | application/json |
404 | The byte-identical “V1 endpoint not found” response returned when the route’s module is not enabled for your organisation — indistinguishable from a route that does not exist (anti-enumeration by design; do not treat this as proof the path is wrong). | V1 endpoint not found | application/json |
503 | Noxtica could not read the key’s record. This is a temporary fault on Noxtica’s side and says nothing about your key. Retry after the Retry-After delay; do not treat it as a revoked key. | api_key_auth_unavailable | application/json |
Response headers
| Status | Header | Type | Description |
|---|---|---|---|
503 | Retry-After | integer | Seconds to wait before retrying. |
Response fields (200, application/json)
| Field | Type | Always returned | Description |
|---|---|---|---|
ok | boolean | No | |
verified | boolean | No | |
verificationLevel | string | No | Allowed values: verified, partial, unverified, inconclusive. |
stepUp | string | No | Allowed values: satisfied, unconfigured, reference_required, reference_replayed, no_match, error. |
requestId | string | No |
Response example (200)
{
"ok": true,
"verified": true,
"verificationLevel": "verified",
"stepUp": "satisfied",
"requestId": "req-test"
}
Example request
curl
curl -X POST "https://collect.noxtica.com/v1/kyc/reverify" \
-H "Authorization: Bearer $NOXTICA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"subjectId": "subj_4821",
"requiredLevel": "verified",
"stepUpReference": "example-stepUpReference"
}'
Node.js (global fetch, Node 18 or later)
const response = await fetch('https://collect.noxtica.com/v1/kyc/reverify', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.NOXTICA_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
subjectId: 'subj_4821',
requiredLevel: 'verified',
stepUpReference: 'example-stepUpReference',
}),
});
console.log(response.status, await response.text());
Python (standard library)
import json
import os
import urllib.request
request = urllib.request.Request(
"https://collect.noxtica.com/v1/kyc/reverify",
data=json.dumps({
"subjectId": "subj_4821",
"requiredLevel": "verified",
"stepUpReference": "example-stepUpReference",
}).encode(),
headers={
"Authorization": f"Bearer {os.environ['NOXTICA_API_KEY']}",
"Content-Type": "application/json",
},
method="POST",
)
with urllib.request.urlopen(request) as response:
print(response.status, response.read().decode())
Mint a single-use ciphertext-upload handle
POST /v1/kyc/evidence/handle
Step 1 of 2 for raw evidence storage. Mints an opaque, short-lived (15-minute) upload handle plus a fresh per-object Data Encryption Key (DEK) wrapped under the tenant’s active Key Encryption Key (KEK). The caller encrypts the evidence bytes client-side with the returned dek and presents only ciphertext to POST /v1/kyc/evidence/finalize — Noxtica’s server never sees plaintext evidence at this step. No R2 storage URL, internal key, or sk_ value is ever returned (D27).
Authentication: Bearer token (ApiKeyAuth), scope write:identity.
Request body
Content type application/json, required.
| Field | Type | Required | Description |
|---|---|---|---|
verificationId | string (uuid) | No | The verification the evidence belongs to. Exactly one of this and evidenceGroupId is required; naming both, or neither, is a 400. |
evidenceGroupId | string (uuid) | No | The group minted by POST /v1/kyc/verify with action:'mint' and evidenceGroup:true, for captures you upload before the verification exists. Only the image roles document_front, document_back, passport_page and selfie_image are accepted; a group holds at most 16 objects. An unknown, expired, or another organisation’s group is the same 404. |
jurisdiction | string | Yes | Must equal the tenant’s configured evidence residency; any mismatch is a 404 (no residency-probe oracle). Allowed values: TR, EU, US. |
evidenceKind | string | Yes | Closed evidence-kind catalog (this enum is the complete set). e.g. id_document_front, proof_of_address, selfie_image. NOTE: id_front/id_back/selfie are NOT members of this catalog (a prior stale example here caused exactly that drift in downstream SDKs). Allowed values: id_document_front, id_document_back, document_front, document_back, passport_page, proof_of_address, selfie_image, document_near_face, liveness_video, liveness_recording, liveness_sequence, voice_sample, speech_audio, supporting_document, document_data. |
purpose | string | Yes | Closed evidence-purpose catalog (e.g. identity_verification, address_verification). |
contentType | string | Yes | |
contentLength | integer | Yes | Minimum 1. |
Responses
| Status | Meaning | Error codes | Content |
|---|---|---|---|
200 | Handle minted. | application/json | |
400 | The request failed validation. error is a route-specific code such as invalid_request; read the operation description for the exact values it can return. | invalid_request | application/json ErrorEnvelope |
401 | The Authorization: Bearer sk_... header is missing or malformed, or the key is unknown or revoked. The body is deliberately identical for every one of those causes. | invalid_api_key | application/json KycSessionError |
403 | The presented key lacks the required scope. | API key does not have write:identity scope | application/json |
404 | The byte-identical “V1 endpoint not found” response returned when the route’s module is not enabled for your organisation — indistinguishable from a route that does not exist (anti-enumeration by design; do not treat this as proof the path is wrong). | V1 endpoint not found | application/json |
413 | body_too_large — the request exceeded this route’s byte cap. | body_too_large | application/json ErrorEnvelope |
429 | Rate limit exceeded. Wait Retry-After seconds (also retryAfter in the body) before retrying; the response carries no other rate-limit headers. | Rate limit exceeded | application/json |
503 | Evidence storage temporarily unavailable (handle secret or KEK unresolved). | invalid_request | application/json ErrorEnvelope |
503 | Noxtica could not read the key’s record. This is a temporary fault on Noxtica’s side and says nothing about your key. Retry after the Retry-After delay; do not treat it as a revoked key. | api_key_auth_unavailable | application/json |
Response headers
| Status | Header | Type | Description |
|---|---|---|---|
429 | Retry-After | integer | Seconds until the bucket allows another request. |
503 | Retry-After | integer | Seconds to wait before retrying. |
Response fields (200, application/json)
| Field | Type | Always returned | Description |
|---|---|---|---|
ok | boolean | No | |
handle | string | No | Opaque signed single-use upload handle. |
dek | string | No | Base64 plaintext DEK — hold only in memory |
wrappedDek | string | No | The same DEK wrapped under the active KEK |
envelope | object | No | |
envelope.envelope_version | integer | No | |
envelope.cipher_suite | string | No | |
envelope.kek_kid | string | No | |
uploadTarget | object | No | |
uploadTarget.objectId | string | No | |
uploadTarget.schemaVersion | integer | No | |
expiresAt | integer | No | Unix ms handle expiry. |
evidenceGroupId | string (uuid) | No | Echo of the request’s evidenceGroupId; present only for an evidence-group handle. |
requestId | string | No |
Response example (200)
{
"ok": true,
"handle": "example-handle",
"dek": "example-dek",
"wrappedDek": "example-wrappedDek",
"envelope": {
"envelope_version": 1,
"cipher_suite": "example-cipher_suite",
"kek_kid": "example-kek_kid"
},
"uploadTarget": {
"objectId": "example-objectId",
"schemaVersion": 1
},
"expiresAt": 1768435200,
"requestId": "req-test"
}
Example request
curl
curl -X POST "https://collect.noxtica.com/v1/kyc/evidence/handle" \
-H "Authorization: Bearer $NOXTICA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"verificationId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"jurisdiction": "TR",
"evidenceKind": "id_document_front",
"purpose": "identity_verification",
"contentType": "image/jpeg",
"contentLength": 1
}'
Node.js (global fetch, Node 18 or later)
const response = await fetch('https://collect.noxtica.com/v1/kyc/evidence/handle', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.NOXTICA_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
verificationId: '3fa85f64-5717-4562-b3fc-2c963f66afa6',
jurisdiction: 'TR',
evidenceKind: 'id_document_front',
purpose: 'identity_verification',
contentType: 'image/jpeg',
contentLength: 1,
}),
});
console.log(response.status, await response.text());
Python (standard library)
import json
import os
import urllib.request
request = urllib.request.Request(
"https://collect.noxtica.com/v1/kyc/evidence/handle",
data=json.dumps({
"verificationId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"jurisdiction": "TR",
"evidenceKind": "id_document_front",
"purpose": "identity_verification",
"contentType": "image/jpeg",
"contentLength": 1,
}).encode(),
headers={
"Authorization": f"Bearer {os.environ['NOXTICA_API_KEY']}",
"Content-Type": "application/json",
},
method="POST",
)
with urllib.request.urlopen(request) as response:
print(response.status, response.read().decode())
Upload sealed ciphertext and finalize the evidence object
POST /v1/kyc/evidence/finalize
Step 2 of 2. Presents the handle from POST /v1/kyc/evidence/handle plus the chunked-AEAD-sealed ciphertext body. The server performs a controlled in-memory decrypt → structural inspection (never persisting plaintext) and promotes the object to usable or rejected. A repeated call with the same handle/body replays the stored terminal result byte-for-byte (idempotent by construction, not by an Idempotency-Key header).
Authentication: Bearer token (ApiKeyAuth), scope write:identity.
Request body
Content type application/json, required.
| Field | Type | Required | Description |
|---|---|---|---|
handle | string | Yes | The opaque handle from the mint step. |
objectId | string | Yes | |
commitment | object | Yes | |
commitment.total_chunks | integer | Yes | Minimum 1. |
commitment.total_plaintext_length | integer | Yes | Minimum 0. |
chunks | array of string | Yes | Chunked-AEAD-sealed ciphertext chunks (schema version carried in the mint-step envelope). |
Responses
| Status | Meaning | Error codes | Content |
|---|---|---|---|
200 | Terminal inspection outcome. | application/json | |
400 | The request failed validation. error is a route-specific code such as invalid_request; read the operation description for the exact values it can return. | invalid_request | application/json ErrorEnvelope |
401 | The Authorization: Bearer sk_... header is missing or malformed, or the key is unknown or revoked. The body is deliberately identical for every one of those causes. | invalid_api_key | application/json KycSessionError |
403 | The presented handle failed verification (signature, expiry, manifest, or binding mismatch) — a single uniform refusal, no probe oracle. | invalid_request | application/json ErrorEnvelope |
404 | The byte-identical “V1 endpoint not found” response returned when the route’s module is not enabled for your organisation — indistinguishable from a route that does not exist (anti-enumeration by design; do not treat this as proof the path is wrong). | V1 endpoint not found | application/json |
413 | body_too_large — the request exceeded this route’s byte cap. | body_too_large | application/json ErrorEnvelope |
429 | Rate limit exceeded. Wait Retry-After seconds (also retryAfter in the body) before retrying; the response carries no other rate-limit headers. | Rate limit exceeded | application/json |
503 | Evidence storage temporarily unavailable. | invalid_request | application/json ErrorEnvelope |
503 | Noxtica could not read the key’s record. This is a temporary fault on Noxtica’s side and says nothing about your key. Retry after the Retry-After delay; do not treat it as a revoked key. | api_key_auth_unavailable | application/json |
Response headers
| Status | Header | Type | Description |
|---|---|---|---|
429 | Retry-After | integer | Seconds until the bucket allows another request. |
503 | Retry-After | integer | Seconds to wait before retrying. |
Response fields (200, application/json)
| Field | Type | Always returned | Description |
|---|---|---|---|
ok | boolean | Yes | Allowed values: true. |
state | string | Yes | Allowed values: usable, rejected. |
reasonCode | string | No | Present only when state is rejected. Allowed values: evidence_kind_forbidden, evidence_mime_mismatch, evidence_size_mismatch, evidence_pixels_invalid, evidence_unreadable. |
objectId | string | Yes | |
requestId | string | Yes |
Response example (200)
{
"ok": true,
"state": "usable",
"reasonCode": "evidence_kind_forbidden",
"objectId": "example-objectId",
"requestId": "req-test"
}
Example request
curl
curl -X POST "https://collect.noxtica.com/v1/kyc/evidence/finalize" \
-H "Authorization: Bearer $NOXTICA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"handle": "example-handle",
"objectId": "example-objectId",
"commitment": {
"total_chunks": 1,
"total_plaintext_length": 0
},
"chunks": [
"example-chunks"
]
}'
Node.js (global fetch, Node 18 or later)
const response = await fetch('https://collect.noxtica.com/v1/kyc/evidence/finalize', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.NOXTICA_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
handle: 'example-handle',
objectId: 'example-objectId',
commitment: {
total_chunks: 1,
total_plaintext_length: 0,
},
chunks: [
'example-chunks',
],
}),
});
console.log(response.status, await response.text());
Python (standard library)
import json
import os
import urllib.request
request = urllib.request.Request(
"https://collect.noxtica.com/v1/kyc/evidence/finalize",
data=json.dumps({
"handle": "example-handle",
"objectId": "example-objectId",
"commitment": {
"total_chunks": 1,
"total_plaintext_length": 0,
},
"chunks": [
"example-chunks",
],
}).encode(),
headers={
"Authorization": f"Bearer {os.environ['NOXTICA_API_KEY']}",
"Content-Type": "application/json",
},
method="POST",
)
with urllib.request.urlopen(request) as response:
print(response.status, response.read().decode())
List the evidence images of a verification
GET /v1/kyc/verifications/{verificationId}/evidence
Server-to-server. Lists the images a person captured during one of your own verifications,
so that your own operators can look at them: the front and back of an identity document, a
passport page, a proof of address, a selfie, a capture of the document held next to the face,
and supporting documents. Only still images whose storage finished successfully are listed.
Videos, voice recordings, the identity details Noxtica extracted (read those with
GET /v1/kyc/verifications/{verificationId}/identity) and anything Noxtica derived from the
images are never listed and cannot be opened through these routes. The response holds no image
bytes: ask for one with POST /v1/kyc/verifications/{verificationId}/evidence/{itemId}/access,
then fetch the signed URL it returns.
Opt-in. The key needs the read:evidence scope, and every evidence image route answers
403 evidence_read_not_enabled until a tenant administrator enables evidence image access for
your organisation in the Backoffice (KYC → Invitations → Settings tab, card “Evidence image API”):
the administrator records the legal basis for receiving these images, Article 6 and Article 9 of
the GDPR, with step-up multi-factor authentication. Identity document images and selfies are
special-category personal data: enable the access only for the operators who need it. Evidence
storage must also be armed for your organisation (card “Evidence storage” on the same tab),
otherwise the answer is 403 evidence_storage_not_armed. While Noxtica has the evidence image API
off for the platform, every route answers the byte-identical 404 of an unknown path. A key keeps its scopes
until a tenant administrator changes them (Edit scopes on the key in the Backoffice, API
Configuration): add read:evidence to the key, or create a new key that carries it. When a
tenant administrator withdraws the enablement, the very next request is refused, a signed URL
that has not expired yet included.
Which id. verificationId is the id returned by POST /v1/kyc/verify (a UUID) or the
verificationId of a hosted session (hosted: followed by 64 lower-case hexadecimal
characters); see KycVerificationId for the formats and the 128-character maximum. The id of
a hosted session and the evidenceGroupId of a verification made from your own capture
screens are accepted too and resolve to the same verification. An id that is malformed,
unknown, or belongs to another organisation gets the same byte-identical 404 as an unknown
path.
What is listed. Newest first, at most 100 items, only images that are ready to be opened.
type and side tell the images apart:
type | side | The image shows |
|---|---|---|
document | front or back | One side of an identity document. |
passport_page | null | A page of a passport. |
proof_of_address | null | A proof of address. |
selfie | null | A selfie of the person. |
document_near_face | null | The document held next to the face. |
supporting_document | null | A supporting document the person added. |
contentType is image/jpeg, image/png or image/webp, size is the size in bytes and
capturedAt is when Noxtica received the image. An item has no hash, purpose or storage
detail.
Audit and handling. Listing does not release an image and is not itself recorded in the
access log; asking for access and fetching an image are (see kycVerificationEvidenceAccess
and kycEvidenceContent). Every response of this route carries Cache-Control: no-store, private. Images are never sent in webhooks.
Rate limit. A per-key limit applies to this route, the access route and the access log
together; a limited request answers 429 with Retry-After. Fetching a signed URL has a
separate per-key limit.
Authentication: Bearer token (ApiKeyAuth), scope read:evidence.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
verificationId | path | string | Yes | The verificationId of the verification. Pattern ^[A-Za-z0-9:_-]{1,128}$. |
Responses
| Status | Meaning | Error codes | Content |
|---|---|---|---|
200 | The images of the verification that can be opened, newest first. | application/json KycEvidenceList | |
400 | invalid_request — the request could not be parsed, or a query value is out of range. Nothing was read. | invalid_request | application/json ErrorEnvelope |
401 | The Authorization: Bearer sk_... header is missing or malformed, or the key is unknown or revoked. The body is deliberately identical for every one of those causes. | invalid_api_key | application/json KycSessionError |
403 | evidence_read_not_enabled — a tenant administrator has not enabled evidence image access for your organisation (or withdrew it), so nothing was read; evidence_storage_not_armed — evidence storage is not on for your organisation; or the key lacks the read:evidence scope (error is API key does not have read:evidence scope). A tenant administrator fixes the first two in the Backoffice, KYC → Invitations → Settings tab (cards “Evidence image API” and “Evidence storage”). | evidence_read_not_enabled, evidence_storage_not_armed | application/json KycEvidenceForbiddenError |
404 | The byte-identical “V1 endpoint not found” response, returned for a malformed id, an id that does not exist, an id or an image that belongs to another organisation or to another verification, an item that is not a readable image, or a route that is not available for your organisation: indistinguishable from an unknown path by design. | V1 endpoint not found | application/json |
410 | verification_deleted — you deleted this verification; its images no longer exist. | verification_deleted | application/json ErrorEnvelope |
429 | Rate limit exceeded. Wait Retry-After seconds (also retryAfter in the body) before retrying; the response carries no other rate-limit headers. | Rate limit exceeded | application/json |
500 | API-key authentication is not configured on this deployment (server_misconfigured). Retrying does not help; contact support with the requestId. | server_misconfigured | application/json KycSessionError |
503 | evidence_audit_unavailable or evidence_unavailable — retryable. The access could not be recorded, so nothing was released; or the stored images could not be read right now. | evidence_audit_unavailable, evidence_unavailable | application/json ErrorEnvelope |
503 | Noxtica could not read the key’s record. This is a temporary fault on Noxtica’s side and says nothing about your key. Retry after the Retry-After delay; do not treat it as a revoked key. | api_key_auth_unavailable | application/json |
Response headers
| Status | Header | Type | Description |
|---|---|---|---|
200 | Cache-Control | string | Always no-store, private. Never cache or store a response of an evidence image route. |
400 | Cache-Control | string | Always no-store, private. Never cache or store a response of an evidence image route. |
403 | Cache-Control | string | Always no-store, private. Never cache or store a response of an evidence image route. |
404 | Cache-Control | string | Always no-store, private. Never cache or store a response of an evidence image route. |
410 | Cache-Control | string | Always no-store, private. Never cache or store a response of an evidence image route. |
429 | Retry-After | integer | Seconds until the bucket allows another request. |
503 | Cache-Control | string | Always no-store, private. Never cache or store a response of an evidence image route. |
503 | Retry-After | integer | Seconds to wait before retrying. |
Response fields (200, application/json)
| Field | Type | Always returned | Description |
|---|---|---|---|
ok | boolean | Yes | Allowed values: true. |
verificationId | string | Yes | The identifier of one verification. Treat it as an opaque string and store it in a column that holds at least 128 characters. The API contract fixes two limits that hold for every id: it is never longer than 128 characters, and it only contains the characters A-Z, a-z, 0-9, :, _ and -. Today an id has one of two shapes. A verification created through POST /v1/kyc/verify (including one made from an evidenceGroupId, and each item of a batch) is a lower-case UUID: 36 characters, for example 3fa85f64-5717-4562-b3fc-2c963f66afa6. The verification a hosted session runs when the customer submits is hosted: followed by 64 lower-case hexadecimal characters: 71 characters in all. Do not parse an id or rely on its prefix; a new shape may appear, but always within the two limits above. At least 1 character, at most 128 characters. Pattern ^[A-Za-z0-9:_-]{1,128}$. |
items | array of KycEvidenceItem | Yes | At most 100 items. |
items[] | object (KycEvidenceItem) | No | One image a person captured during a verification. It holds no image bytes, hash or storage detail. |
items[].itemId | string | Yes | The opaque id of the image. Use it as itemId of the access route. Store it in a column that holds at least 128 characters. Pattern ^[A-Za-z0-9_-]{1,128}$. |
items[].type | string | Yes | What the image shows. Videos, voice recordings and extracted data are never listed. Allowed values: document, passport_page, proof_of_address, selfie, document_near_face, supporting_document. |
items[].side | string, nullable | Yes | front or back for a document; null for every other type. Allowed values: front, back, null. |
items[].contentType | string | Yes | The type of the image. Allowed values: image/jpeg, image/png, image/webp. |
items[].size | integer | Yes | The size of the image in bytes. Minimum 0. |
items[].capturedAt | string (date-time) | Yes | When Noxtica received the image. |
requestId | string | Yes |
Response example (200)
{
"ok": true,
"verificationId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"items": [
{
"itemId": "evd_example_0003",
"type": "selfie",
"side": null,
"contentType": "image/png",
"size": 95311,
"capturedAt": "2026-11-05T10:12:40.000Z"
},
{
"itemId": "evd_example_0002",
"type": "document",
"side": "back",
"contentType": "image/jpeg",
"size": 171903,
"capturedAt": "2026-11-05T10:11:58.000Z"
},
{
"itemId": "evd_example_0001",
"type": "document",
"side": "front",
"contentType": "image/jpeg",
"size": 183422,
"capturedAt": "2026-11-05T10:11:12.000Z"
}
],
"requestId": "req-test"
}
Example request
curl
curl "https://collect.noxtica.com/v1/kyc/verifications/example-verificationId/evidence" \
-H "Authorization: Bearer $NOXTICA_API_KEY"
Node.js (global fetch, Node 18 or later)
const response = await fetch('https://collect.noxtica.com/v1/kyc/verifications/example-verificationId/evidence', {
method: 'GET',
headers: {
Authorization: `Bearer ${process.env.NOXTICA_API_KEY}`,
},
});
console.log(response.status, await response.text());
Python (standard library)
import os
import urllib.request
request = urllib.request.Request(
"https://collect.noxtica.com/v1/kyc/verifications/example-verificationId/evidence",
headers={
"Authorization": f"Bearer {os.environ['NOXTICA_API_KEY']}",
},
method="GET",
)
with urllib.request.urlopen(request) as response:
print(response.status, response.read().decode())
Get a short-lived signed URL for one evidence image
POST /v1/kyc/verifications/{verificationId}/evidence/{itemId}/access
Server-to-server. Returns a signed URL for one image listed by
GET /v1/kyc/verifications/{verificationId}/evidence. Hand the URL to the operator’s browser,
or fetch it from your backend and pass the bytes on. The URL is the credential:
GET /v1/kyc/evidence/content needs no Authorization header. The key needs the
read:evidence scope, and the opt-in described on kycVerificationEvidenceList applies.
The URL. It is valid for 120 seconds from the grant (see expiresAt). The lifetime is
fixed: it cannot be extended or requested longer. Within that time the URL can be fetched
more than once, for example when a browser reloads the image; every fetch is recorded and
rate limited. Anyone who holds the URL can fetch the image until it expires, so treat it as a
secret: give it only to the operator who asked to open the image, never log or store it, and
never put it on a page that loads content from other sites. Ask for a new URL instead of
keeping one. The URL has the host you called, and its query holds an opaque token: do not
parse, shorten or rebuild it.
What stops a URL early. A URL stops working at once when the key that asked for it is
revoked or loses the read:evidence scope, when a tenant administrator withdraws the
enablement, and when the image no longer exists.
Which ids. verificationId is accepted in the same forms as on
kycVerificationEvidenceList. itemId is the itemId of an item of that list. A malformed
or unknown id, an item that belongs to another verification or another organisation, and an
item that is not a readable image (a video, a voice recording, extracted data) all get the
same byte-identical 404 as an unknown path.
Availability. An item of this verification that is an image but whose storage did not
finish successfully answers 409 evidence_not_available with reason not_usable. A
verification you deleted answers 410 verification_deleted.
Audit and handling. Each grant is recorded for your organisation before the URL is
released: the key, the verification, the type of image, the time and, as Noxtica saw them,
the caller address and user agent; never the image. If the record cannot be written, nothing
is released and the answer is 503 evidence_audit_unavailable. Read the record with
GET /v1/kyc/verifications/{verificationId}/evidence/access-log. Every response of this route
carries Cache-Control: no-store, private: do not cache or log the body.
Request body. None is needed. An empty JSON object is accepted and ignored.
Rate limit. The same per-key limit as kycVerificationEvidenceList.
Authentication: Bearer token (ApiKeyAuth), scope read:evidence.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
verificationId | path | string | Yes | The verificationId of the verification. Pattern ^[A-Za-z0-9:_-]{1,128}$. |
itemId | path | string | Yes | The itemId of an item of the evidence list of that verification. Pattern ^[A-Za-z0-9_-]{1,128}$. |
Request body
Content type application/json, optional.
Type: object.
Responses
| Status | Meaning | Error codes | Content |
|---|---|---|---|
200 | A signed URL for the image. It is valid until expiresAt, 120 seconds after the grant. | application/json KycEvidenceAccess | |
400 | invalid_request — the request could not be parsed, or a query value is out of range. Nothing was read. | invalid_request | application/json ErrorEnvelope |
401 | The Authorization: Bearer sk_... header is missing or malformed, or the key is unknown or revoked. The body is deliberately identical for every one of those causes. | invalid_api_key | application/json KycSessionError |
403 | evidence_read_not_enabled — a tenant administrator has not enabled evidence image access for your organisation (or withdrew it), so nothing was read; evidence_storage_not_armed — evidence storage is not on for your organisation; or the key lacks the read:evidence scope (error is API key does not have read:evidence scope). A tenant administrator fixes the first two in the Backoffice, KYC → Invitations → Settings tab (cards “Evidence image API” and “Evidence storage”). | evidence_read_not_enabled | application/json KycEvidenceForbiddenError |
404 | The byte-identical “V1 endpoint not found” response, returned for a malformed id, an id that does not exist, an id or an image that belongs to another organisation or to another verification, an item that is not a readable image, or a route that is not available for your organisation: indistinguishable from an unknown path by design. | V1 endpoint not found | application/json |
409 | evidence_not_available — the item is an image of this verification, but its storage did not finish successfully, so it cannot be opened. reason is not_usable. | evidence_not_available | application/json KycEvidenceNotAvailableError |
410 | verification_deleted — you deleted this verification; its images no longer exist. | verification_deleted | application/json ErrorEnvelope |
429 | Rate limit exceeded. Wait Retry-After seconds (also retryAfter in the body) before retrying; the response carries no other rate-limit headers. | Rate limit exceeded | application/json |
500 | API-key authentication is not configured on this deployment (server_misconfigured). Retrying does not help; contact support with the requestId. | server_misconfigured | application/json KycSessionError |
503 | evidence_audit_unavailable or evidence_unavailable — retryable. The access could not be recorded, so nothing was released; or the stored images could not be read right now. | evidence_audit_unavailable, evidence_unavailable | application/json ErrorEnvelope |
503 | Noxtica could not read the key’s record. This is a temporary fault on Noxtica’s side and says nothing about your key. Retry after the Retry-After delay; do not treat it as a revoked key. | api_key_auth_unavailable | application/json |
Response headers
| Status | Header | Type | Description |
|---|---|---|---|
200 | Cache-Control | string | Always no-store, private. Never cache or store a response of an evidence image route. |
400 | Cache-Control | string | Always no-store, private. Never cache or store a response of an evidence image route. |
403 | Cache-Control | string | Always no-store, private. Never cache or store a response of an evidence image route. |
404 | Cache-Control | string | Always no-store, private. Never cache or store a response of an evidence image route. |
409 | Cache-Control | string | Always no-store, private. Never cache or store a response of an evidence image route. |
410 | Cache-Control | string | Always no-store, private. Never cache or store a response of an evidence image route. |
429 | Retry-After | integer | Seconds until the bucket allows another request. |
503 | Cache-Control | string | Always no-store, private. Never cache or store a response of an evidence image route. |
503 | Retry-After | integer | Seconds to wait before retrying. |
Response fields (200, application/json)
| Field | Type | Always returned | Description |
|---|---|---|---|
ok | boolean | Yes | Allowed values: true. |
verificationId | string | Yes | The identifier of one verification. Treat it as an opaque string and store it in a column that holds at least 128 characters. The API contract fixes two limits that hold for every id: it is never longer than 128 characters, and it only contains the characters A-Z, a-z, 0-9, :, _ and -. Today an id has one of two shapes. A verification created through POST /v1/kyc/verify (including one made from an evidenceGroupId, and each item of a batch) is a lower-case UUID: 36 characters, for example 3fa85f64-5717-4562-b3fc-2c963f66afa6. The verification a hosted session runs when the customer submits is hosted: followed by 64 lower-case hexadecimal characters: 71 characters in all. Do not parse an id or rely on its prefix; a new shape may appear, but always within the two limits above. At least 1 character, at most 128 characters. Pattern ^[A-Za-z0-9:_-]{1,128}$. |
itemId | string | Yes | The image the URL opens. Pattern ^[A-Za-z0-9_-]{1,128}$. |
url | string (uri) | Yes | The signed URL, on the host you called. Fetch it with GET and no Authorization header. Treat it as a secret; do not log or store it. |
expiresAt | string (date-time) | Yes | When the URL stops working. |
expiresInSeconds | integer | Yes | The fixed lifetime of the URL, in seconds. Allowed values: 120. |
contentType | string | Yes | The type of the image the URL returns. Allowed values: image/jpeg, image/png, image/webp. |
size | integer | Yes | The size of the image in bytes. Minimum 0. |
requestId | string | Yes |
Response example (200)
{
"ok": true,
"verificationId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"itemId": "evd_example_0001",
"url": "https://collect.noxtica.com/v1/kyc/evidence/content?token=v1.EXAMPLE_PAYLOAD.EXAMPLE_SIGNATURE",
"expiresAt": "2026-11-05T10:13:12.000Z",
"expiresInSeconds": 120,
"contentType": "image/jpeg",
"size": 183422,
"requestId": "req-test"
}
Example request
curl
curl -X POST "https://collect.noxtica.com/v1/kyc/verifications/example-verificationId/evidence/example-itemId/access" \
-H "Authorization: Bearer $NOXTICA_API_KEY" \
-H "Content-Type: application/json" \
-d '{}'
Node.js (global fetch, Node 18 or later)
const response = await fetch('https://collect.noxtica.com/v1/kyc/verifications/example-verificationId/evidence/example-itemId/access', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.NOXTICA_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({}),
});
console.log(response.status, await response.text());
Python (standard library)
import json
import os
import urllib.request
request = urllib.request.Request(
"https://collect.noxtica.com/v1/kyc/verifications/example-verificationId/evidence/example-itemId/access",
data=json.dumps({}).encode(),
headers={
"Authorization": f"Bearer {os.environ['NOXTICA_API_KEY']}",
"Content-Type": "application/json",
},
method="POST",
)
with urllib.request.urlopen(request) as response:
print(response.status, response.read().decode())
Fetch an evidence image with a signed URL
GET /v1/kyc/evidence/content
Returns the bytes of one image. The signed URL that kycVerificationEvidenceAccess returned
is the credential: send no Authorization header, and use the URL exactly as it was given.
The response. 200 carries the image itself. Content-Type is the type of the stored
image (image/jpeg, image/png or image/webp), Content-Disposition is
inline; filename="evidence-<itemId>.<jpg|png|webp>" and Content-Length is the exact size.
Every response of this route, errors included, carries Cache-Control: no-store, private,
X-Content-Type-Options: nosniff and Referrer-Policy: no-referrer: do not cache or store the
response.
Checks on every fetch. A URL that was altered, is malformed or has expired answers
401 invalid_or_expired_url; the body is the same for every one of those causes. The same
401 follows when the key that asked for the URL has been revoked or no longer carries the
read:evidence scope. The tenant administrator’s enablement and evidence storage must still
be in place, otherwise the answer is 403 evidence_read_not_enabled or
403 evidence_storage_not_armed. An image that no longer exists, or that is not a readable
image of the verification the URL was granted for, answers the byte-identical 404.
Audit. Each fetch is recorded for your organisation before any byte is released, with the
key, the verification, the type of image, the time and, as Noxtica saw them, the caller
address and user agent. If the record cannot be written, nothing is released and the answer is
503 evidence_audit_unavailable. A request that is refused before that point is not
recorded. The record is readable with
GET /v1/kyc/verifications/{verificationId}/evidence/access-log.
Stored image check. Noxtica checks that the bytes are an image of the type they were
stored as before serving them. If they are not, nothing is served and the answer is
502 evidence_unreadable.
Rate limit. A per-key limit applies to fetches, separate from the one on the other
evidence image routes; a limited request answers 429 with Retry-After.
Authentication: none. This operation does not take a credential.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
token | query | string | Yes | The signed token of the URL returned by kycVerificationEvidenceAccess. Use the URL as returned; do not build or change the token. At least 1 character. |
Responses
| Status | Meaning | Error codes | Content |
|---|---|---|---|
200 | The image. | image/jpeg, image/png, image/webp | |
401 | invalid_or_expired_url — the URL was altered, is malformed or has expired, or the key that asked for it is revoked or no longer carries the read:evidence scope. The body is the same for every cause. | invalid_or_expired_url | application/json ErrorEnvelope |
403 | evidence_read_not_enabled — a tenant administrator has withdrawn (or never gave) the enablement for your organisation; or evidence_storage_not_armed — evidence storage is not on for your organisation. Nothing was released. A tenant administrator fixes either in the Backoffice, KYC → Invitations → Settings tab (cards “Evidence image API” and “Evidence storage”). | evidence_read_not_enabled, evidence_storage_not_armed | application/json ErrorEnvelope |
404 | The byte-identical “V1 endpoint not found” response, returned for a malformed id, an id that does not exist, an id or an image that belongs to another organisation or to another verification, an item that is not a readable image, or a route that is not available for your organisation: indistinguishable from an unknown path by design. | V1 endpoint not found | application/json |
429 | Rate limit exceeded. Wait Retry-After seconds (also retryAfter in the body) before retrying; the response carries no other rate-limit headers. | Rate limit exceeded | application/json |
502 | evidence_unreadable — the stored bytes are not an image of the type they were stored as, so nothing was served. The fetch record had already been written. | evidence_unreadable | application/json ErrorEnvelope |
503 | evidence_audit_unavailable or evidence_unavailable — retryable. The access could not be recorded, so nothing was released; or the stored images could not be read right now. | evidence_audit_unavailable, evidence_unavailable | application/json ErrorEnvelope |
Response headers
| Status | Header | Type | Description |
|---|---|---|---|
200 | Cache-Control | string | Always no-store, private. Never cache or store a response of an evidence image route. |
200 | Content-Disposition | string | Always inline, with the file name evidence-<itemId>.<jpg|png|webp>; the extension follows the image type. |
200 | Content-Length | integer | The exact size of the image in bytes. |
200 | X-Content-Type-Options | string | Always nosniff. |
401 | Cache-Control | string | Always no-store, private. Never cache or store a response of an evidence image route. |
403 | Cache-Control | string | Always no-store, private. Never cache or store a response of an evidence image route. |
404 | Cache-Control | string | Always no-store, private. Never cache or store a response of an evidence image route. |
429 | Retry-After | integer | Seconds until the bucket allows another request. |
502 | Cache-Control | string | Always no-store, private. Never cache or store a response of an evidence image route. |
503 | Cache-Control | string | Always no-store, private. Never cache or store a response of an evidence image route. |
Example request
curl
curl "https://collect.noxtica.com/v1/kyc/evidence/content?token=v1.EXAMPLE_PAYLOAD.EXAMPLE_SIGNATURE"
Node.js (global fetch, Node 18 or later)
const response = await fetch('https://collect.noxtica.com/v1/kyc/evidence/content?token=v1.EXAMPLE_PAYLOAD.EXAMPLE_SIGNATURE', {
method: 'GET',
});
console.log(response.status, await response.text());
Python (standard library)
import urllib.request
request = urllib.request.Request(
"https://collect.noxtica.com/v1/kyc/evidence/content?token=v1.EXAMPLE_PAYLOAD.EXAMPLE_SIGNATURE",
method="GET",
)
with urllib.request.urlopen(request) as response:
print(response.status, response.read().decode())
List who opened the evidence images of a verification
GET /v1/kyc/verifications/{verificationId}/evidence/access-log
Server-to-server. Lists the openings of the images of one of your own verifications, newest
first: one row for each access grant and one for each fetch of a signed URL. Use it to answer
who looked at an image, from where and when. A row never holds image bytes, a signed URL or
its token. The key needs the read:evidence scope.
After the enablement is withdrawn. Unlike the other evidence image routes, this one does
not need the tenant administrator’s enablement to be in place, nor evidence storage to be on,
so you can still read the history after the enablement was withdrawn. It is not available
while evidence image access is not available for your organisation (404).
Which id. verificationId is accepted in the same forms as on
kycVerificationEvidenceList; a malformed, unknown or foreign id gets the same
byte-identical 404. A verification you deleted answers 410 verification_deleted.
Rows. event is grant (a signed URL was issued) or fetch (the image was served).
itemId is the image, apiKeyId the key that asked for the URL, and at the time. ip and
userAgent are what Noxtica saw on the request (the address the request came from and its
User-Agent header, shortened to 256 characters) and are null when there was none.
Paging. limit is 1 to 100 (default 50). When more rows exist, nextBefore is a cursor in
Unix milliseconds: pass it unchanged as before to read the next page. It is null on the
last page. A limit or before that is repeated, not a whole number or out of range answers
400 invalid_request.
Handling. Every response of this route carries Cache-Control: no-store, private.
Rate limit. The same per-key limit as kycVerificationEvidenceList.
Authentication: Bearer token (ApiKeyAuth), scope read:evidence.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
verificationId | path | string | Yes | The verificationId of the verification. Pattern ^[A-Za-z0-9:_-]{1,128}$. |
limit | query | integer | No | The most rows to return. Minimum 1, maximum 100. Default 50. |
before | query | integer (int64) | No | A cursor in Unix milliseconds. Only rows older than it are returned. Use nextBefore of the previous page unchanged. Minimum 1. |
Responses
| Status | Meaning | Error codes | Content |
|---|---|---|---|
200 | The recorded openings of the images of the verification, newest first. | application/json KycEvidenceAccessLog | |
400 | invalid_request — the request could not be parsed, or a query value is out of range. Nothing was read. | invalid_request | application/json ErrorEnvelope |
401 | The Authorization: Bearer sk_... header is missing or malformed, or the key is unknown or revoked. The body is deliberately identical for every one of those causes. | invalid_api_key | application/json KycSessionError |
403 | evidence_read_not_enabled — a tenant administrator has not enabled evidence image access for your organisation (or withdrew it), so nothing was read; evidence_storage_not_armed — evidence storage is not on for your organisation; or the key lacks the read:evidence scope (error is API key does not have read:evidence scope). A tenant administrator fixes the first two in the Backoffice, KYC → Invitations → Settings tab (cards “Evidence image API” and “Evidence storage”). | evidence_read_not_enabled | application/json KycEvidenceForbiddenError |
404 | The byte-identical “V1 endpoint not found” response, returned for a malformed id, an id that does not exist, an id or an image that belongs to another organisation or to another verification, an item that is not a readable image, or a route that is not available for your organisation: indistinguishable from an unknown path by design. | V1 endpoint not found | application/json |
410 | verification_deleted — you deleted this verification; its images no longer exist. | verification_deleted | application/json ErrorEnvelope |
429 | Rate limit exceeded. Wait Retry-After seconds (also retryAfter in the body) before retrying; the response carries no other rate-limit headers. | Rate limit exceeded | application/json |
500 | API-key authentication is not configured on this deployment (server_misconfigured). Retrying does not help; contact support with the requestId. | server_misconfigured | application/json KycSessionError |
503 | evidence_audit_unavailable or evidence_unavailable — retryable. The access could not be recorded, so nothing was released; or the stored images could not be read right now. | evidence_audit_unavailable, evidence_unavailable | application/json ErrorEnvelope |
503 | Noxtica could not read the key’s record. This is a temporary fault on Noxtica’s side and says nothing about your key. Retry after the Retry-After delay; do not treat it as a revoked key. | api_key_auth_unavailable | application/json |
Response headers
| Status | Header | Type | Description |
|---|---|---|---|
200 | Cache-Control | string | Always no-store, private. Never cache or store a response of an evidence image route. |
400 | Cache-Control | string | Always no-store, private. Never cache or store a response of an evidence image route. |
403 | Cache-Control | string | Always no-store, private. Never cache or store a response of an evidence image route. |
404 | Cache-Control | string | Always no-store, private. Never cache or store a response of an evidence image route. |
410 | Cache-Control | string | Always no-store, private. Never cache or store a response of an evidence image route. |
429 | Retry-After | integer | Seconds until the bucket allows another request. |
503 | Cache-Control | string | Always no-store, private. Never cache or store a response of an evidence image route. |
503 | Retry-After | integer | Seconds to wait before retrying. |
Response fields (200, application/json)
| Field | Type | Always returned | Description |
|---|---|---|---|
ok | boolean | Yes | Allowed values: true. |
verificationId | string | Yes | The identifier of one verification. Treat it as an opaque string and store it in a column that holds at least 128 characters. The API contract fixes two limits that hold for every id: it is never longer than 128 characters, and it only contains the characters A-Z, a-z, 0-9, :, _ and -. Today an id has one of two shapes. A verification created through POST /v1/kyc/verify (including one made from an evidenceGroupId, and each item of a batch) is a lower-case UUID: 36 characters, for example 3fa85f64-5717-4562-b3fc-2c963f66afa6. The verification a hosted session runs when the customer submits is hosted: followed by 64 lower-case hexadecimal characters: 71 characters in all. Do not parse an id or rely on its prefix; a new shape may appear, but always within the two limits above. At least 1 character, at most 128 characters. Pattern ^[A-Za-z0-9:_-]{1,128}$. |
events | array of KycEvidenceAccessEvent | Yes | |
events[] | object (KycEvidenceAccessEvent) | No | One recorded opening of an image. It never holds image bytes, a signed URL or its token. |
events[].id | string | Yes | The id of the record. |
events[].at | string (date-time) | Yes | When it happened. |
events[].event | string | Yes | grant — a signed URL was issued. fetch — the image was served. Allowed values: grant, fetch. |
events[].itemId | string | Yes | The image. |
events[].apiKeyId | string | Yes | The id of the API key that asked for the URL. |
events[].ip | string, nullable | Yes | The address the request came from as Noxtica saw it; null when there was none. |
events[].userAgent | string, nullable | Yes | The User-Agent of the request, at most 256 characters; null when there was none. At most 256 characters. |
nextBefore | integer (int64), nullable | Yes | A cursor in Unix milliseconds. Pass it unchanged as before to read the next page; null on the last page. |
requestId | string | Yes |
Response example (200)
{
"ok": true,
"verificationId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"events": [
{
"id": "req-example-0002",
"at": "2026-11-05T10:11:40.000Z",
"event": "fetch",
"itemId": "evd_example_0001",
"apiKeyId": "key_example_01",
"ip": "203.0.113.7",
"userAgent": "ExampleBackend/1.0"
},
{
"id": "req-example-0001",
"at": "2026-11-05T10:11:12.000Z",
"event": "grant",
"itemId": "evd_example_0001",
"apiKeyId": "key_example_01",
"ip": "203.0.113.7",
"userAgent": "ExampleBackend/1.0"
}
],
"nextBefore": null,
"requestId": "req-test"
}
Example request
curl
curl "https://collect.noxtica.com/v1/kyc/verifications/example-verificationId/evidence/access-log" \
-H "Authorization: Bearer $NOXTICA_API_KEY"
Node.js (global fetch, Node 18 or later)
const response = await fetch('https://collect.noxtica.com/v1/kyc/verifications/example-verificationId/evidence/access-log', {
method: 'GET',
headers: {
Authorization: `Bearer ${process.env.NOXTICA_API_KEY}`,
},
});
console.log(response.status, await response.text());
Python (standard library)
import os
import urllib.request
request = urllib.request.Request(
"https://collect.noxtica.com/v1/kyc/verifications/example-verificationId/evidence/access-log",
headers={
"Authorization": f"Bearer {os.environ['NOXTICA_API_KEY']}",
},
method="GET",
)
with urllib.request.urlopen(request) as response:
print(response.status, response.read().decode())
Mint a Noxtica-hosted KYC verification-session link
POST /v1/kyc/sessions
Server-to-server. Creates a hosted verification session and returns a one-shot, opaque URL to hand to your end customer (redirect, email, SMS or QR). The URL embeds a bearer session token that is never returned again by any route: treat it as a secret, do not log it and do not put it in an analytics URL. Minting is free; the verification is counted when the customer submits.
Before you can mint. (1) The identity verification module must be enabled for your organisation;
until it is, this route answers the same byte-identical 404 as an unknown path. (2) Your
sk_ key needs the write:identity scope (a key created with the default scope is
refused with 403). (3) A tenant administrator must have recorded an active legal-basis
receipt for hosted document capture in the Backoffice; without it the mint is refused with
403 kyc_legal_basis_receipt_required. Noxtica never selects a legal basis for you. (4)
Your organisation needs a hosted configuration (business name, privacy URL, supported
documents) in the Backoffice, or the customer sees “Verification isn’t ready yet”; the mint
itself does not check this. (5) Your organisation’s raw-evidence storage must be provisioned
in the Backoffice: evidence storage armed and an evidence residency of TR, EU or US
set. A link for an organisation that cannot store the customer’s capture would fail after
the first photo, so the mint is refused before any session exists with
403 evidence_not_provisioned and a reason (raw_storage_not_armed or
residency_unconfigured). Retrying does not help until the organisation is provisioned.
Request. subjectRef is required. The expiry field is expiresInSeconds: a key named
ttlSeconds is not read and is silently ignored (the link then lasts the 24 hour default).
Unknown keys are ignored. The snake_case spellings subject_ref, return_url,
id_type, expires_in_seconds and hosted_options are also accepted; the camelCase names
are the contract. Locale, customer name, email and phone are not request fields: the page
language follows the customer’s browser and cannot be forced, and the webhook endpoint is
registered once for your organisation in the Backoffice, not per session.
Lifetime and expiry. expiresAt is Unix time in milliseconds in every response.
At mint it is now + expiresInSeconds: 24 hours (86400 s) by default, or the 5
minutes (300 s) to 7 days (604800 s) you ask for; a value outside that range is refused
with 400 invalid_request and is never clamped. An unopened link keeps that whole
validity. The first customer action the platform records on the hosted page (for example
accepting the privacy notice) opens the session: openedAt is stamped and expiresAt
becomes min(expiresAt, openedAt + 30 minutes), so the customer must finish capture
inside that window. The deadline only ever moves earlier: a link minted for 10 minutes
still ends 10 minutes after mint, and a link minted for 24 hours ends 30 minutes after the
customer first acts. It is never extended and there is no renewal call: when a customer
needs more time, mint a new session. Each timed capture step (document front or back,
selfie, document near face, liveness, voice) also has its own 10 minute limit; a step that
runs out restarts and the session carries on until its own deadline. When the deadline
passes the session is expired: the platform records it on the next read or on its expiry
sweep (every 5 minutes), the hosted page answers 410 session_expired,
GET /v1/kyc/sessions/{sessionId} keeps answering 200 with status: "expired", a
session that was never submitted is never billed, and kyc.session.completed is never
sent. A session in review_pending never expires: it waits for a Backoffice reviewer.
Using the link. The hosted page cannot be embedded in an iframe (X-Frame-Options: DENY); open it in a new tab, or redirect the customer to it. The page never posts a
message to the opener or parent window and never redirects on its own. If you minted with
a returnUrl, the page shows a plain “Return to <business>” link to exactly that URL
(nothing is appended) on the completion and status screens only for a completed outcome,
and on the screen shown when a step cannot continue. Sessions created from Backoffice
invitations carry no returnUrl. Correlate by the sessionId you stored at mint, and
never treat a visit to your return page as proof of completion.
Outcome. Learn the outcome with the kyc.session.completed webhook or by polling
GET /v1/kyc/sessions/{sessionId}. With the identity provider configured today every
successful capture ends in review_pending, not completed: the webhook fires only
after a Backoffice reviewer approves or overrides the case (usually within seconds,
at most about 5 minutes after the decision), and a review_pending session never
expires. Read verification and review on the status read to tell an approval from an
override. See that route for the status table.
Email verification (optional step). hostedOptions.steps.emailVerification
(off, optional or required) adds an email-verification step to the hosted flow: the
customer types an address, receives one confirmation link, and only a click on that link
marks the address verified. Omit the key to use your organisation’s default (Hosted
configuration); an explicit value for this link wins in both directions. The effective
mode is resolved once, here, and is fixed for the session. This is available only once the
platform has enabled hosted email verification: until then an explicit optional or
required is refused with 400 email_verification_unavailable (an explicit off is
always accepted, and your organisation default is ignored), so a required session can
never silently run unverified.
Domains (optional). One organisation can own several domains, and each domain can carry
its own hosted configuration (business name, purpose, privacy and support pages, supported
documents, approved return hosts, embed origins, email verification) that overrides your
organisation default field by field; see GET /v1/kyc/config. The invitation is created
for one domain, chosen in this order: domainId in the body, then siteKey in the body,
then the request’s Origin header when it exactly equals the origin of one of your active
domains (any other origin is ignored). With none of these the invitation uses your
organisation default. A domainId and a siteKey that name different domains are refused
with 400 domain_mismatch; an unknown or disabled domain, or one that belongs to another
organisation, is refused with 404 domain_not_found (the same answer in all three cases,
so the route cannot be used to discover other organisations’ domains). The domain is fixed
at mint: the returnUrl check and the email-verification mode use that domain’s
configuration, and the hosted page always shows the configuration the domain has at that
moment (if you remove a domain’s override later, its sessions fall back to your
organisation default). The mint response and the status read carry the session’s
domainId, null for the organisation default. Until a domain has its own configuration,
every invitation behaves as before.
Countries (optional). hostedOptions.allowedIssuingCountries narrows one invitation to
some issuing countries, for example the countries you expect this customer to come from,
without changing your hosted configuration. Send any ISO 3166-1 alpha-2 codes
(CountryCodeList: 1 to 249 distinct codes; all 249 is the same as sending none). Each
document type your configuration accepts keeps only the issuing countries that are also in
the list, and a document type left with no country is dropped from the invitation. When no
document type is left the mint is refused with 400 invalid_hosted_options (field
allowedIssuingCountries, reason no_matching_document_country) and no session is
created. Omit the key to keep every configured country. A list outside your configuration
is not an error as long as one document type remains. hostedOptions must be the complete
object when you send it, so send this key together with the other required keys. The list
is fixed at mint: the hosted page shows your configuration at that moment narrowed by it,
and the customer cannot choose a country outside it.
Idempotency. Idempotency-Key is optional (8 to 128 characters). Without it every
call mints a fresh session. With it, a retry under the same key returns the SAME session
with replayed: true and WITHOUT the url: the one-shot URL of a lost response cannot be
recovered, so mint a new session under a new key instead. The replay matches on the key
only; a different body under the same key still returns the original session.
Rate limits. A per-source-IP limit applies first, before authentication, then a per-key limit shared with the status read. See “Rate limits” above.
Authentication: Bearer token (ApiKeyAuth), scope write:identity.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
Idempotency-Key | header | string | No | When supplied, a replayed key returns the SAME minted session (WITHOUT re-exposing its one-shot URL) instead of a fresh mint. Omit entirely for a plain, always-fresh mint. At least 8 characters, at most 128 characters. |
Request body
Content type application/json, required.
| Field | Type | Required | Description |
|---|---|---|---|
subjectRef | string | Yes | Your own opaque reference for this end customer (trimmed, non-empty). It is returned to you as subjectId on the status read and is the identifier an erasure request targets. It is never sent in webhooks. At least 1 character, at most 256 characters. |
returnUrl | string (uri) | No | Where the “Return to <business>” link on the hosted page points. https only, no user info, at most 2048 characters, and the host must exactly equal (case insensitive) an entry in your organisation’s approved return hosts (allowedReturnUrlHosts in the Backoffice hosted configuration; at most 20 entries, host names only, no wildcards; port, path and query are not checked). An organisation with no approved hosts cannot send a returnUrl. Anything else is refused with 400 return_url_not_allowed. The page appends nothing to it. For an invitation created for a domain, the list is that domain’s own when it sets one, otherwise your organisation’s. At most 2048 characters. Pattern ^https://. |
purpose | string | No | Shown to the customer on the consent screen. Falls back to the default purpose of the invitation’s domain, then to your organisation’s default purpose. More than 500 characters is refused with 400 invalid_request. At most 500 characters. |
idType | string | No | Document type hint; it must be one of hostedOptions.allowedDocumentTypes (default: the first allowed type, national_id when hostedOptions is omitted), else 400 invalid_id_type. The customer still chooses the type and issuing country on the page from the allowed types your organisation configured. Allowed values: national_id, passport, driving_license, residence_permit. |
expiresInSeconds | integer | No | Lifetime of the UNOPENED link in seconds: 5 minutes to 7 days, default 24 hours. A value outside the range or a non-integer is refused with 400 invalid_request (it is not clamped). After the customer opens the link the 30 minute capture window applies. Minimum 300, maximum 604800. Default 86400. |
hostedOptions | object (KycHostedSessionOptions) | No | Invitation-scoped hosted-flow switches, frozen into the session at mint. When you send hostedOptions it must be the complete object (every required key below, no merging with defaults); omit it to use the defaults (all four document types, allowDeviceUpload false, documentBack auto, documentNearFace false, activeLiveness true, speechAnalysis false, ageGenderEstimation false, emailVerification off). Inside steps, ageGenderEstimation and emailVerification are optional. allowedIssuingCountries is optional too. |
hostedOptions.allowedDocumentTypes | array of string | Yes | At least 1 item, at most 4 items, unique items. |
hostedOptions.allowedIssuingCountries | array of string | No | Optional. Narrows the invitation to these issuing countries: each document type your hosted configuration accepts keeps only the issuing countries that are also listed, a document type left with none is dropped, and a mint where no document type remains is refused with 400 invalid_hosted_options (field allowedIssuingCountries, reason no_matching_document_country). Absent means no narrowing. Any ISO 3166-1 alpha-2 code is accepted, including codes your configuration does not offer. At least 1 item, at most 249 items, unique items. |
hostedOptions.allowDeviceUpload | boolean | Yes | |
hostedOptions.steps | object | Yes | |
hostedOptions.steps.documentBack | string | Yes | Allowed values: auto. |
hostedOptions.steps.documentNearFace | boolean | Yes | |
hostedOptions.steps.activeLiveness | boolean | Yes | |
hostedOptions.steps.speechAnalysis | boolean | Yes | |
hostedOptions.steps.ageGenderEstimation | boolean | No | Optional; default false. Adds an advisory age and gender estimate from the selfie frames (biometric, special-category data); the customer gives a separate consent on the page. Setting true is refused with 403 age_gender_estimation_not_enabled (reasonCode age_gender_platform_disabled or age_gender_tenant_acknowledgement_missing) unless the platform has enabled the feature AND your organisation has recorded the matching legal-basis acknowledgement in the Backoffice. It never fails a verification. |
hostedOptions.steps.emailVerification | string | No | Optional (added 2026-10-01; default off when the key is absent). When omitted from a mint request your organisation’s default applies; an explicit value wins in both directions. An explicit optional or required is refused with 400 email_verification_unavailable until the platform has enabled hosted email verification. The mint response echoes the effective mode. Allowed values: off, optional, required. |
domainId | string | No | Create the invitation for this domain of your organisation (the d_... id shown on the Backoffice Domains page), so that it uses that domain’s hosted configuration. Optional. It is chosen before siteKey and the Origin header (see “Domains” above). An unknown, disabled or foreign domain is refused with 404 domain_not_found. At least 1 character, at most 64 characters. |
siteKey | string | No | The site key (pk_...) of one of your active domains, as an alternative to domainId. When both are sent they must name the same domain, otherwise the call is refused with 400 domain_mismatch. Optional. An unknown or disabled site key is refused with 404 domain_not_found. At least 1 character. |
Responses
| Status | Meaning | Error codes | Content |
|---|---|---|---|
200 | Session minted, or (idempotent replay) the existing session returned without its URL. | application/json | |
400 | invalid_request (unreadable JSON, Idempotency-Key outside 8 to 128 characters, subjectRef missing or longer than 256, expiresInSeconds outside 300 to 604800 or not an integer, purpose longer than 500, a bad payload), invalid_id_type, invalid_hosted_options (hostedOptions is not a complete, well-formed object, steps.emailVerification is not one of off, optional, required, or allowedIssuingCountries is not a list of 1 to 249 distinct ISO 3166-1 alpha-2 codes, with field allowedIssuingCountries; also when none of your configured document types is offered in any listed country, with reason no_matching_document_country), return_url_not_allowed, email_verification_unavailable (an explicit steps.emailVerification of optional or required was requested but hosted email verification is not enabled for the platform), or domain_mismatch (domainId and siteKey name different domains). | domain_mismatch, email_verification_unavailable, invalid_hosted_options, invalid_id_type, invalid_request, return_url_not_allowed | application/json KycHostedOptionsError |
401 | The Authorization: Bearer sk_... header is missing or malformed, or the key is unknown or revoked. The body is deliberately identical for every one of those causes. | invalid_api_key | application/json KycSessionError |
403 | The key lacks the write:identity scope; or kyc_legal_basis_receipt_required (no active hosted-capture legal-basis receipt for your organisation); or evidence_not_provisioned (your organisation’s raw-evidence storage is not armed, reason raw_storage_not_armed, or is armed without an evidence residency of TR, EU or US, reason residency_unconfigured; no session was created); or age_gender_estimation_not_enabled (hostedOptions.steps.ageGenderEstimation was true but the platform feature or your organisation’s acknowledgement is missing; see reasonCode). | age_gender_estimation_not_enabled, evidence_not_provisioned, kyc_legal_basis_receipt_required | application/json KycSessionError |
404 | Either the byte-identical “V1 endpoint not found” response returned when the route’s module is not enabled for your organisation (see GatedNotFound), or domain_not_found: the domainId or siteKey is unknown, names a disabled domain, or belongs to another organisation. The three causes of domain_not_found get the same answer by design. | domain_not_found | application/json KycGatedOrDomainNotFoundError |
413 | body_too_large — the request body exceeds 32768 bytes. | body_too_large | application/json KycSessionError |
415 | The request is not Content-Type: application/json (body invalid_request). | invalid_request | application/json KycSessionError |
429 | Rate limit exceeded. Wait Retry-After seconds (also retryAfter in the body) before retrying; the response carries no other rate-limit headers. | Rate limit exceeded | application/json |
500 | API-key authentication is not configured on this deployment (server_misconfigured). Retrying does not help; contact support with the requestId. | server_misconfigured | application/json KycSessionError |
503 | Retryable unless noted. tenant_module_gate_unavailable (the module gate could not be answered; Retry-After: 2), kyc_legal_basis_receipt_unavailable (the receipt check faulted; reason, Retry-After: 2), hosted_verify_not_configured (the hosted deployment is not configured; contact support, no Retry-After), or invalid_request with a reason (a database or backend fault; Retry-After: 2). Retry a 503 with the SAME Idempotency-Key. | invalid_request | application/json KycSessionError |
503 | Noxtica could not read the key’s record. This is a temporary fault on Noxtica’s side and says nothing about your key. Retry after the Retry-After delay; do not treat it as a revoked key. | api_key_auth_unavailable | application/json |
Response headers
| Status | Header | Type | Description |
|---|---|---|---|
429 | Retry-After | integer | Seconds until the bucket allows another request. |
503 | Retry-After | integer | Seconds to wait before retrying. |
Response fields (200, application/json)
| Field | Type | Always returned | Description |
|---|---|---|---|
ok | boolean | Yes | Allowed values: true. |
sessionId | string (uuid) | Yes | The session id. Store it with your customer record; it is the correlation handle for status reads and webhooks. |
url | string (uri) | No | One-shot hosted-flow URL; the ONLY place the bearer token is ever returned. Absent on an idempotent replay. |
expiresAt | integer (int64) | Yes | Unix time in milliseconds at which the unopened link expires. |
replayed | boolean | No | Present (and true) only when an Idempotency-Key matched an existing session; url is then absent. Allowed values: true. |
hostedOptions | object | No | The effective hosted options stored with the session, including the resolved steps.emailVerification mode (your organisation default applied when the request did not set it). Present only when hosted email verification is enabled for the platform AND the session’s effective mode is not off; absent otherwise (a session that does not use the step gets the pre-feature response). |
hostedOptions.allowedDocumentTypes | array of string | Yes | At least 1 item, at most 4 items, unique items. |
hostedOptions.allowedIssuingCountries | array of string | No | Optional. Narrows the invitation to these issuing countries: each document type your hosted configuration accepts keeps only the issuing countries that are also listed, a document type left with none is dropped, and a mint where no document type remains is refused with 400 invalid_hosted_options (field allowedIssuingCountries, reason no_matching_document_country). Absent means no narrowing. Any ISO 3166-1 alpha-2 code is accepted, including codes your configuration does not offer. At least 1 item, at most 249 items, unique items. |
hostedOptions.allowDeviceUpload | boolean | Yes | |
hostedOptions.steps | object | Yes | |
hostedOptions.steps.documentBack | string | Yes | Allowed values: auto. |
hostedOptions.steps.documentNearFace | boolean | Yes | |
hostedOptions.steps.activeLiveness | boolean | Yes | |
hostedOptions.steps.speechAnalysis | boolean | Yes | |
hostedOptions.steps.ageGenderEstimation | boolean | No | Optional; default false. Adds an advisory age and gender estimate from the selfie frames (biometric, special-category data); the customer gives a separate consent on the page. Setting true is refused with 403 age_gender_estimation_not_enabled (reasonCode age_gender_platform_disabled or age_gender_tenant_acknowledgement_missing) unless the platform has enabled the feature AND your organisation has recorded the matching legal-basis acknowledgement in the Backoffice. It never fails a verification. |
hostedOptions.steps.emailVerification | string | No | Optional (added 2026-10-01; default off when the key is absent). When omitted from a mint request your organisation’s default applies; an explicit value wins in both directions. An explicit optional or required is refused with 400 email_verification_unavailable until the platform has enabled hosted email verification. The mint response echoes the effective mode. Allowed values: off, optional, required. |
domainId | string, nullable | Yes | The domain the invitation was created for, or null when it uses your organisation default. Fixed at mint; the status read returns it too. |
requestId | string | Yes |
Response example (200)
{
"ok": true,
"sessionId": "6d3c3a8e-6b4b-4a8f-8f4e-0a0e6b3d9d21",
"url": "https://verify.noxtica.com/s/kycs.tenant_42.q3JkT0vN8mYb1xW6cE4aZ7uP2sH9dLfR5gKiA0oXyUw",
"expiresAt": 1790765600000,
"domainId": null,
"requestId": "req-test"
}
Example request
curl
curl -X POST "https://collect.noxtica.com/v1/kyc/sessions" \
-H "Authorization: Bearer $NOXTICA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"subjectRef": "cust_4821",
"returnUrl": "https://merchant.example.com/kyc/return",
"purpose": "Open your brokerage account",
"idType": "passport",
"expiresInSeconds": 3600,
"hostedOptions": {
"allowedDocumentTypes": [
"passport",
"national_id"
],
"allowedIssuingCountries": [
"TR",
"GB",
"DE"
],
"allowDeviceUpload": false,
"steps": {
"documentBack": "auto",
"documentNearFace": false,
"activeLiveness": true,
"speechAnalysis": false,
"ageGenderEstimation": false
}
}
}'
Node.js (global fetch, Node 18 or later)
const response = await fetch('https://collect.noxtica.com/v1/kyc/sessions', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.NOXTICA_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
subjectRef: 'cust_4821',
returnUrl: 'https://merchant.example.com/kyc/return',
purpose: 'Open your brokerage account',
idType: 'passport',
expiresInSeconds: 3600,
hostedOptions: {
allowedDocumentTypes: [
'passport',
'national_id',
],
allowedIssuingCountries: [
'TR',
'GB',
'DE',
],
allowDeviceUpload: false,
steps: {
documentBack: 'auto',
documentNearFace: false,
activeLiveness: true,
speechAnalysis: false,
ageGenderEstimation: false,
},
},
}),
});
console.log(response.status, await response.text());
Python (standard library)
import json
import os
import urllib.request
request = urllib.request.Request(
"https://collect.noxtica.com/v1/kyc/sessions",
data=json.dumps({
"subjectRef": "cust_4821",
"returnUrl": "https://merchant.example.com/kyc/return",
"purpose": "Open your brokerage account",
"idType": "passport",
"expiresInSeconds": 3600,
"hostedOptions": {
"allowedDocumentTypes": [
"passport",
"national_id",
],
"allowedIssuingCountries": [
"TR",
"GB",
"DE",
],
"allowDeviceUpload": False,
"steps": {
"documentBack": "auto",
"documentNearFace": False,
"activeLiveness": True,
"speechAnalysis": False,
"ageGenderEstimation": False,
},
},
}).encode(),
headers={
"Authorization": f"Bearer {os.environ['NOXTICA_API_KEY']}",
"Content-Type": "application/json",
},
method="POST",
)
with urllib.request.urlopen(request) as response:
print(response.status, response.read().decode())
Read a hosted session’s current status (tenant backend read)
GET /v1/kyc/sessions/{sessionId}
Server-to-server. The authoritative tenant-side read of a hosted session; use it to
confirm any webhook before you act on it, and to poll when you have no webhook endpoint.
Everything is nested under session, and every timestamp is Unix time in
milliseconds. A session that is past its deadline reads 200 with
status: "expired" (never 410; the 410 answers belong to the customer’s browser
routes). A session id that does not exist, or belongs to another organisation, gets the
same byte-identical 404 as an unknown path.
Status lifecycle.
status | Meaning | kyc.session.completed / kyc.session.cancelled |
|---|---|---|
pending | Minted; the customer has not opened the link. | none |
opened | The customer reached the capture page. | none |
verifying | The customer submitted; the verification is running. | none |
review_pending | The verification finished and awaits a Backoffice review decision. With the identity provider configured today every successful hosted capture lands here. It never expires. | none yet; kyc.session.completed fires later, when a reviewer decides |
completed | A business decision exists: a reviewer approved or overrode the case (or, by design, a machine verified). completed does not mean verified: read verification and review below. | kyc.session.completed with status: completed |
unverified | Finished with a negative outcome and no manual-review code (for example capture attempts exhausted). | kyc.session.completed with status: unverified |
error | A provider or infrastructure fault, or the submit was refused at admission. | kyc.session.completed with status: error |
expired | Past the deadline without a decision (see “Lifetime and expiry” on POST /v1/kyc/sessions). | never |
cancelled_by_user | The customer cancelled on the hosted page (only when cancellation is enabled for your organisation). | kyc.session.cancelled |
review_pending to completed. kyc.session.completed does not fire when a session
lands in review_pending. It fires when a Backoffice reviewer approves or overrides the
case, usually within seconds and at most about 5 minutes after the decision. Until then
the only way to observe review_pending is this read. An override also moves the session
to completed, so completed is not an approval. decision.outcome is derived from
status alone (completed reads approved), so it reads approved after an override
too: do not use it to tell the two apart. Use verification and review.
Verification outcome and review. Once the session has a verificationId (the customer
submitted), session carries two more blocks that state what the verification finally is;
before that neither key is present.
verification.statusis the outcome:verified,unverified,failed,errororpending(the verification has no verdict yet).verification.sourcesays who produced it:pipelineis the automated verdict exactly as recorded,reviewmeans a reviewer’sapprovedecision was applied over it. Onlyapproveever turns the reported status intoverified; anoverrideis reported inreviewbut never upgrades the status.reviewisnullwhile no reviewer has acted, otherwise{state, decision, version, decidedAt}.stateisin_review(escalated, no decision yet),approvedoroverridden; the vocabulary also holdsopenandclosed, which no review action writes today.decisionisapprove,override, ornullwhile the state isin_review.versionis the number of review writes, starting at 1 and increasing by one with every write.decidedAtis Unix ms of the last decision, ornullwhile there is none. No reviewer identity and no reviewer note is ever returned.
reasonCodes keeps the automated reasons after a review: provider_manual_review stays
in the list once a reviewer has approved the case, so it does not mean the case is still
waiting. Read status, verification and review instead.
| Situation | status | verification | review |
|---|---|---|---|
| Waiting for a reviewer | review_pending | unverified / pipeline | null |
| Escalated, undecided | review_pending | unverified / pipeline | state: in_review, decision: null |
| Approved | completed | verified / review | state: approved, decision: approve |
| Overridden | completed | the automated status / pipeline | state: overridden, decision: override |
There is no reject decision in the review vocabulary today, and a rejected end state
cannot be expressed. kyc.verification.completed is sent once, when the automated
verification ends (for a hosted session, unverified with provider_manual_review); it
is never sent again after a review, so do not wait for it: read review.
Review decision and status events. A reviewer’s approve or override is announced by the kyc.review.decided event, for hosted sessions and for verifications you created through the API; for a hosted session that was review_pending, kyc.session.completed follows. An escalation (in_review) sends nothing. Each status a session enters is also announced by its own opt-in event: kyc.session.created (pending), kyc.session.opened, kyc.session.verifying, kyc.session.review_pending, kyc.session.unverified, kyc.session.error and kyc.session.expired; see the webhooks section.
Cancellation. status: cancelled_by_user is reported here, but cancelledAt and
cancelReason are NOT part of this response; they are delivered in the
kyc.session.cancelled webhook. An expiry never sends kyc.session.completed or
kyc.session.cancelled. A cancelled or expired session is never billed as a verification
(the kyc.verification meter is recorded only at submit-time admission); only the
request-level endpoint.collect.request counts.
Email verification result. Only when hosted email verification is enabled for the
platform AND the session’s effective mode is not off or an address is stored, the
session object also carries email (the normalised address the customer entered, or
null), emailVerified (true only after the customer clicked the confirmation link for
that exact address), emailVerifiedAt (Unix ms, or null) and emailVerification (the
effective mode: off, optional or required). Treat email with emailVerified: false
as an unverified, customer-typed value. Otherwise these four keys are absent.
Noxtica Verification ID. Only when the Noxtica Verification ID (NVID) is enabled for
the platform, the session object also carries nvid: the session’s NVID
(NVID-XXXX-XXXX-XXXX-XXXX) or null (none was issued: an NVID is issued when the
submit ends completed or review_pending). The key is absent while the feature is not
enabled. The public status page of an NVID is https://verify.<environment host>/v/<NVID>;
the public lookup route behind it can only be called from the hosted site’s browser
origin, never from a tenant backend.
Check results. The per-check results below are evidence for a reviewer and for your own risk rules. Every number is a value the service uses today and may be retuned without a version bump unless a row says otherwise; model, engine and vendor names are deliberately not published.
- Nothing here rejects a person by itself. Every hosted verification currently ends
verification.status: unverifiedwithreasonCodes: [provider_manual_review]; a reviewer’s approval completes it (verification.source: review). A check result never changesverification.statuson its own. failmeans a confident failure;reviewmeans no confident answer, a person should look. An advisory or unqualified signal can only readreview. Afailon a person who is clearly genuine is a false reject: report it with thesessionId.- While presentation-attack detection (PAD) is advisory,
livenessandselfienever readpass. Their best outcome isreview, soscorecard.overallis at bestreviewfor every session today. This is expected and is not a problem with the person’s capture.
What these readings mean for your integration, what to automate and what not to, and every PAD and liveness reason code are on the page Liveness & PAD policy.
Thresholds are platform-wide. Every threshold named below (the selfie quality limits, the
face-match bands, the liveness and blink limits, the document-authenticity bands and the PAD
operating points) is one value for all customers. No request carries a tenant, profile or
threshold parameter, and neither your organisation default, a domain override nor an
invitation changes a threshold: there is no tenant-level or domain-level threshold setting
today. What you can configure, per organisation, per domain (a domain overrides your
organisation default field by field) and per invitation, is which steps and documents run
and which identity fields must be read: in the hosted verification configuration
(GET /v1/kyc/config) documents[] (idType, issuingCountries, requiredIdentityFields),
requireSelfie, requireVoice, allowDeviceUpload and emailVerification; on an
invitation (POST /v1/kyc/sessions) hostedOptions (allowedDocumentTypes,
allowedIssuingCountries and steps, for example steps.activeLiveness). None of these
changes how a check is judged. The verdict of a hosted verification (unverified with
provider_manual_review until a reviewer approves) is not configurable either.
Where the results appear: session.scorecard.<step> for document_front, document_back,
selfie, liveness, voice, cross_validation and overall
({status, score 0..1, checks[{name, pass, score}], reasonCodes[]}), and
session.stages.<stage> ({status, score, reasonCodes[]}). The identity read
(GET /v1/kyc/verifications/{verificationId}/identity, scope read:identity_details)
returns the verdicts only, as checks.documentAuthenticity, checks.faceMatch,
checks.liveness and checks.selfie, without scores.
checks.* on the identity read | Built from | Scorecard step |
|---|---|---|
documentAuthenticity | the document authenticity analysis | document_front |
faceMatch | document portrait against selfie (and the liveness frames, the worse of the two) | selfie |
liveness | the active-liveness challenge (five gestures) | liveness |
selfie | selfie quality and PAD, the worse of the two | selfie |
Reading a scorecard reason code.
| Pattern | Meaning |
|---|---|
<check>_failed | that check (a name from the tables below) returned a confident failure. |
<check>_inconclusive | the analysis could not judge that check (for example a blink that could not be measured). Never a failure. |
<capability>_pass, _fail, _inconclusive | the analysis’s own overall verdict, in public capability names: document_authenticity, face_match, active_liveness, presentation_attack_detection, face_quality, document_recognition, document_text_reading, mrz_reading, barcode_reading, document_consistency. For an advisory analysis _fail is that analysis’s verdict only; the step still reads review. |
awaiting_capture, awaiting_evaluation, not_required, not_evaluated | the step has no result yet, is not required, or the analysis never produced one (gated or unavailable): review. |
not_applicable | the document type has nothing to measure for this stage (a driving licence has no machine-readable zone): the stage reads skipped. |
additional_check_failed | a check outside the public vocabulary failed; no detail is exposed. |
Document authenticity (checks.documentAuthenticity, step document_front). Advisory:
the analysis is an unqualified classical image analysis whose own decision is always manual
review. A failed check reads review at most; only an overall pass reads pass. It is
never used to reject.
| Check | What it measures | Failing means | Weight |
|---|---|---|---|
card_geometry | The document’s own outline has the shape of a card or booklet page. | The outline’s long to short side ratio differs by more than 0.08 (relative to the reference ratio) from a bank-card (ID-1) card (1.586) or a passport page (1.389). A card smaller than 300 px on its short side, or not found, is inconclusive, never fail. | hard gate |
moire_recapture | Screen-photographed or re-photographed document patterns. | A recapture-like pattern was found. | hard gate |
layout_conformance | The expected elements and photo region of a card of this kind are present. | Too few elements were found (blank, cropped or wrong-side image). | scored |
mrz_structure | Structure of the machine-readable-zone band (back sides only). | The band is malformed. | scored |
print_consistency | The print noise pattern is uniform across the card. | Locally inconsistent print texture (possible overlay or edit). | scored |
copy_move | Duplicated regions inside the image. | Repeated image regions. Soft: never fails alone. | scored |
ela | Compression-history consistency (JPEG sources). | Locally different compression history. | scored |
hologram_presence | A light-variable (hologram-like) region is visible. | Not visible at this capture angle. Soft: never fails alone. | scored |
image_similarity_replay | The image is not a known replayed or duplicate capture. | Matches a known replay (only when a reference catalogue is configured). | scored |
The analysis is fail when a hard-gate check fails or the weighted score is below the fail
band; pass only when every check passes, the score is in the pass band and card_geometry
was actually measured; everything else is inconclusive. The numeric bands and the forensic
thresholds of copy_move, ela, moire_recapture, print_consistency and
hologram_presence are not published. The advisory portrait-substitution observation
(doc_auth_portrait_substitution) is computed after the verdict and is not part of the
score: it appears in checks[] when it did not pass, adds no _failed code and never
lowers a pass. card_geometry_failed on a real card almost always
means the document was not found or was held at a strong angle: ask for a flat, well-lit
capture with the whole card in view. A document other than the supported card layout (for
example a driving licence) is measured against the card profile, so its layout and hologram
results are weaker evidence. session.scorecard.document_front is wider than
checks.documentAuthenticity: it also folds in the document recognition and text reading
rows of the front. The text reading row is always inconclusive today
(document_text_reading_inconclusive), so document_front reads review at best, even
when checks.documentAuthenticity reads pass. Treat review as “a person should look”.
Liveness (checks.liveness, step liveness, stage active_liveness). Advisory evidence
plus a capture gate in the page: the hosted page does not accept a gesture until the person
has moved by the capture thresholds below. The server then re-checks the captured frames
independently, and the browser’s own measurements can only make a result worse, never rescue
a failed server check. Five gestures are captured in the order turn_up, turn_down,
turn_left, turn_right, blink. Checks in the scorecard: active_liveness (the overall
result), turn_left, turn_right, turn_up, turn_down, blink and pad.
| Check | pass | review (<name>_inconclusive) | fail (<name>_failed) |
|---|---|---|---|
turn_left, turn_right, turn_up, turn_down | The head moved clearly in the requested direction, measured against the person’s own starting pose. | No starting pose or head-pose measurement could be formed, or the browser’s measurements were unavailable. | The head did not move enough, moved the wrong way, or the peak frame the browser claimed is not where the server saw it. A frame without exactly one face fails the gesture. |
blink | A complete open-closed-open eyelid cycle lasting 50 to 1500 ms was observed. | The blink could not be measured, or its duration was outside 50 to 1500 ms. | No credible eyelid closure. |
pad | not applicable (advisory) | always (pad_unvalidated_model) | never while advisory |
active_liveness | All five gestures pass and pad passes. Not reachable while PAD is advisory. | Any gesture is review, or PAD is advisory. | Any gesture failed, or there was no usable face. |
Publishable capture thresholds (browser, per gesture, relative to the start pose): turn left or right at least 6 degrees; look up or down at least 4.5 degrees; at most 42 degrees sideways or 38 degrees up and down is accepted; blink between 50 and 1500 ms. The server-side gate, its noise margins and the PAD operating points are not published.
| Liveness reason code | Meaning | Status it causes |
|---|---|---|
challenge_direction_inconsistent | Head movement did not match the requested direction. It is present when the gesture failed, or when the gesture reads review and no more specific cause from this table applies; a review that names its cause (for example client_metrics_unavailable) does not carry it. | gesture fail (or review) |
landmark_peak_mismatch | The browser’s reported peak frame does not match the server’s measurement. For up and down it is judged only when the head-pose measurement was available; without it the gesture reads review with pose_landmarks_unavailable, not fail. | gesture fail (turn left and right, and up and down with a head-pose measurement) |
no_face_detected, multiple_faces | A frame had no face or more than one face. | gesture fail |
face_track_lost | The face was lost between frames. | review or fail |
client_metrics_unavailable, client_landmark_low_confidence | The browser did not provide its own measurements, or with low confidence. A blink is judged by the server on its own frames and does not carry them. | caps at review |
neutral_reference_unavailable | No usable starting pose (fewer than three single-face frames in the blink burst). | review, never pass |
pose_landmarks_unavailable | The head-pose measurement was unavailable for a frame; up and down cannot be judged. | pitch gesture review |
blink_inconsistent | The blink could not be confirmed. When a blink-like cycle existed it comes with one of the three codes below. | blink review or fail |
blink_closed_plateau | A real eye closing and reopening was found, but the closed frame sits next to another almost identical near-closed frame, so a frozen or painted eyelid cannot be told apart from a very long closure. | blink review |
blink_scene_changed | The blink was found, but the nose region, which serves as a control for lighting and head movement, changed between the open frame and the reopened frame. | blink review |
blink_cycle_partial | Only part of the evidence for a blink was present (the eyelid shape or the eye-region change, not both), or the cycle was incomplete. | blink review |
blink_duration_out_of_range | The blink cycle lasted outside 50 to 1500 ms. | blink review |
pad_unvalidated_model | The PAD result is advisory by design. | pad review |
pad_unavailable, pad_incomplete, pad_face_not_detected | PAD could not run on every frame. | pad review |
pad_review, pad_rejected | Reserved for when PAD becomes decision-affecting; not emitted today. | not applicable |
passive_temporal_evidence_unavailable, passive_temporal_inconsistent, motion_implausible, frame_duplicate_suspected | Passive-hold variant only: not enough or implausible motion across the three frames, or repeated frames. passive_temporal_inconsistent is present when the result is fail, or when a review has no more specific cause. | review (inconsistent: fail) |
evidence_missing, evidence_unavailable, evidence_wrong_content_type, invalid_evidence_package, evidence_lookup_failed, transport_error, unexpected_status, malformed_response, malformed_refusal_body, adapter_not_configured, adapter_config_invalid, not_configured, weights_ungated | The liveness check could not run at all (capture package missing or invalid, or the verification service unavailable). Appears as the stage reason of stages.active_liveness. | review, never fail |
turn_*_failed on a genuine person usually means the head barely moved, a very small face
or poor light: ask the person to turn clearly past the on-screen mark. blink_inconclusive
is common and means “not measured”, not “no blink”; together with blink_closed_plateau,
blink_scene_changed or blink_cycle_partial it means a blink-like movement was seen but
not confirmed, so a person should look at it. A server result stored for the session counts
as proof that the challenge was captured: the liveness step is not pending once that result
exists, even when the browser never reported a completed gesture.
Selfie quality (checks.selfie, step selfie, stage selfie_quality). The hard checks
are flow-affecting (the hosted page asks for a retake); the soft checks are advisory. Every check
after face_count needs exactly one detected face. A failed check is reported as
<check>_failed, with the check name from the first column, in
scorecard.selfie.reasonCodes and session.stages.selfie_quality.reasonCodes; the overall
result of the quality analysis adds face_quality_fail or face_quality_inconclusive. The
quality analysis also has internal issue names (for example eyes_not_open,
image_too_blurry, exposure_out_of_range); they are not reason codes of the scorecard and
are not returned in these lists.
| Check | Passes when | Reason code when it fails | Kind |
|---|---|---|---|
face_count | exactly one face | face_count_failed | hard (fail) |
face_area_ratio | the face box covers 0.03 to 0.90 of the image area | face_area_ratio_failed | hard (fail) |
face_centered | the face centre is within 0.18 of the width and 0.20 of the height from the image centre | face_centered_failed | hard (fail) |
pose_yaw, pose_pitch | the landmark pose ratio of each axis is within 0.25 of frontal (a unitless ratio of facial landmark positions, not an angle) | pose_yaw_failed, pose_pitch_failed | soft (review) |
eyes_open | eye-region texture of at least 8 (luma standard deviation) | eyes_open_failed | hard (fail) |
not_occluded | nose and mouth-corner texture of at least 5 | not_occluded_failed | hard (fail) |
sharpness | Laplacian variance of the face box of at least 40 | sharpness_failed | soft (review) |
exposure | mean face luma between 35 and 220 (of 255) and at most 0.15 of the pixels clipped | exposure_failed | soft (review) |
The selfie analysis is fail (face_quality_fail) when any hard check fails,
inconclusive (face_quality_inconclusive, reads review) when only soft checks fail or
when every check passed but their mean score is below 0.6, otherwise pass. In the last
case no <check>_failed code is present. The check scores (0..1) are in
scorecard.selfie.checks. The thresholds are calibrated for typical webcam selfies around
640x480; sharpness is a pixel-scale measure and reads lower on higher-resolution captures
of the same sharp face (a soft check: it can only produce review). exposure covers both
too dark and too bright faces. A head that is clearly turned away can also make the hosted
page ask the person to retake the selfie, within the retake budget of the session.
checks.selfie also folds in PAD, so while PAD is advisory it is at best review even when
every check above passes; it reads fail only when a hard selfie check fails.
Presentation-attack detection (PAD). Advisory, never decision-affecting. No operating
point has been qualified on real replay and print cohorts, so every PAD result is reported
as review with the reason pad_unvalidated_model and the scorecard code
presentation_attack_detection_inconclusive. The live-score is recorded for reviewers but
is not interpreted as live or spoof; its thresholds and model details are not published.
Promoting PAD to a decision requires a new qualification and a documented contract change;
the policy page linked above states what changes for your integration and what does not.
When the PAD analysis does not find a face in the selfie although the face comparison did,
the verification does not end in an error: document, machine-readable-zone and barcode
reading continue, the selfie and liveness steps read review with the same two codes,
and the session ends like every other hosted verification (unverified with
provider_manual_review).
Face match (checks.faceMatch). Reviewer evidence: a low score can force manual review,
it never rejects.
| Check | Meaning |
|---|---|
similarity_accept_band | Primary comparison of the document portrait with the selfie: pass at a similarity of 0.42 or higher, review from 0.30 up to 0.42, fail below 0.30. |
face_match_calibrated_score | The fused, calibrated score (0..1), advisory. |
cross_check_1, cross_check_2, … | Independent second-opinion comparisons (anonymous ordinal ids). Each one agrees only when its own calibrated score reaches its own accept bound; one that does not agree reads cross_check_N_failed and makes checks.faceMatch and the selfie step review even when similarity_accept_band passes. A second opinion never makes either of them fail: a fail comes only from the primary comparison. |
face_match_borderline | The comparison sat near a band edge. |
A missing result reads null on the identity read and not_evaluated on the scorecard: no
face was found in one of the images, or the analysis was unavailable. Different lighting,
angle or age lowers the score: ask for a frontal, well-lit selfie.
Machine-readable zone and cross-check (stage mrz_crosscheck, steps document_back and
cross_validation). A driving licence has no machine-readable zone. For a session whose
document type is driving_license, session.stages.mrz_crosscheck reads
{status: skipped, score: null, reasonCodes: [not_applicable]} and the machine-readable-zone
rows are left out of document_back and cross_validation, so the missing zone is not a
failure; a zone that was read from that document anyway is shown as usual. For every other
document type, a zone that was not found, or that cannot be compared with the printed text,
reads review (a measurement gap) and never fail. fail is kept for a decoded zone whose
check digits are wrong or whose content contradicts the printed text. A barcode that was not
found or could not be decoded on the back is a measurement gap as well: document_back
reads review with barcode_reading_inconclusive, never fail; fail is kept for a
barcode that was decoded and is invalid. On a zone that was read, the structure checks that
only report which fields the band carries (for example mrz_personal_number_present, a
field that a passport or a foreign identity card does not have) are informational: they stay
in checks[] and never turn document_back into fail unless the zone itself failed. A
driving_license whose cross_validation compared nothing reads skipped with
not_applicable there instead of review. An auxiliary check of document_front that
compares a secondary document classifier with the primary document type reads review at
most and never turns the step into fail; the two label sets are compared by document type
and side, so a document named as the same type and side by both agrees.
How steps roll up (scorecard.overall). fail dominates review, which dominates
pass. skipped steps are ignored; a pending step turns an otherwise pass roll-up into
pending but never hides a fail or a review.
The document recognition and reading codes that can appear next to these (document_type_uncertain,
document_type_not_identified, document_front_no_portrait, document_back_has_portrait,
mrz_not_detected, mrz_invalid, mrz_checksum_invalid, front_field_missing,
front_back_field_mismatch) concern reading the document. Two informational codes concern the
selection the person made: document_type_mismatch_selection (the classifier contradicts the
selected document type) and issuing_country_mismatch_selection (the document shows another
country than the selected one); see the extraction block of
GET /v1/kyc/verifications/{verificationId}/identity.
Rate limit. Shares the per-key limit (30 requests per 60 seconds) with the mint call, after the per-source-IP limit that applies to every request.
Authentication: Bearer token (ApiKeyAuth), scope write:identity.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
sessionId | path | string | Yes | The sessionId returned by the mint call. Pattern ^[A-Za-z0-9-]{1,64}$. |
Responses
| Status | Meaning | Error codes | Content |
|---|---|---|---|
200 | The session’s current status. | application/json KycTenantSessionEnvelope | |
400 | Invalid sessionId — the path segment is not valid percent-encoding. | Invalid sessionId | application/json KycSessionError |
401 | The Authorization: Bearer sk_... header is missing or malformed, or the key is unknown or revoked. The body is deliberately identical for every one of those causes. | invalid_api_key | application/json KycSessionError |
403 | The presented key lacks the required scope. | API key does not have write:identity scope | application/json |
404 | The byte-identical “V1 endpoint not found” response returned when the route’s module is not enabled for your organisation — indistinguishable from a route that does not exist (anti-enumeration by design; do not treat this as proof the path is wrong). | V1 endpoint not found | application/json |
429 | Rate limit exceeded. Wait Retry-After seconds (also retryAfter in the body) before retrying; the response carries no other rate-limit headers. | Rate limit exceeded | application/json |
500 | API-key authentication is not configured on this deployment (server_misconfigured). Retrying does not help; contact support with the requestId. | server_misconfigured | application/json KycSessionError |
503 | Retryable (Retry-After: 2). tenant_module_gate_unavailable, or invalid_request with a reason (pg_unavailable or backend_unavailable). | invalid_request | application/json KycSessionError |
503 | Noxtica could not read the key’s record. This is a temporary fault on Noxtica’s side and says nothing about your key. Retry after the Retry-After delay; do not treat it as a revoked key. | api_key_auth_unavailable | application/json |
Response headers
| Status | Header | Type | Description |
|---|---|---|---|
429 | Retry-After | integer | Seconds until the bucket allows another request. |
503 | Retry-After | integer | Seconds to wait before retrying. |
Response fields (200, application/json)
| Field | Type | Always returned | Description |
|---|---|---|---|
ok | boolean | Yes | Allowed values: true. |
session | object (KycTenantSessionStatus) | Yes | The tenant-backend view of a hosted session. Every timestamp is Unix time in milliseconds. Not exposed on this route: the session URL or token, evidence bytes or URLs, the stored hostedOptions, cancelledAt and cancelReason (those are delivered only in the kyc.session.cancelled webhook), model provenance, and the address a person may have given to receive their Noxtica Verification ID. |
session.id | string (uuid) | Yes | The session id returned by the mint call. |
session.domainId | string, nullable | Yes | The domain the invitation was created for (see the domainId, siteKey and Origin rules of the mint call), or null when it uses your organisation default. Fixed at mint. |
session.status | string | Yes | The hosted session lifecycle status. pending — minted, link not opened yet. opened — the customer reached the capture page. verifying — the customer submitted and the verification is running. review_pending — the verification finished and awaits a Backoffice review decision (today every successful hosted capture lands here; it never expires, and kyc.session.completed has NOT fired yet). completed — a business decision exists (a reviewer’s approval or override: read verification and review on the status read for which). unverified — finished with a negative outcome and no manual-review code. error — a provider or infrastructure fault, or the submit was refused at admission. expired — the effective deadline passed without a decision (the status read reports it with 200; kyc.session.completed never fires for it). cancelled_by_user — the customer cancelled on the hosted page. Allowed values: pending, opened, verifying, review_pending, completed, unverified, error, expired, cancelled_by_user. |
session.idType | string | Yes | The document type hint given at mint (default national_id). |
session.subjectId | string | Yes | Your own subjectRef from the mint call, returned unchanged. It is never sent in webhooks. |
session.captureStep | string | Yes | The furthest capture step the customer has reached. Allowed values: consent, id_front, id_back, selfie, document_near_face, liveness, voice, review, submit. |
session.createdAt | integer (int64) | Yes | |
session.openedAt | integer (int64), nullable | Yes | When the customer’s first recorded action on the hosted page happened (the session left pending); null until then. |
session.expiresAt | integer (int64) | Yes | The effective deadline. At mint it is now + expiresInSeconds (24 hours by default, 5 minutes to 7 days); once the customer opens the session it is tightened to min(expiresAt, openedAt + 1800000) (the 30-minute capture window). It only moves earlier and is never extended. A session in review_pending does not expire; a session past this instant that was never submitted reads status: expired. |
session.completedAt | integer (int64), nullable | Yes | When the decision was made (pipeline outcome, or the reviewer’s decision for a session that was review_pending); null before. |
session.verificationId | string, nullable | Yes | The verification the submit ran (also data.verificationId of kyc.verification.completed); null before the customer submits. For a hosted session it is hosted: followed by 64 lowercase hexadecimal characters (71 characters in all); see KycVerificationId for the formats and the 128-character limit. At most 128 characters. |
session.reasonCodes | array of string | Yes | Closed reason-code catalog. The reasons of the automated verification; a review does not rewrite them, so provider_manual_review stays after a reviewer approved the case. Read verification and review for the current outcome. |
session.purpose | string, nullable | Yes | The mint-time purpose. |
session.returnUrl | string, nullable | Yes | The mint-time returnUrl. |
session.selectedIdType | string, nullable | Yes | The document type the customer chose on the page: passport, national_id, driving_license or residence_permit; null until the customer chooses. |
session.selectedIssuingCountry | string, nullable | Yes | The issuing country the customer chose, as an ISO 3166-1 alpha-2 code (one of the CountryCode values); null until the customer chooses. Pattern ^[A-Z]{2}$. |
session.consentVersion | string, nullable | Yes | Version of the privacy notice the customer acknowledged. |
session.consentAcceptedAt | integer (int64), nullable | Yes | |
session.speechProfilingConsentAcceptedAt | integer (int64), nullable | Yes | |
session.ageGenderConsentAcceptedAt | integer (int64), nullable | Yes | |
session.evidence | object (KycSessionEvidenceRefs) | Yes | Opaque object ids of the finalized (usable) uploads, by role. Never a URL or media bytes. A role appears only once its upload is finalized. |
session.evidence.front | string | No | |
session.evidence.back | string | No | |
session.evidence.selfie | string | No | |
session.evidence.voice | string | No | |
session.evidence.documentNearFace | string | No | |
session.decision | object (KycSessionDecision) | No | The decision block, present once the pipeline or a review decision has produced one. outcome maps from the session status alone: completed to approved, unverified to rejected, review_pending to review_pending, error to error. It is not the review decision: a session a reviewer overrode is completed too and reads approved here. Use verification and review to tell an approval from an override. |
session.decision.outcome | string | Yes | Allowed values: approved, rejected, review_pending, error. |
session.decision.reasonCodes | array of string | Yes | |
session.decision.decidedAt | integer (int64) | Yes | Unix ms. |
session.verification | object | No | The reported outcome of the verification. Present only once the session has a verificationId; absent before that. |
session.verification.status | string | Yes | The outcome of a verification. verified, unverified, failed and error are the verdict words of the automated verification; pending means the verification has no verdict yet. A reviewer’s approve decision turns the reported status into verified (see KycVerificationStatusSource); no other decision changes it. Allowed values: verified, unverified, failed, error, pending. |
session.verification.source | string | Yes | Who produced the reported status. pipeline — the automated verdict exactly as it was recorded. review — a reviewer’s approve decision was applied over it. An override decision never produces review. Allowed values: pipeline, review. |
session.review | one of KycTenantReview or any, nullable | No | The reviewer’s work on the verification: null while no reviewer has acted. Present (as null or an object) exactly when verification is; absent before the session has a verificationId. |
session.review{KycTenantReview} | object (KycTenantReview) | No | What a reviewer did with a verification. No reviewer identity, no reviewer note and no reviewer reason codes are returned. |
session.review{KycTenantReview}.state | string | Yes | State of a reviewer’s work on a verification. in_review — a reviewer escalated the case without deciding (no decision yet). approved — a reviewer approved it. overridden — a reviewer replaced the automated verdict. open and closed belong to the vocabulary but no Backoffice review action writes them today; handle an unknown or unexpected value as “not decided”. Allowed values: open, in_review, approved, overridden, closed. |
session.review{KycTenantReview}.decision | one of KycReviewDecision or any, nullable | Yes | The reviewer’s decision, or null while the state is in_review. |
session.review{KycTenantReview}.version | integer | Yes | Number of review writes for this verification, starting at 1 and increasing by one with every write (an escalation counts). Minimum 1. |
session.review{KycTenantReview}.decidedAt | integer (int64), nullable | Yes | Unix ms of the last review write that carried a decision, or null while there is none. |
session.crossValidation | object | No | The OCR / MRZ / barcode cross-validation advisory block, when the pipeline attached one. Absent for sessions without back-of-document evidence. Verdict-neutral. |
session.stages | object (KycTenantStages) | Yes | Per-stage results keyed by stage name. A stage that was not requested or has not run reads status: skipped or pending with a reason code, never a fabricated pass. |
session.stages.scene_inventory | object (KycTenantStageResult) | No | One analysis stage’s tenant-facing result. Stages that carry extra advisory detail (speech_profile, age_gender, scene_inventory, document_type_id) add typed members next to these three; model and engine provenance is never exposed. |
session.stages.scene_inventory.status | string | Yes | Allowed values: pass, review, fail, skipped, pending. |
session.stages.scene_inventory.score | number, nullable | Yes | Range 0 to 1, or null when the stage produced no score. |
session.stages.scene_inventory.reasonCodes | array of string | Yes | |
session.stages.document_type_id | object (KycTenantStageResult) | No | One analysis stage’s tenant-facing result. Stages that carry extra advisory detail (speech_profile, age_gender, scene_inventory, document_type_id) add typed members next to these three; model and engine provenance is never exposed. |
session.stages.document_type_id.status | string | Yes | Allowed values: pass, review, fail, skipped, pending. |
session.stages.document_type_id.score | number, nullable | Yes | Range 0 to 1, or null when the stage produced no score. |
session.stages.document_type_id.reasonCodes | array of string | Yes | |
session.stages.ocr_back | object (KycTenantStageResult) | No | One analysis stage’s tenant-facing result. Stages that carry extra advisory detail (speech_profile, age_gender, scene_inventory, document_type_id) add typed members next to these three; model and engine provenance is never exposed. |
session.stages.ocr_back.status | string | Yes | Allowed values: pass, review, fail, skipped, pending. |
session.stages.ocr_back.score | number, nullable | Yes | Range 0 to 1, or null when the stage produced no score. |
session.stages.ocr_back.reasonCodes | array of string | Yes | |
session.stages.mrz_crosscheck | object (KycTenantStageResult) | No | One analysis stage’s tenant-facing result. Stages that carry extra advisory detail (speech_profile, age_gender, scene_inventory, document_type_id) add typed members next to these three; model and engine provenance is never exposed. |
session.stages.mrz_crosscheck.status | string | Yes | Allowed values: pass, review, fail, skipped, pending. |
session.stages.mrz_crosscheck.score | number, nullable | Yes | Range 0 to 1, or null when the stage produced no score. |
session.stages.mrz_crosscheck.reasonCodes | array of string | Yes | |
session.stages.selfie_quality | object (KycTenantStageResult) | No | One analysis stage’s tenant-facing result. Stages that carry extra advisory detail (speech_profile, age_gender, scene_inventory, document_type_id) add typed members next to these three; model and engine provenance is never exposed. |
session.stages.selfie_quality.status | string | Yes | Allowed values: pass, review, fail, skipped, pending. |
session.stages.selfie_quality.score | number, nullable | Yes | Range 0 to 1, or null when the stage produced no score. |
session.stages.selfie_quality.reasonCodes | array of string | Yes | |
session.stages.document_near_face | object (KycTenantStageResult) | No | One analysis stage’s tenant-facing result. Stages that carry extra advisory detail (speech_profile, age_gender, scene_inventory, document_type_id) add typed members next to these three; model and engine provenance is never exposed. |
session.stages.document_near_face.status | string | Yes | Allowed values: pass, review, fail, skipped, pending. |
session.stages.document_near_face.score | number, nullable | Yes | Range 0 to 1, or null when the stage produced no score. |
session.stages.document_near_face.reasonCodes | array of string | Yes | |
session.stages.active_liveness | object (KycTenantStageResult) | No | One analysis stage’s tenant-facing result. Stages that carry extra advisory detail (speech_profile, age_gender, scene_inventory, document_type_id) add typed members next to these three; model and engine provenance is never exposed. |
session.stages.active_liveness.status | string | Yes | Allowed values: pass, review, fail, skipped, pending. |
session.stages.active_liveness.score | number, nullable | Yes | Range 0 to 1, or null when the stage produced no score. |
session.stages.active_liveness.reasonCodes | array of string | Yes | |
session.stages.speech_profile | object (KycTenantStageResult) | No | One analysis stage’s tenant-facing result. Stages that carry extra advisory detail (speech_profile, age_gender, scene_inventory, document_type_id) add typed members next to these three; model and engine provenance is never exposed. |
session.stages.speech_profile.status | string | Yes | Allowed values: pass, review, fail, skipped, pending. |
session.stages.speech_profile.score | number, nullable | Yes | Range 0 to 1, or null when the stage produced no score. |
session.stages.speech_profile.reasonCodes | array of string | Yes | |
session.stages.speech_analysis | object (KycTenantStageResult) | No | One analysis stage’s tenant-facing result. Stages that carry extra advisory detail (speech_profile, age_gender, scene_inventory, document_type_id) add typed members next to these three; model and engine provenance is never exposed. |
session.stages.speech_analysis.status | string | Yes | Allowed values: pass, review, fail, skipped, pending. |
session.stages.speech_analysis.score | number, nullable | Yes | Range 0 to 1, or null when the stage produced no score. |
session.stages.speech_analysis.reasonCodes | array of string | Yes | |
session.stages.age_gender | object (KycTenantStageResult) | No | One analysis stage’s tenant-facing result. Stages that carry extra advisory detail (speech_profile, age_gender, scene_inventory, document_type_id) add typed members next to these three; model and engine provenance is never exposed. |
session.stages.age_gender.status | string | Yes | Allowed values: pass, review, fail, skipped, pending. |
session.stages.age_gender.score | number, nullable | Yes | Range 0 to 1, or null when the stage produced no score. |
session.stages.age_gender.reasonCodes | array of string | Yes | |
session.scorecard | object (KycTenantScorecard) | Yes | The numeric per-step summary. A non-authoritative roll-up of what the pipeline already decided: never an approval by itself. overall is the roll-up of the other six. |
session.scorecard.document_front | object (KycScorecardEntry) | No | |
session.scorecard.document_front.status | string | Yes | Allowed values: pass, review, fail, skipped, pending. |
session.scorecard.document_front.score | number | Yes | Range 0 to 1; 0 for pending and skipped (no evaluation occurred). |
session.scorecard.document_front.checks | array of object | Yes | |
session.scorecard.document_front.checks[] | object | No | |
session.scorecard.document_front.checks[].name | string | Yes | |
session.scorecard.document_front.checks[].pass | boolean | Yes | |
session.scorecard.document_front.checks[].score | number, nullable | No | |
session.scorecard.document_front.reasonCodes | array of string | Yes | |
session.scorecard.document_back | object (KycScorecardEntry) | No | |
session.scorecard.document_back.status | string | Yes | Allowed values: pass, review, fail, skipped, pending. |
session.scorecard.document_back.score | number | Yes | Range 0 to 1; 0 for pending and skipped (no evaluation occurred). |
session.scorecard.document_back.checks | array of object | Yes | |
session.scorecard.document_back.checks[] | object | No | |
session.scorecard.document_back.checks[].name | string | Yes | |
session.scorecard.document_back.checks[].pass | boolean | Yes | |
session.scorecard.document_back.checks[].score | number, nullable | No | |
session.scorecard.document_back.reasonCodes | array of string | Yes | |
session.scorecard.selfie | object (KycScorecardEntry) | No | |
session.scorecard.selfie.status | string | Yes | Allowed values: pass, review, fail, skipped, pending. |
session.scorecard.selfie.score | number | Yes | Range 0 to 1; 0 for pending and skipped (no evaluation occurred). |
session.scorecard.selfie.checks | array of object | Yes | |
session.scorecard.selfie.checks[] | object | No | |
session.scorecard.selfie.checks[].name | string | Yes | |
session.scorecard.selfie.checks[].pass | boolean | Yes | |
session.scorecard.selfie.checks[].score | number, nullable | No | |
session.scorecard.selfie.reasonCodes | array of string | Yes | |
session.scorecard.liveness | object (KycScorecardEntry) | No | |
session.scorecard.liveness.status | string | Yes | Allowed values: pass, review, fail, skipped, pending. |
session.scorecard.liveness.score | number | Yes | Range 0 to 1; 0 for pending and skipped (no evaluation occurred). |
session.scorecard.liveness.checks | array of object | Yes | |
session.scorecard.liveness.checks[] | object | No | |
session.scorecard.liveness.checks[].name | string | Yes | |
session.scorecard.liveness.checks[].pass | boolean | Yes | |
session.scorecard.liveness.checks[].score | number, nullable | No | |
session.scorecard.liveness.reasonCodes | array of string | Yes | |
session.scorecard.voice | object (KycScorecardEntry) | No | |
session.scorecard.voice.status | string | Yes | Allowed values: pass, review, fail, skipped, pending. |
session.scorecard.voice.score | number | Yes | Range 0 to 1; 0 for pending and skipped (no evaluation occurred). |
session.scorecard.voice.checks | array of object | Yes | |
session.scorecard.voice.checks[] | object | No | |
session.scorecard.voice.checks[].name | string | Yes | |
session.scorecard.voice.checks[].pass | boolean | Yes | |
session.scorecard.voice.checks[].score | number, nullable | No | |
session.scorecard.voice.reasonCodes | array of string | Yes | |
session.scorecard.cross_validation | object (KycScorecardEntry) | No | |
session.scorecard.cross_validation.status | string | Yes | Allowed values: pass, review, fail, skipped, pending. |
session.scorecard.cross_validation.score | number | Yes | Range 0 to 1; 0 for pending and skipped (no evaluation occurred). |
session.scorecard.cross_validation.checks | array of object | Yes | |
session.scorecard.cross_validation.checks[] | object | No | |
session.scorecard.cross_validation.checks[].name | string | Yes | |
session.scorecard.cross_validation.checks[].pass | boolean | Yes | |
session.scorecard.cross_validation.checks[].score | number, nullable | No | |
session.scorecard.cross_validation.reasonCodes | array of string | Yes | |
session.scorecard.overall | object (KycScorecardEntry) | No | |
session.scorecard.overall.status | string | Yes | Allowed values: pass, review, fail, skipped, pending. |
session.scorecard.overall.score | number | Yes | Range 0 to 1; 0 for pending and skipped (no evaluation occurred). |
session.scorecard.overall.checks | array of object | Yes | |
session.scorecard.overall.checks[] | object | No | |
session.scorecard.overall.checks[].name | string | Yes | |
session.scorecard.overall.checks[].pass | boolean | Yes | |
session.scorecard.overall.checks[].score | number, nullable | No | |
session.scorecard.overall.reasonCodes | array of string | Yes | |
session.email | string, nullable | No | Present only when hosted email verification is enabled for your organisation AND (the session’s effective emailVerification mode is not off OR an address is stored). The normalised address the customer entered, or null. It is the customer’s own typed value until emailVerified is true. |
session.emailVerified | boolean | No | Present under the same condition as email. true only after the customer clicked the confirmation link for that exact address. |
session.emailVerifiedAt | integer (int64), nullable | No | Present under the same condition as email. When the confirmation link was clicked, or null. |
session.emailVerification | string | No | Hosted email-verification mode. off — no email step. optional — the customer is offered the step and may skip it. required — the customer must verify an address before the session can be submitted (409 email_verification_required until then). The mode is resolved once at mint and fixed for the session. On the tenant-facing GET /v1/kyc/sessions/{sessionId} this key is the effective mode string; the hosted page’s own session routes carry the customer-facing state object KycEmailVerificationState under the same key. Allowed values: off, optional, required. |
session.nvid | string, nullable | No | Present only when the Noxtica Verification ID is enabled for your organisation. The session’s NVID (NVID-XXXX-XXXX-XXXX-XXXX), or null when none was issued (an NVID is issued when the submit ends completed or review_pending). The public status of an NVID is read from GET /v1/kyc/verification-status, which only the hosted verification site’s browser origin may call; a tenant backend takes the NVID from this field or from data.nvid of kyc.session.completed. |
requestId | string | Yes |
Response example (200)
{
"ok": true,
"session": {
"id": "6d3c3a8e-6b4b-4a8f-8f4e-0a0e6b3d9d21",
"domainId": null,
"status": "review_pending",
"idType": "passport",
"subjectId": "cust_4821",
"captureStep": "submit",
"createdAt": 1790762000000,
"openedAt": 1790762100000,
"expiresAt": 1790763900000,
"completedAt": 1790762445000,
"verificationId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"reasonCodes": [
"provider_manual_review"
],
"purpose": "Open your brokerage account",
"returnUrl": "https://merchant.example.com/kyc/return",
"selectedIdType": "passport",
"selectedIssuingCountry": "GB",
"consentVersion": "2026-09-01",
"consentAcceptedAt": 1790762110000,
"speechProfilingConsentAcceptedAt": null,
"ageGenderConsentAcceptedAt": null,
"evidence": {
"front": "obj_9c1e4a",
"selfie": "obj_5b7d02"
},
"decision": {
"outcome": "review_pending",
"reasonCodes": [
"provider_manual_review"
],
"decidedAt": 1790762445000
},
"verification": {
"status": "unverified",
"source": "pipeline"
},
"review": null,
"stages": {
"scene_inventory": {
"status": "pass",
"score": 0.97,
"reasonCodes": []
},
"document_type_id": {
"status": "pass",
"score": 0.94,
"reasonCodes": []
},
"ocr_back": {
"status": "skipped",
"score": null,
"reasonCodes": [
"not_required"
]
},
"mrz_crosscheck": {
"status": "pass",
"score": 0.99,
"reasonCodes": []
},
"selfie_quality": {
"status": "pass",
"score": 0.91,
"reasonCodes": []
},
"document_near_face": {
"status": "skipped",
"score": null,
"reasonCodes": [
"not_required"
]
},
"active_liveness": {
"status": "pass",
"score": 0.93,
"reasonCodes": []
},
"speech_profile": {
"status": "skipped",
"score": null,
"reasonCodes": [
"speech_profiling_not_requested"
]
},
"speech_analysis": {
"status": "skipped",
"score": null,
"reasonCodes": [
"speech_analysis_not_requested"
]
},
"age_gender": {
"status": "skipped",
"score": null,
"reasonCodes": [
"age_gender_not_requested"
]
}
},
"scorecard": {
"document_front": {
"status": "pass",
"score": 0.94,
"checks": [],
"reasonCodes": []
},
"document_back": {
"status": "skipped",
"score": 0,
"checks": [],
"reasonCodes": [
"not_required"
]
},
"selfie": {
"status": "review",
"score": 0.91,
"checks": [],
"reasonCodes": [
"presentation_attack_detection_inconclusive"
]
},
"liveness": {
"status": "review",
"score": 0.93,
"checks": [],
"reasonCodes": [
"presentation_attack_detection_inconclusive"
]
},
"voice": {
"status": "skipped",
"score": 0,
"checks": [],
"reasonCodes": [
"not_required"
]
},
"cross_validation": {
"status": "pass",
"score": 0.99,
"checks": [],
"reasonCodes": []
},
"overall": {
"status": "review",
"score": 0.94,
"checks": [],
"reasonCodes": [
"presentation_attack_detection_inconclusive"
]
}
},
"nvid": "NVID-7K3M-Q9XA-T2RW-4HZD"
},
"requestId": "req-test"
}
Example request
curl
curl "https://collect.noxtica.com/v1/kyc/sessions/example-sessionId" \
-H "Authorization: Bearer $NOXTICA_API_KEY"
Node.js (global fetch, Node 18 or later)
const response = await fetch('https://collect.noxtica.com/v1/kyc/sessions/example-sessionId', {
method: 'GET',
headers: {
Authorization: `Bearer ${process.env.NOXTICA_API_KEY}`,
},
});
console.log(response.status, await response.text());
Python (standard library)
import os
import urllib.request
request = urllib.request.Request(
"https://collect.noxtica.com/v1/kyc/sessions/example-sessionId",
headers={
"Authorization": f"Bearer {os.environ['NOXTICA_API_KEY']}",
},
method="GET",
)
with urllib.request.urlopen(request) as response:
print(response.status, response.read().decode())
Read the hosted verification configuration, for your organisation or for one domain
GET /v1/kyc/config
Server-to-server, read-only. Returns the hosted verification configuration your invitations
use: with no domainId, your organisation default; with a domainId, the configuration
that applies to that domain.
Per-domain configuration. One organisation can own several domains, and each domain can override the hosted configuration (business name, purpose, privacy page, support page, supported documents, approved return hosts, embed origins, email verification, selfie, voice, device upload and liveness mode). Inheritance is field by field:
- For every field the effective value is the domain’s own value when it sets one, otherwise your organisation default, otherwise the platform default. A field the domain leaves unset inherits; nothing is merged inside a field.
- The lists (
documents,allowedReturnUrlHosts,allowedEmbedOrigins) are replaced as a whole, never combined. An empty list is a real override (“no return hosts”, “cannot be embedded”) and is not the same as an unset list. supportUrl: unset inherits. A domain can explicitly have no support link, and the effective value is thennull.- The platform default applies only to a field neither the domain nor your organisation
sets:
requireSelfietrue,requireVoicefalse,allowDeviceUploadfalse,livenessModepassive,allowedReturnUrlHosts[],allowedEmbedOrigins[],emailVerificationoff,supportUrlnull. businessName,purpose,privacyUrl,privacyVersionanddocumentshave no platform default. When no layer sets one of them,effectiveisnulland the hosted page shows “Verification isn’t ready yet”. A domain can therefore stand alone without an organisation default (for example when you operate several companies), or override only a few fields of the default.- A domain that sets its own
privacyUrlshould also set its ownprivacyVersion: the customer’s consent receipt records the version.
What a session uses. An invitation is created for one domain (see POST /v1/kyc/sessions; the mint response and the status read carry its domainId) and always
shows that domain’s configuration as it is at that moment: if you remove a domain’s
override later, its sessions fall back to your organisation default. The approved return
hosts and the email-verification mode are checked once, at mint, against the chosen domain
and are then fixed for the session. Until a domain has an override, every invitation behaves
as it did with your organisation default alone.
Countries. documents[].issuingCountries accepts any ISO 3166-1 alpha-2 country
(CountryCodeList: 1 to 249 distinct codes per document type), so a document type can be
offered for every country. A single invitation can offer fewer countries without changing
this configuration: see hostedOptions.allowedIssuingCountries on POST /v1/kyc/sessions.
This read returns the configuration itself, not the narrowing of any invitation.
Required identity fields. An entry of documents can carry requiredIdentityFields: which
of the five identity fields (surname, givenNames, dateOfBirth, personalNumber,
documentNumber) must be read from that document type for the extraction to count as complete
(see extraction on GET /v1/kyc/verifications/{verificationId}/identity). While the member
is absent the default applies: surname, givenNames, dateOfBirth and documentNumber,
plus personalNumber on a Turkish national ID card or driving licence, and on a Turkish
document whose T.C. Kimlik No was read and passes its check digits documentNumber is not
required. A non-empty list is exactly the set that is required and replaces that default,
including the Turkish rule; an empty list requires nothing. A domain override replaces the
whole documents list, like every other member of it. The member is edited in the Backoffice
hosted verification settings (organisation default or one domain), not through this API, and
this read returns it where it is set. It only decides which fields count towards
extraction.status and the informational reason code identity_fields_unreadable: it never
changes whether a session goes to review.
Response. With domainId: domainId and domain identify the domain, effective is
the configuration its invitations use, sources names the layer each effective field comes
from, and tenantDefault is your organisation default for comparison (null when you have
none). Without domainId: domainId and domain are null, effective equals
tenantDefault, and domains lists the active domains that carry an override, with the
fields each overrides (a domain without an override is not listed: it uses your
organisation default). An unknown
domain, a disabled domain and a domain of another organisation all answer the same
404 domain_not_found.
Read only. The configuration is edited in the Backoffice hosted verification settings (a domain selector chooses the domain), not through the API: a change there needs a signed-in administrator’s fresh multi-factor confirmation, which an API key cannot give.
Before you can call. The identity verification module must be enabled for your
organisation (otherwise the same byte-identical 404 as an unknown path) and your sk_ key
needs the write:identity scope. See “Rate limits” above.
Authentication: Bearer token (ApiKeyAuth), scope write:identity.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
domainId | query | string | No | Return the configuration that applies to this domain (the d_... id of a domain of your organisation, shown on the Backoffice Domains page; domains lists the ones that carry an override when this parameter is omitted). Omit it to read your organisation default and that list. An empty value is refused with 400 invalid_request. At least 1 character, at most 64 characters. |
Responses
| Status | Meaning | Error codes | Content |
|---|---|---|---|
200 | The configuration. | application/json KycHostedConfigResponse | |
400 | invalid_request — domainId is present but empty. | invalid_request | application/json KycSessionError |
401 | The Authorization: Bearer sk_... header is missing or malformed, or the key is unknown or revoked. The body is deliberately identical for every one of those causes. | invalid_api_key | application/json KycSessionError |
403 | The presented key lacks the required scope. | API key does not have write:identity scope | application/json |
404 | Either the byte-identical “V1 endpoint not found” response returned when the route’s module is not enabled for your organisation (see GatedNotFound), or domain_not_found: the domainId or siteKey is unknown, names a disabled domain, or belongs to another organisation. The three causes of domain_not_found get the same answer by design. | domain_not_found | application/json KycGatedOrDomainNotFoundError |
429 | Rate limit exceeded. Wait Retry-After seconds (also retryAfter in the body) before retrying; the response carries no other rate-limit headers. | Rate limit exceeded | application/json |
500 | API-key authentication is not configured on this deployment (server_misconfigured). Retrying does not help; contact support with the requestId. | server_misconfigured | application/json KycSessionError |
503 | Retryable. tenant_module_gate_unavailable (the module gate could not be answered), or invalid_request with a reason (pg_unavailable or backend_unavailable). | invalid_request | application/json KycSessionError |
503 | Noxtica could not read the key’s record. This is a temporary fault on Noxtica’s side and says nothing about your key. Retry after the Retry-After delay; do not treat it as a revoked key. | api_key_auth_unavailable | application/json |
Response headers
| Status | Header | Type | Description |
|---|---|---|---|
429 | Retry-After | integer | Seconds until the bucket allows another request. |
503 | Retry-After | integer | Seconds to wait before retrying. |
Response fields (200, application/json)
| Field | Type | Always returned | Description |
|---|---|---|---|
ok | boolean | Yes | Allowed values: true. |
domainId | string, nullable | Yes | The requested domain, or null when the call named none. |
domain | one of KycHostedConfigDomainRef or any, nullable | Yes | The requested domain, or null when the call named none. |
domain{KycHostedConfigDomainRef} | object (KycHostedConfigDomainRef) | No | A domain of your organisation. |
domain{KycHostedConfigDomainRef}.id | string | Yes | The domain id (d_...). |
domain{KycHostedConfigDomainRef}.origin | string | Yes | The domain’s origin, for example https://shop.example.com. |
effective | one of KycHostedConfig or any, nullable | Yes | The configuration invitations use. null when a required field (businessName, purpose, privacyUrl, privacyVersion, documents) is set by no layer; the hosted page then shows “Verification isn’t ready yet”. |
effective{KycHostedConfig} | object (KycHostedConfig) | No | One hosted verification configuration: your organisation default, or the effective configuration of one domain (see GET /v1/kyc/config for how a domain’s own values and your organisation default combine). Timestamps are Unix time in milliseconds. |
effective{KycHostedConfig}.businessName | string | Yes | The business name the hosted page shows to the customer. |
effective{KycHostedConfig}.purpose | string | Yes | The default purpose shown on the consent screen when the mint call sends none. |
effective{KycHostedConfig}.privacyUrl | string (uri) | Yes | The privacy page the hosted page links to (https). |
effective{KycHostedConfig}.supportUrl | string (uri), nullable | Yes | The support page the hosted page links to (https), or null when there is none. |
effective{KycHostedConfig}.privacyVersion | string | Yes | The version label of the privacy notice at privacyUrl; the customer’s consent receipt records it. |
effective{KycHostedConfig}.documents | array of KycHostedConfigDocument | Yes | The document types and issuing countries the customer can choose from. |
effective{KycHostedConfig}.documents[] | object (KycHostedConfigDocument) | No | One document type the hosted flow accepts, with the issuing countries it accepts it from. |
effective{KycHostedConfig}.documents[].idType | string | Yes | Allowed values: national_id, passport, driving_license, residence_permit. |
effective{KycHostedConfig}.documents[].issuingCountries | array of string | Yes | Issuing countries the customer can pick for this document type, as ISO 3166-1 alpha-2 codes (CountryCode values; any of the 249 assigned codes). At most 249 items. |
effective{KycHostedConfig}.documents[].requireBack | boolean | Yes | Whether the back of the document is captured. |
effective{KycHostedConfig}.documents[].requiredIdentityFields | array of string | No | The identity fields that must be read from this document type for the extraction to count as complete (see extraction on GET /v1/kyc/verifications/{verificationId}/identity). Present only when set. Absent means the default: surname, given names, date of birth and document number, plus the personal number on a Turkish national ID card or driving licence, where a read T.C. Kimlik No that passes its check digits makes the document number optional. A non-empty list replaces that default exactly; an empty list requires none. Edited in the Backoffice hosted verification settings, not through this API. Unique items. |
effective{KycHostedConfig}.requireSelfie | boolean | Yes | |
effective{KycHostedConfig}.requireVoice | boolean | Yes | |
effective{KycHostedConfig}.allowDeviceUpload | boolean | Yes | Whether the customer may upload a file from their device instead of using the camera. |
effective{KycHostedConfig}.livenessMode | string | Yes | Allowed values: passive, assisted_review, full_challenge. |
effective{KycHostedConfig}.emailVerification | string | Yes | Hosted email-verification mode. off — no email step. optional — the customer is offered the step and may skip it. required — the customer must verify an address before the session can be submitted (409 email_verification_required until then). The mode is resolved once at mint and fixed for the session. On the tenant-facing GET /v1/kyc/sessions/{sessionId} this key is the effective mode string; the hosted page’s own session routes carry the customer-facing state object KycEmailVerificationState under the same key. Allowed values: off, optional, required. |
effective{KycHostedConfig}.allowedReturnUrlHosts | array of string | Yes | Host names a returnUrl may point to at mint. |
effective{KycHostedConfig}.allowedEmbedOrigins | array of string | Yes | Origins (scheme, host and optional port, for example https://shop.example.com) that may embed the hosted page in a frame. An empty list means it cannot be embedded. |
effective{KycHostedConfig}.updatedAt | integer (int64) | Yes | When the configuration last changed. For the effective configuration of a domain, the later of the domain’s change and your organisation’s change. |
sources | object (KycHostedConfigSources) | Yes | For every overridable field, the layer its effective value comes from. All thirteen members are always present. |
sources.businessName | string | Yes | The layer an effective value comes from. domain — the domain’s own override. tenant — your organisation default. platform — the built-in default Noxtica applies to a field neither the domain nor your organisation sets. Allowed values: domain, tenant, platform. |
sources.defaultPurpose | string | Yes | The layer an effective value comes from. domain — the domain’s own override. tenant — your organisation default. platform — the built-in default Noxtica applies to a field neither the domain nor your organisation sets. Allowed values: domain, tenant, platform. |
sources.privacyUrl | string | Yes | The layer an effective value comes from. domain — the domain’s own override. tenant — your organisation default. platform — the built-in default Noxtica applies to a field neither the domain nor your organisation sets. Allowed values: domain, tenant, platform. |
sources.supportUrl | string | Yes | The layer an effective value comes from. domain — the domain’s own override. tenant — your organisation default. platform — the built-in default Noxtica applies to a field neither the domain nor your organisation sets. Allowed values: domain, tenant, platform. |
sources.privacyVersion | string | Yes | The layer an effective value comes from. domain — the domain’s own override. tenant — your organisation default. platform — the built-in default Noxtica applies to a field neither the domain nor your organisation sets. Allowed values: domain, tenant, platform. |
sources.documents | string | Yes | The layer an effective value comes from. domain — the domain’s own override. tenant — your organisation default. platform — the built-in default Noxtica applies to a field neither the domain nor your organisation sets. Allowed values: domain, tenant, platform. |
sources.requireSelfie | string | Yes | The layer an effective value comes from. domain — the domain’s own override. tenant — your organisation default. platform — the built-in default Noxtica applies to a field neither the domain nor your organisation sets. Allowed values: domain, tenant, platform. |
sources.requireVoice | string | Yes | The layer an effective value comes from. domain — the domain’s own override. tenant — your organisation default. platform — the built-in default Noxtica applies to a field neither the domain nor your organisation sets. Allowed values: domain, tenant, platform. |
sources.allowDeviceUpload | string | Yes | The layer an effective value comes from. domain — the domain’s own override. tenant — your organisation default. platform — the built-in default Noxtica applies to a field neither the domain nor your organisation sets. Allowed values: domain, tenant, platform. |
sources.livenessMode | string | Yes | The layer an effective value comes from. domain — the domain’s own override. tenant — your organisation default. platform — the built-in default Noxtica applies to a field neither the domain nor your organisation sets. Allowed values: domain, tenant, platform. |
sources.allowedReturnUrlHosts | string | Yes | The layer an effective value comes from. domain — the domain’s own override. tenant — your organisation default. platform — the built-in default Noxtica applies to a field neither the domain nor your organisation sets. Allowed values: domain, tenant, platform. |
sources.allowedEmbedOrigins | string | Yes | The layer an effective value comes from. domain — the domain’s own override. tenant — your organisation default. platform — the built-in default Noxtica applies to a field neither the domain nor your organisation sets. Allowed values: domain, tenant, platform. |
sources.emailVerification | string | Yes | The layer an effective value comes from. domain — the domain’s own override. tenant — your organisation default. platform — the built-in default Noxtica applies to a field neither the domain nor your organisation sets. Allowed values: domain, tenant, platform. |
tenantDefault | one of KycHostedConfig or any, nullable | Yes | Your organisation default, or null when you have not configured one. |
tenantDefault{KycHostedConfig} | object (KycHostedConfig) | No | One hosted verification configuration: your organisation default, or the effective configuration of one domain (see GET /v1/kyc/config for how a domain’s own values and your organisation default combine). Timestamps are Unix time in milliseconds. |
tenantDefault{KycHostedConfig}.businessName | string | Yes | The business name the hosted page shows to the customer. |
tenantDefault{KycHostedConfig}.purpose | string | Yes | The default purpose shown on the consent screen when the mint call sends none. |
tenantDefault{KycHostedConfig}.privacyUrl | string (uri) | Yes | The privacy page the hosted page links to (https). |
tenantDefault{KycHostedConfig}.supportUrl | string (uri), nullable | Yes | The support page the hosted page links to (https), or null when there is none. |
tenantDefault{KycHostedConfig}.privacyVersion | string | Yes | The version label of the privacy notice at privacyUrl; the customer’s consent receipt records it. |
tenantDefault{KycHostedConfig}.documents | array of KycHostedConfigDocument | Yes | The document types and issuing countries the customer can choose from. |
tenantDefault{KycHostedConfig}.documents[] | object (KycHostedConfigDocument) | No | One document type the hosted flow accepts, with the issuing countries it accepts it from. |
tenantDefault{KycHostedConfig}.documents[].idType | string | Yes | Allowed values: national_id, passport, driving_license, residence_permit. |
tenantDefault{KycHostedConfig}.documents[].issuingCountries | array of string | Yes | Issuing countries the customer can pick for this document type, as ISO 3166-1 alpha-2 codes (CountryCode values; any of the 249 assigned codes). At most 249 items. |
tenantDefault{KycHostedConfig}.documents[].requireBack | boolean | Yes | Whether the back of the document is captured. |
tenantDefault{KycHostedConfig}.documents[].requiredIdentityFields | array of string | No | The identity fields that must be read from this document type for the extraction to count as complete (see extraction on GET /v1/kyc/verifications/{verificationId}/identity). Present only when set. Absent means the default: surname, given names, date of birth and document number, plus the personal number on a Turkish national ID card or driving licence, where a read T.C. Kimlik No that passes its check digits makes the document number optional. A non-empty list replaces that default exactly; an empty list requires none. Edited in the Backoffice hosted verification settings, not through this API. Unique items. |
tenantDefault{KycHostedConfig}.requireSelfie | boolean | Yes | |
tenantDefault{KycHostedConfig}.requireVoice | boolean | Yes | |
tenantDefault{KycHostedConfig}.allowDeviceUpload | boolean | Yes | Whether the customer may upload a file from their device instead of using the camera. |
tenantDefault{KycHostedConfig}.livenessMode | string | Yes | Allowed values: passive, assisted_review, full_challenge. |
tenantDefault{KycHostedConfig}.emailVerification | string | Yes | Hosted email-verification mode. off — no email step. optional — the customer is offered the step and may skip it. required — the customer must verify an address before the session can be submitted (409 email_verification_required until then). The mode is resolved once at mint and fixed for the session. On the tenant-facing GET /v1/kyc/sessions/{sessionId} this key is the effective mode string; the hosted page’s own session routes carry the customer-facing state object KycEmailVerificationState under the same key. Allowed values: off, optional, required. |
tenantDefault{KycHostedConfig}.allowedReturnUrlHosts | array of string | Yes | Host names a returnUrl may point to at mint. |
tenantDefault{KycHostedConfig}.allowedEmbedOrigins | array of string | Yes | Origins (scheme, host and optional port, for example https://shop.example.com) that may embed the hosted page in a frame. An empty list means it cannot be embedded. |
tenantDefault{KycHostedConfig}.updatedAt | integer (int64) | Yes | When the configuration last changed. For the effective configuration of a domain, the later of the domain’s change and your organisation’s change. |
domains | array of KycHostedConfigDomainSummary | No | Present only when the call named no domainId. The active domains that carry an override. |
domains[] | object (KycHostedConfigDomainSummary) | No | One active domain that carries an override, with the hosted configuration fields it overrides. |
domains[].domainId | string | Yes | |
domains[].origin | string | Yes | |
domains[].overriddenFields | array of string | Yes | The fields this domain sets itself; every other field inherits from your organisation default. Never empty. |
requestId | string | No |
Response example (200)
{
"ok": true,
"domainId": "d_7Kp3Zx9QmW2aLr5T",
"domain": {
"id": "d_7Kp3Zx9QmW2aLr5T",
"origin": "https://markets.example.com"
},
"effective": {
"businessName": "Acme Markets Inc.",
"purpose": "Open your brokerage account",
"privacyUrl": "https://markets.example.com/privacy",
"supportUrl": "https://markets.example.com/help",
"privacyVersion": "2026-10-15",
"documents": [
{
"idType": "passport",
"issuingCountries": [
"GB",
"DE"
],
"requireBack": false
},
{
"idType": "national_id",
"issuingCountries": [
"GB"
],
"requireBack": true
}
],
"requireSelfie": true,
"requireVoice": false,
"allowDeviceUpload": false,
"livenessMode": "passive",
"emailVerification": "off",
"allowedReturnUrlHosts": [
"acme.example.com"
],
"allowedEmbedOrigins": [],
"updatedAt": 1790761000000
},
"sources": {
"businessName": "domain",
"defaultPurpose": "tenant",
"privacyUrl": "domain",
"supportUrl": "domain",
"privacyVersion": "domain",
"documents": "tenant",
"requireSelfie": "tenant",
"requireVoice": "tenant",
"allowDeviceUpload": "tenant",
"livenessMode": "tenant",
"allowedReturnUrlHosts": "tenant",
"allowedEmbedOrigins": "tenant",
"emailVerification": "tenant"
},
"tenantDefault": {
"businessName": "Acme Brokerage Ltd",
"purpose": "Open your brokerage account",
"privacyUrl": "https://acme.example.com/privacy",
"supportUrl": "https://acme.example.com/support",
"privacyVersion": "2026-09-01",
"documents": [
{
"idType": "passport",
"issuingCountries": [
"GB",
"DE"
],
"requireBack": false
},
{
"idType": "national_id",
"issuingCountries": [
"GB"
],
"requireBack": true
}
],
"requireSelfie": true,
"requireVoice": false,
"allowDeviceUpload": false,
"livenessMode": "passive",
"emailVerification": "off",
"allowedReturnUrlHosts": [
"acme.example.com"
],
"allowedEmbedOrigins": [],
"updatedAt": 1790760000000
},
"requestId": "req-test"
}
Example request
curl
curl "https://collect.noxtica.com/v1/kyc/config" \
-H "Authorization: Bearer $NOXTICA_API_KEY"
Node.js (global fetch, Node 18 or later)
const response = await fetch('https://collect.noxtica.com/v1/kyc/config', {
method: 'GET',
headers: {
Authorization: `Bearer ${process.env.NOXTICA_API_KEY}`,
},
});
console.log(response.status, await response.text());
Python (standard library)
import os
import urllib.request
request = urllib.request.Request(
"https://collect.noxtica.com/v1/kyc/config",
headers={
"Authorization": f"Bearer {os.environ['NOXTICA_API_KEY']}",
},
method="GET",
)
with urllib.request.urlopen(request) as response:
print(response.status, response.read().decode())
Read the current hosted session (browser-facing)
GET /v1/kyc/session-view
Called by the hosted verification page itself, never by a tenant backend.
Noxtica Verification ID. Only when the Noxtica Verification ID (NVID) is enabled for the platform, the session object carries verificationIdentifier (KycVerificationIdentifier) — the NVID with its derived status and an emailOnFile flag — or null when the session has no NVID. It lets the page show the ID again when a finished link is reloaded. The key is absent while the feature is not enabled; the email address is never returned.
Authentication: Bearer token (SessionTokenAuth).
Responses
| Status | Meaning | Error codes | Content |
|---|---|---|---|
200 | The session snapshot. | application/json KycHostedSessionEnvelope | |
403 | Missing/disallowed Origin, or an invalid/expired session token. | invalid_request | application/json ErrorEnvelope |
404 | The byte-identical “V1 endpoint not found” response returned when the route’s module is not enabled for your organisation — indistinguishable from a route that does not exist (anti-enumeration by design; do not treat this as proof the path is wrong). | V1 endpoint not found | application/json |
410 | The session can no longer be used by the hosted verification page, in one of two byte-stable shapes told apart by code. kyc_session_expired (error: session_expired) — the session is past its effective expiresAt (the original mint TTL while unopened, or openedAt plus the 30-minute capture window once opened) and never reached a terminal/completed state. kyc_session_cancelled (error: session_cancelled) — the end customer cancelled the session via POST /v1/kyc/session-cancel (terminal status cancelled_by_user). The hosted verification page MUST stop polling on either response. | kyc_session_cancelled, session_cancelled | application/json |
429 | Rate limit exceeded. Wait Retry-After seconds (also retryAfter in the body) before retrying; the response carries no other rate-limit headers. | Rate limit exceeded | application/json |
Response headers
| Status | Header | Type | Description |
|---|---|---|---|
429 | Retry-After | integer | Seconds until the bucket allows another request. |
Response fields (200, application/json)
| Field | Type | Always returned | Description |
|---|---|---|---|
ok | boolean | No | |
session | object (KycHostedSessionSnapshot) | No | The customer-facing snapshot the hosted verification page renders from. Called by the page itself, never by a tenant backend; it is not a stable integration surface. All timestamps are Unix milliseconds. Optional members appear only when the matching capability is active for the session. |
session.status | string | Yes | The hosted session lifecycle status. pending — minted, link not opened yet. opened — the customer reached the capture page. verifying — the customer submitted and the verification is running. review_pending — the verification finished and awaits a Backoffice review decision (today every successful hosted capture lands here; it never expires, and kyc.session.completed has NOT fired yet). completed — a business decision exists (a reviewer’s approval or override: read verification and review on the status read for which). unverified — finished with a negative outcome and no manual-review code. error — a provider or infrastructure fault, or the submit was refused at admission. expired — the effective deadline passed without a decision (the status read reports it with 200; kyc.session.completed never fires for it). cancelled_by_user — the customer cancelled on the hosted page. Allowed values: pending, opened, verifying, review_pending, completed, unverified, error, expired, cancelled_by_user. |
session.idType | string | Yes | |
session.captureStep | string | Yes | The furthest capture step the customer has reached. Allowed values: consent, id_front, id_back, selfie, document_near_face, liveness, voice, review, submit. |
session.expiresAt | integer (int64) | Yes | Effective session deadline: the original mint-time deadline while the link is unopened, tightened in place to openedAt + 1800000 (the 30-minute capture window) once opened, whichever is sooner. |
session.openedAt | integer (int64), nullable | Yes | When the customer’s first hosted-flow action landed (the pending to opened transition); null until then. |
session.steps | array of string | Yes | The full step vocabulary, in order. |
session.configuration | one of KycHostedSessionConfiguration or any, nullable | Yes | The hosted setup that applies to this session (the configuration of the domain it was created for, with every field the domain leaves unset taken from your organisation default; see GET /v1/kyc/config), filtered by this session’s options; null when no hosted configuration applies (the page shows “Verification isn’t ready yet”). |
session.configuration{KycHostedSessionConfiguration} | object (KycHostedSessionConfiguration) | No | |
session.configuration{KycHostedSessionConfiguration}.businessName | string | Yes | |
session.configuration{KycHostedSessionConfiguration}.purpose | string | Yes | |
session.configuration{KycHostedSessionConfiguration}.privacyUrl | string | Yes | |
session.configuration{KycHostedSessionConfiguration}.supportUrl | string, nullable | Yes | |
session.configuration{KycHostedSessionConfiguration}.returnUrl | string, nullable | Yes | The mint-time returnUrl. The page renders it only as a “Return to <business>” link (completed outcome, or the screen shown when a step cannot continue); nothing is appended and nothing redirects automatically. |
session.configuration{KycHostedSessionConfiguration}.privacyVersion | string | Yes | |
session.configuration{KycHostedSessionConfiguration}.biometricConsentVersion | string | No | Present only when biometric consent is enabled for the platform. |
session.configuration{KycHostedSessionConfiguration}.documents | array of object | Yes | |
session.configuration{KycHostedSessionConfiguration}.documents[] | object | No | |
session.configuration{KycHostedSessionConfiguration}.documents[].idType | string | Yes | Allowed values: national_id, passport, driving_license, residence_permit. |
session.configuration{KycHostedSessionConfiguration}.documents[].issuingCountries | array of string | Yes | The issuing countries the customer can pick for this document type, as ISO 3166-1 alpha-2 codes (CountryCode values), after the invitation’s hostedOptions.allowedIssuingCountries narrowing. At most 249 items. |
session.configuration{KycHostedSessionConfiguration}.documents[].requireBack | boolean | Yes | |
session.configuration{KycHostedSessionConfiguration}.documents[].requiredIdentityFields | array of string | No | Present only when your configuration sets it. The identity fields that must be read from this document type for the extraction to count as complete (extraction on the identity read); an empty list requires none. Absent means the default (see GET /v1/kyc/config). Unique items. |
session.configuration{KycHostedSessionConfiguration}.requireSelfie | boolean | Yes | |
session.configuration{KycHostedSessionConfiguration}.requireVoice | boolean | Yes | |
session.configuration{KycHostedSessionConfiguration}.livenessMode | string | Yes | Allowed values: passive, assisted_review, full_challenge. |
session.configuration{KycHostedSessionConfiguration}.options | object (KycHostedSessionOptions) | Yes | Invitation-scoped hosted-flow switches, frozen into the session at mint. When you send hostedOptions it must be the complete object (every required key below, no merging with defaults); omit it to use the defaults (all four document types, allowDeviceUpload false, documentBack auto, documentNearFace false, activeLiveness true, speechAnalysis false, ageGenderEstimation false, emailVerification off). Inside steps, ageGenderEstimation and emailVerification are optional. allowedIssuingCountries is optional too. |
session.configuration{KycHostedSessionConfiguration}.options.allowedDocumentTypes | array of string | Yes | At least 1 item, at most 4 items, unique items. |
session.configuration{KycHostedSessionConfiguration}.options.allowedIssuingCountries | array of string | No | Optional. Narrows the invitation to these issuing countries: each document type your hosted configuration accepts keeps only the issuing countries that are also listed, a document type left with none is dropped, and a mint where no document type remains is refused with 400 invalid_hosted_options (field allowedIssuingCountries, reason no_matching_document_country). Absent means no narrowing. Any ISO 3166-1 alpha-2 code is accepted, including codes your configuration does not offer. At least 1 item, at most 249 items, unique items. |
session.configuration{KycHostedSessionConfiguration}.options.allowDeviceUpload | boolean | Yes | |
session.configuration{KycHostedSessionConfiguration}.options.steps | object | Yes | |
session.configuration{KycHostedSessionConfiguration}.options.steps.documentBack | string | Yes | Allowed values: auto. |
session.configuration{KycHostedSessionConfiguration}.options.steps.documentNearFace | boolean | Yes | |
session.configuration{KycHostedSessionConfiguration}.options.steps.activeLiveness | boolean | Yes | |
session.configuration{KycHostedSessionConfiguration}.options.steps.speechAnalysis | boolean | Yes | |
session.configuration{KycHostedSessionConfiguration}.options.steps.ageGenderEstimation | boolean | No | Optional; default false. Adds an advisory age and gender estimate from the selfie frames (biometric, special-category data); the customer gives a separate consent on the page. Setting true is refused with 403 age_gender_estimation_not_enabled (reasonCode age_gender_platform_disabled or age_gender_tenant_acknowledgement_missing) unless the platform has enabled the feature AND your organisation has recorded the matching legal-basis acknowledgement in the Backoffice. It never fails a verification. |
session.configuration{KycHostedSessionConfiguration}.options.steps.emailVerification | string | No | Optional (added 2026-10-01; default off when the key is absent). When omitted from a mint request your organisation’s default applies; an explicit value wins in both directions. An explicit optional or required is refused with 400 email_verification_unavailable until the platform has enabled hosted email verification. The mint response echoes the effective mode. Allowed values: off, optional, required. |
session.selection | one of object or any, nullable | Yes | The customer’s document choice, once made. |
session.selection{variant 1} | object | No | |
session.selection{variant 1}.idType | string | Yes | |
session.selection{variant 1}.issuingCountry | string | Yes | The issuing country the customer chose, as an ISO 3166-1 alpha-2 code (one of the CountryCode values). Pattern ^[A-Z]{2}$. |
session.consent | one of KycHostedSessionConsent or any, nullable | Yes | The privacy-notice receipt; immutable once set. |
session.consent{KycHostedSessionConsent} | object (KycHostedSessionConsent) | No | |
session.consent{KycHostedSessionConsent}.version | string | Yes | |
session.consent{KycHostedSessionConsent}.acceptedAt | integer (int64) | Yes | |
session.biometricConsent | one of KycHostedSessionConsent or any, nullable | No | The biometric-processing consent receipt. Present only when biometric consent is enabled for the platform. |
session.biometricConsent{KycHostedSessionConsent} | object (KycHostedSessionConsent) | No | |
session.biometricConsent{KycHostedSessionConsent}.version | string | Yes | |
session.biometricConsent{KycHostedSessionConsent}.acceptedAt | integer (int64) | Yes | |
session.speechProfilingConsent | object | No | |
session.speechProfilingConsent.acceptedAt | integer (int64) | Yes | |
session.ageGenderConsent | object | No | |
session.ageGenderConsent.acceptedAt | integer (int64) | Yes | |
session.evidence | object (KycSessionEvidenceRefs) | Yes | Opaque object ids of the finalized (usable) uploads, by role. Never a URL or media bytes. A role appears only once its upload is finalized. |
session.evidence.front | string | No | |
session.evidence.back | string | No | |
session.evidence.selfie | string | No | |
session.evidence.voice | string | No | |
session.evidence.documentNearFace | string | No | |
session.resume | object (KycHostedSessionResume) | Yes | Whether the customer link this snapshot answers for is still usable for further capture. |
session.resume.eligible | boolean | Yes | |
session.resume.reason | string | No | Present when eligible is false. cancelled means the session is in the terminal cancelled_by_user status (defensive — every customer route already answers 410 session_cancelled for such a session, so a snapshot of a cancelled row is not normally served to the hosted page). Allowed values: expired, completed, consumed, cancelled. |
session.decision | object (KycSessionDecision) | No | The decision block, present once the pipeline or a review decision has produced one. outcome maps from the session status alone: completed to approved, unverified to rejected, review_pending to review_pending, error to error. It is not the review decision: a session a reviewer overrode is completed too and reads approved here. Use verification and review to tell an approval from an override. |
session.decision.outcome | string | Yes | Allowed values: approved, rejected, review_pending, error. |
session.decision.reasonCodes | array of string | Yes | |
session.decision.decidedAt | integer (int64) | Yes | Unix ms. |
session.processing | object | No | Coarse “what is happening now” for the status page; absent while the customer is still capturing. |
session.processing.stage | string | Yes | Allowed values: uploading, queued, evaluating, review_pending, decided. |
session.processing.updatedAt | integer (int64) | Yes | |
session.evidenceQuality | object | No | Per-role structural quality receipt; absent until a role’s evidence has been inspected. |
session.livenessSteps | array of object | No | The configured liveness challenge with per-step completion. |
session.livenessSteps[] | object | No | |
session.livenessSteps[].id | string | Yes | |
session.livenessSteps[].kind | string | Yes | |
session.livenessSteps[].completed | boolean | Yes | |
session.voiceChallenge | object | No | The anti-replay digit phrase; present only when the tenant requires the voice step. |
session.voiceChallenge.phrase | string | Yes | |
session.failureBudget | object | Yes | Attempt counters per retryable step (front, back, selfie, liveness, voice, upload). |
session.failureBudget.* | object | No | |
session.failureBudget.*.attempts | integer | Yes | |
session.failureBudget.*.max | integer | Yes | |
session.deviceChoice | string | No | A previously recorded cross-device continuation choice; absent when none was recorded. Allowed values: desktop, phone. |
session.serverNow | integer (int64) | Yes | The server clock when the snapshot was built; the page computes remaining time against it, not against its own clock. |
session.sessionExpiresAt | integer (int64) | Yes | The same deadline as expiresAt, named for the two-timer UI. |
session.currentStep | string, nullable | No | The step the customer most recently entered. Present only when per-step timers are enabled for the platform. |
session.stepStartedAt | integer (int64), nullable | No | |
session.stepExpiresAt | integer (int64), nullable | No | min(stepStartedAt + 10 minutes, sessionExpiresAt); null for an untimed step. |
session.capabilities | object (KycHostedSessionCapabilities) | No | Server capabilities the hosted verification page may rely on. The object lists only the capabilities that are active for this session and is omitted entirely when none is. cancel is advertised when the collector serves POST /v1/kyc/session-cancel AND has cancel armed; emailVerification is advertised when hosted email verification is enabled for the platform and the session’s effective mode is not off. The hosted page treats “field present” as the only capability signal (it never probes a route, since a probe could itself create state) and, when a capability is absent, falls back to the behaviour of an older collector (for cancel: a client-only end screen with no network call; for email verification: no email step). |
session.capabilities.cancel | boolean | No | The collector accepts POST /v1/kyc/session-cancel for this session. Allowed values: true. |
session.capabilities.emailVerification | boolean | No | The hosted flow includes the email-verification step for this session. Allowed values: true. |
session.emailVerification | object (KycEmailVerificationState) | No | The customer-facing email-verification state returned by the hosted page’s email routes and embedded in the snapshot as emailVerification. The address is always masked (j***@example.com). |
session.emailVerification.mode | string | Yes | Allowed values: optional, required. |
session.emailVerification.email | string, nullable | Yes | The masked address; null before one was entered. |
session.emailVerification.verified | boolean | Yes | |
session.emailVerification.verifiedAt | integer (int64), nullable | Yes | |
session.emailVerification.expiresAt | integer (int64), nullable | Yes | Deadline of the live confirmation link (30 minutes or the session end, whichever is sooner); null when none is live. |
session.emailVerification.resendAvailableAt | integer (int64), nullable | Yes | Earliest next send while the 60 second cooldown runs; null when a resend is allowed now or the cap is exhausted. |
session.emailVerification.sendsRemaining | integer | Yes | Sends left of the 5 allowed per session. |
session.verificationIdentifier | one of KycVerificationIdentifier or any, nullable | No | Present only while the NVID is enabled for the platform; null when the session has none. |
session.verificationIdentifier{KycVerificationIdentifier} | object (KycVerificationIdentifier) | No | The session’s Noxtica Verification ID as shown on the hosted page’s final screen (session.verificationIdentifier on GET /v1/kyc/session-view). Customer-facing; never carries an address. |
session.verificationIdentifier{KycVerificationIdentifier}.nvid | string | Yes | Canonical NVID, NVID-XXXX-XXXX-XXXX-XXXX. |
session.verificationIdentifier{KycVerificationIdentifier}.status | string | Yes | Public status of a Noxtica Verification ID. revoked wins over everything; pending = the verification awaits a review decision; expired = 365 days after completion; otherwise verified. Allowed values: verified, pending, revoked, expired. |
session.verificationIdentifier{KycVerificationIdentifier}.verifiedAt | integer (int64), nullable | Yes | Unix ms of completion; null while pending. |
session.verificationIdentifier{KycVerificationIdentifier}.expiresAt | integer (int64), nullable | Yes | Unix ms, completion + 365 days; null while pending. |
session.verificationIdentifier{KycVerificationIdentifier}.emailOnFile | boolean | Yes | Whether the person gave an address to receive the NVID. The address itself is never returned. |
session.evidenceUpload | object | No | Capability marker for step-wise evidence upload; present only when that capability is enabled for the platform. |
session.evidenceReceipts | object | No | Server-authoritative per-step evidence receipts; present with evidenceUpload. |
session.scorecard | object | No | The customer-masked per-step scorecard (GET /v1/kyc/session-view only): a status per step and nothing else; no score, check or reason code reaches a browser. The full numeric version is session.scorecard on the tenant status read. |
session.scorecard.document_front | object (KycCustomerScorecardEntry) | No | |
session.scorecard.document_front.status | string | Yes | Allowed values: pass, needs_review, failed, skipped, pending. |
session.scorecard.document_back | object (KycCustomerScorecardEntry) | No | |
session.scorecard.document_back.status | string | Yes | Allowed values: pass, needs_review, failed, skipped, pending. |
session.scorecard.selfie | object (KycCustomerScorecardEntry) | No | |
session.scorecard.selfie.status | string | Yes | Allowed values: pass, needs_review, failed, skipped, pending. |
session.scorecard.liveness | object (KycCustomerScorecardEntry) | No | |
session.scorecard.liveness.status | string | Yes | Allowed values: pass, needs_review, failed, skipped, pending. |
session.scorecard.voice | object (KycCustomerScorecardEntry) | No | |
session.scorecard.voice.status | string | Yes | Allowed values: pass, needs_review, failed, skipped, pending. |
session.scorecard.cross_validation | object (KycCustomerScorecardEntry) | No | |
session.scorecard.cross_validation.status | string | Yes | Allowed values: pass, needs_review, failed, skipped, pending. |
session.scorecard.overall | object (KycCustomerScorecardEntry) | No | |
session.scorecard.overall.status | string | Yes | Allowed values: pass, needs_review, failed, skipped, pending. |
requestId | string | No |
Response example (200)
{
"ok": true,
"session": {
"status": "opened",
"idType": "passport",
"captureStep": "consent",
"expiresAt": 1790763900000,
"openedAt": 1790762100000,
"steps": [
"consent",
"id_front",
"id_back",
"selfie",
"document_near_face",
"liveness",
"voice",
"review",
"submit"
],
"configuration": {
"businessName": "Example Bank",
"purpose": "Open your brokerage account",
"privacyUrl": "https://merchant.example.com/privacy",
"supportUrl": "https://merchant.example.com/support",
"returnUrl": "https://merchant.example.com/kyc/return",
"privacyVersion": "2026-09-01",
"documents": [
{
"idType": "passport",
"issuingCountries": [
"GB"
],
"requireBack": false
}
],
"requireSelfie": true,
"requireVoice": false,
"livenessMode": "passive",
"options": {
"allowedDocumentTypes": [
"passport"
],
"allowDeviceUpload": false,
"steps": {
"documentBack": "auto",
"documentNearFace": false,
"activeLiveness": true,
"speechAnalysis": false
}
}
},
"selection": null,
"consent": null,
"evidence": {},
"resume": {
"eligible": true
},
"failureBudget": {
"front": {
"attempts": 0,
"max": 10
},
"back": {
"attempts": 0,
"max": 10
},
"selfie": {
"attempts": 0,
"max": 10
},
"liveness": {
"attempts": 0,
"max": 3
},
"voice": {
"attempts": 0,
"max": 10
},
"upload": {
"attempts": 0,
"max": 3
}
},
"serverNow": 1790762150000,
"sessionExpiresAt": 1790763900000
},
"requestId": "req-test"
}
Example request
curl
curl "https://collect.noxtica.com/v1/kyc/session-view" \
-H "Authorization: Bearer $NOXTICA_SESSION_TOKEN"
Node.js (global fetch, Node 18 or later)
const response = await fetch('https://collect.noxtica.com/v1/kyc/session-view', {
method: 'GET',
headers: {
Authorization: `Bearer ${process.env.NOXTICA_SESSION_TOKEN}`,
},
});
console.log(response.status, await response.text());
Python (standard library)
import os
import urllib.request
request = urllib.request.Request(
"https://collect.noxtica.com/v1/kyc/session-view",
headers={
"Authorization": f"Bearer {os.environ['NOXTICA_SESSION_TOKEN']}",
},
method="GET",
)
with urllib.request.urlopen(request) as response:
print(response.status, response.read().decode())
Checkpoint hosted-flow progress (browser-facing)
POST /v1/kyc/session-progress
Records consent, document/issuing-country selection, and step completion as the customer moves through the hosted flow.
Authentication: Bearer token (SessionTokenAuth).
Request body
Content type application/json, required.
Type: object.
Responses
| Status | Meaning | Error codes | Content |
|---|---|---|---|
200 | Checkpoint recorded; returns the updated snapshot. | application/json KycHostedSessionEnvelope | |
400 | The request failed validation. error is a route-specific code such as invalid_request; read the operation description for the exact values it can return. | invalid_request | application/json ErrorEnvelope |
403 | Missing/disallowed Origin, or an invalid/expired session token. | invalid_request | application/json ErrorEnvelope |
404 | The byte-identical “V1 endpoint not found” response returned when the route’s module is not enabled for your organisation — indistinguishable from a route that does not exist (anti-enumeration by design; do not treat this as proof the path is wrong). | V1 endpoint not found | application/json |
409 | The session is already terminal. | invalid_request | application/json ErrorEnvelope |
410 | The session can no longer be used by the hosted verification page, in one of two byte-stable shapes told apart by code. kyc_session_expired (error: session_expired) — the session is past its effective expiresAt (the original mint TTL while unopened, or openedAt plus the 30-minute capture window once opened) and never reached a terminal/completed state. kyc_session_cancelled (error: session_cancelled) — the end customer cancelled the session via POST /v1/kyc/session-cancel (terminal status cancelled_by_user). The hosted verification page MUST stop polling on either response. | kyc_session_cancelled, session_cancelled | application/json |
429 | Rate limit exceeded. Wait Retry-After seconds (also retryAfter in the body) before retrying; the response carries no other rate-limit headers. | Rate limit exceeded | application/json |
Response headers
| Status | Header | Type | Description |
|---|---|---|---|
429 | Retry-After | integer | Seconds until the bucket allows another request. |
Response fields (200, application/json)
| Field | Type | Always returned | Description |
|---|---|---|---|
ok | boolean | No | |
session | object (KycHostedSessionSnapshot) | No | The customer-facing snapshot the hosted verification page renders from. Called by the page itself, never by a tenant backend; it is not a stable integration surface. All timestamps are Unix milliseconds. Optional members appear only when the matching capability is active for the session. |
session.status | string | Yes | The hosted session lifecycle status. pending — minted, link not opened yet. opened — the customer reached the capture page. verifying — the customer submitted and the verification is running. review_pending — the verification finished and awaits a Backoffice review decision (today every successful hosted capture lands here; it never expires, and kyc.session.completed has NOT fired yet). completed — a business decision exists (a reviewer’s approval or override: read verification and review on the status read for which). unverified — finished with a negative outcome and no manual-review code. error — a provider or infrastructure fault, or the submit was refused at admission. expired — the effective deadline passed without a decision (the status read reports it with 200; kyc.session.completed never fires for it). cancelled_by_user — the customer cancelled on the hosted page. Allowed values: pending, opened, verifying, review_pending, completed, unverified, error, expired, cancelled_by_user. |
session.idType | string | Yes | |
session.captureStep | string | Yes | The furthest capture step the customer has reached. Allowed values: consent, id_front, id_back, selfie, document_near_face, liveness, voice, review, submit. |
session.expiresAt | integer (int64) | Yes | Effective session deadline: the original mint-time deadline while the link is unopened, tightened in place to openedAt + 1800000 (the 30-minute capture window) once opened, whichever is sooner. |
session.openedAt | integer (int64), nullable | Yes | When the customer’s first hosted-flow action landed (the pending to opened transition); null until then. |
session.steps | array of string | Yes | The full step vocabulary, in order. |
session.configuration | one of KycHostedSessionConfiguration or any, nullable | Yes | The hosted setup that applies to this session (the configuration of the domain it was created for, with every field the domain leaves unset taken from your organisation default; see GET /v1/kyc/config), filtered by this session’s options; null when no hosted configuration applies (the page shows “Verification isn’t ready yet”). |
session.configuration{KycHostedSessionConfiguration} | object (KycHostedSessionConfiguration) | No | |
session.configuration{KycHostedSessionConfiguration}.businessName | string | Yes | |
session.configuration{KycHostedSessionConfiguration}.purpose | string | Yes | |
session.configuration{KycHostedSessionConfiguration}.privacyUrl | string | Yes | |
session.configuration{KycHostedSessionConfiguration}.supportUrl | string, nullable | Yes | |
session.configuration{KycHostedSessionConfiguration}.returnUrl | string, nullable | Yes | The mint-time returnUrl. The page renders it only as a “Return to <business>” link (completed outcome, or the screen shown when a step cannot continue); nothing is appended and nothing redirects automatically. |
session.configuration{KycHostedSessionConfiguration}.privacyVersion | string | Yes | |
session.configuration{KycHostedSessionConfiguration}.biometricConsentVersion | string | No | Present only when biometric consent is enabled for the platform. |
session.configuration{KycHostedSessionConfiguration}.documents | array of object | Yes | |
session.configuration{KycHostedSessionConfiguration}.documents[] | object | No | |
session.configuration{KycHostedSessionConfiguration}.documents[].idType | string | Yes | Allowed values: national_id, passport, driving_license, residence_permit. |
session.configuration{KycHostedSessionConfiguration}.documents[].issuingCountries | array of string | Yes | The issuing countries the customer can pick for this document type, as ISO 3166-1 alpha-2 codes (CountryCode values), after the invitation’s hostedOptions.allowedIssuingCountries narrowing. At most 249 items. |
session.configuration{KycHostedSessionConfiguration}.documents[].requireBack | boolean | Yes | |
session.configuration{KycHostedSessionConfiguration}.documents[].requiredIdentityFields | array of string | No | Present only when your configuration sets it. The identity fields that must be read from this document type for the extraction to count as complete (extraction on the identity read); an empty list requires none. Absent means the default (see GET /v1/kyc/config). Unique items. |
session.configuration{KycHostedSessionConfiguration}.requireSelfie | boolean | Yes | |
session.configuration{KycHostedSessionConfiguration}.requireVoice | boolean | Yes | |
session.configuration{KycHostedSessionConfiguration}.livenessMode | string | Yes | Allowed values: passive, assisted_review, full_challenge. |
session.configuration{KycHostedSessionConfiguration}.options | object (KycHostedSessionOptions) | Yes | Invitation-scoped hosted-flow switches, frozen into the session at mint. When you send hostedOptions it must be the complete object (every required key below, no merging with defaults); omit it to use the defaults (all four document types, allowDeviceUpload false, documentBack auto, documentNearFace false, activeLiveness true, speechAnalysis false, ageGenderEstimation false, emailVerification off). Inside steps, ageGenderEstimation and emailVerification are optional. allowedIssuingCountries is optional too. |
session.configuration{KycHostedSessionConfiguration}.options.allowedDocumentTypes | array of string | Yes | At least 1 item, at most 4 items, unique items. |
session.configuration{KycHostedSessionConfiguration}.options.allowedIssuingCountries | array of string | No | Optional. Narrows the invitation to these issuing countries: each document type your hosted configuration accepts keeps only the issuing countries that are also listed, a document type left with none is dropped, and a mint where no document type remains is refused with 400 invalid_hosted_options (field allowedIssuingCountries, reason no_matching_document_country). Absent means no narrowing. Any ISO 3166-1 alpha-2 code is accepted, including codes your configuration does not offer. At least 1 item, at most 249 items, unique items. |
session.configuration{KycHostedSessionConfiguration}.options.allowDeviceUpload | boolean | Yes | |
session.configuration{KycHostedSessionConfiguration}.options.steps | object | Yes | |
session.configuration{KycHostedSessionConfiguration}.options.steps.documentBack | string | Yes | Allowed values: auto. |
session.configuration{KycHostedSessionConfiguration}.options.steps.documentNearFace | boolean | Yes | |
session.configuration{KycHostedSessionConfiguration}.options.steps.activeLiveness | boolean | Yes | |
session.configuration{KycHostedSessionConfiguration}.options.steps.speechAnalysis | boolean | Yes | |
session.configuration{KycHostedSessionConfiguration}.options.steps.ageGenderEstimation | boolean | No | Optional; default false. Adds an advisory age and gender estimate from the selfie frames (biometric, special-category data); the customer gives a separate consent on the page. Setting true is refused with 403 age_gender_estimation_not_enabled (reasonCode age_gender_platform_disabled or age_gender_tenant_acknowledgement_missing) unless the platform has enabled the feature AND your organisation has recorded the matching legal-basis acknowledgement in the Backoffice. It never fails a verification. |
session.configuration{KycHostedSessionConfiguration}.options.steps.emailVerification | string | No | Optional (added 2026-10-01; default off when the key is absent). When omitted from a mint request your organisation’s default applies; an explicit value wins in both directions. An explicit optional or required is refused with 400 email_verification_unavailable until the platform has enabled hosted email verification. The mint response echoes the effective mode. Allowed values: off, optional, required. |
session.selection | one of object or any, nullable | Yes | The customer’s document choice, once made. |
session.selection{variant 1} | object | No | |
session.selection{variant 1}.idType | string | Yes | |
session.selection{variant 1}.issuingCountry | string | Yes | The issuing country the customer chose, as an ISO 3166-1 alpha-2 code (one of the CountryCode values). Pattern ^[A-Z]{2}$. |
session.consent | one of KycHostedSessionConsent or any, nullable | Yes | The privacy-notice receipt; immutable once set. |
session.consent{KycHostedSessionConsent} | object (KycHostedSessionConsent) | No | |
session.consent{KycHostedSessionConsent}.version | string | Yes | |
session.consent{KycHostedSessionConsent}.acceptedAt | integer (int64) | Yes | |
session.biometricConsent | one of KycHostedSessionConsent or any, nullable | No | The biometric-processing consent receipt. Present only when biometric consent is enabled for the platform. |
session.biometricConsent{KycHostedSessionConsent} | object (KycHostedSessionConsent) | No | |
session.biometricConsent{KycHostedSessionConsent}.version | string | Yes | |
session.biometricConsent{KycHostedSessionConsent}.acceptedAt | integer (int64) | Yes | |
session.speechProfilingConsent | object | No | |
session.speechProfilingConsent.acceptedAt | integer (int64) | Yes | |
session.ageGenderConsent | object | No | |
session.ageGenderConsent.acceptedAt | integer (int64) | Yes | |
session.evidence | object (KycSessionEvidenceRefs) | Yes | Opaque object ids of the finalized (usable) uploads, by role. Never a URL or media bytes. A role appears only once its upload is finalized. |
session.evidence.front | string | No | |
session.evidence.back | string | No | |
session.evidence.selfie | string | No | |
session.evidence.voice | string | No | |
session.evidence.documentNearFace | string | No | |
session.resume | object (KycHostedSessionResume) | Yes | Whether the customer link this snapshot answers for is still usable for further capture. |
session.resume.eligible | boolean | Yes | |
session.resume.reason | string | No | Present when eligible is false. cancelled means the session is in the terminal cancelled_by_user status (defensive — every customer route already answers 410 session_cancelled for such a session, so a snapshot of a cancelled row is not normally served to the hosted page). Allowed values: expired, completed, consumed, cancelled. |
session.decision | object (KycSessionDecision) | No | The decision block, present once the pipeline or a review decision has produced one. outcome maps from the session status alone: completed to approved, unverified to rejected, review_pending to review_pending, error to error. It is not the review decision: a session a reviewer overrode is completed too and reads approved here. Use verification and review to tell an approval from an override. |
session.decision.outcome | string | Yes | Allowed values: approved, rejected, review_pending, error. |
session.decision.reasonCodes | array of string | Yes | |
session.decision.decidedAt | integer (int64) | Yes | Unix ms. |
session.processing | object | No | Coarse “what is happening now” for the status page; absent while the customer is still capturing. |
session.processing.stage | string | Yes | Allowed values: uploading, queued, evaluating, review_pending, decided. |
session.processing.updatedAt | integer (int64) | Yes | |
session.evidenceQuality | object | No | Per-role structural quality receipt; absent until a role’s evidence has been inspected. |
session.livenessSteps | array of object | No | The configured liveness challenge with per-step completion. |
session.livenessSteps[] | object | No | |
session.livenessSteps[].id | string | Yes | |
session.livenessSteps[].kind | string | Yes | |
session.livenessSteps[].completed | boolean | Yes | |
session.voiceChallenge | object | No | The anti-replay digit phrase; present only when the tenant requires the voice step. |
session.voiceChallenge.phrase | string | Yes | |
session.failureBudget | object | Yes | Attempt counters per retryable step (front, back, selfie, liveness, voice, upload). |
session.failureBudget.* | object | No | |
session.failureBudget.*.attempts | integer | Yes | |
session.failureBudget.*.max | integer | Yes | |
session.deviceChoice | string | No | A previously recorded cross-device continuation choice; absent when none was recorded. Allowed values: desktop, phone. |
session.serverNow | integer (int64) | Yes | The server clock when the snapshot was built; the page computes remaining time against it, not against its own clock. |
session.sessionExpiresAt | integer (int64) | Yes | The same deadline as expiresAt, named for the two-timer UI. |
session.currentStep | string, nullable | No | The step the customer most recently entered. Present only when per-step timers are enabled for the platform. |
session.stepStartedAt | integer (int64), nullable | No | |
session.stepExpiresAt | integer (int64), nullable | No | min(stepStartedAt + 10 minutes, sessionExpiresAt); null for an untimed step. |
session.capabilities | object (KycHostedSessionCapabilities) | No | Server capabilities the hosted verification page may rely on. The object lists only the capabilities that are active for this session and is omitted entirely when none is. cancel is advertised when the collector serves POST /v1/kyc/session-cancel AND has cancel armed; emailVerification is advertised when hosted email verification is enabled for the platform and the session’s effective mode is not off. The hosted page treats “field present” as the only capability signal (it never probes a route, since a probe could itself create state) and, when a capability is absent, falls back to the behaviour of an older collector (for cancel: a client-only end screen with no network call; for email verification: no email step). |
session.capabilities.cancel | boolean | No | The collector accepts POST /v1/kyc/session-cancel for this session. Allowed values: true. |
session.capabilities.emailVerification | boolean | No | The hosted flow includes the email-verification step for this session. Allowed values: true. |
session.emailVerification | object (KycEmailVerificationState) | No | The customer-facing email-verification state returned by the hosted page’s email routes and embedded in the snapshot as emailVerification. The address is always masked (j***@example.com). |
session.emailVerification.mode | string | Yes | Allowed values: optional, required. |
session.emailVerification.email | string, nullable | Yes | The masked address; null before one was entered. |
session.emailVerification.verified | boolean | Yes | |
session.emailVerification.verifiedAt | integer (int64), nullable | Yes | |
session.emailVerification.expiresAt | integer (int64), nullable | Yes | Deadline of the live confirmation link (30 minutes or the session end, whichever is sooner); null when none is live. |
session.emailVerification.resendAvailableAt | integer (int64), nullable | Yes | Earliest next send while the 60 second cooldown runs; null when a resend is allowed now or the cap is exhausted. |
session.emailVerification.sendsRemaining | integer | Yes | Sends left of the 5 allowed per session. |
session.verificationIdentifier | one of KycVerificationIdentifier or any, nullable | No | Present only while the NVID is enabled for the platform; null when the session has none. |
session.verificationIdentifier{KycVerificationIdentifier} | object (KycVerificationIdentifier) | No | The session’s Noxtica Verification ID as shown on the hosted page’s final screen (session.verificationIdentifier on GET /v1/kyc/session-view). Customer-facing; never carries an address. |
session.verificationIdentifier{KycVerificationIdentifier}.nvid | string | Yes | Canonical NVID, NVID-XXXX-XXXX-XXXX-XXXX. |
session.verificationIdentifier{KycVerificationIdentifier}.status | string | Yes | Public status of a Noxtica Verification ID. revoked wins over everything; pending = the verification awaits a review decision; expired = 365 days after completion; otherwise verified. Allowed values: verified, pending, revoked, expired. |
session.verificationIdentifier{KycVerificationIdentifier}.verifiedAt | integer (int64), nullable | Yes | Unix ms of completion; null while pending. |
session.verificationIdentifier{KycVerificationIdentifier}.expiresAt | integer (int64), nullable | Yes | Unix ms, completion + 365 days; null while pending. |
session.verificationIdentifier{KycVerificationIdentifier}.emailOnFile | boolean | Yes | Whether the person gave an address to receive the NVID. The address itself is never returned. |
session.evidenceUpload | object | No | Capability marker for step-wise evidence upload; present only when that capability is enabled for the platform. |
session.evidenceReceipts | object | No | Server-authoritative per-step evidence receipts; present with evidenceUpload. |
session.scorecard | object | No | The customer-masked per-step scorecard (GET /v1/kyc/session-view only): a status per step and nothing else; no score, check or reason code reaches a browser. The full numeric version is session.scorecard on the tenant status read. |
session.scorecard.document_front | object (KycCustomerScorecardEntry) | No | |
session.scorecard.document_front.status | string | Yes | Allowed values: pass, needs_review, failed, skipped, pending. |
session.scorecard.document_back | object (KycCustomerScorecardEntry) | No | |
session.scorecard.document_back.status | string | Yes | Allowed values: pass, needs_review, failed, skipped, pending. |
session.scorecard.selfie | object (KycCustomerScorecardEntry) | No | |
session.scorecard.selfie.status | string | Yes | Allowed values: pass, needs_review, failed, skipped, pending. |
session.scorecard.liveness | object (KycCustomerScorecardEntry) | No | |
session.scorecard.liveness.status | string | Yes | Allowed values: pass, needs_review, failed, skipped, pending. |
session.scorecard.voice | object (KycCustomerScorecardEntry) | No | |
session.scorecard.voice.status | string | Yes | Allowed values: pass, needs_review, failed, skipped, pending. |
session.scorecard.cross_validation | object (KycCustomerScorecardEntry) | No | |
session.scorecard.cross_validation.status | string | Yes | Allowed values: pass, needs_review, failed, skipped, pending. |
session.scorecard.overall | object (KycCustomerScorecardEntry) | No | |
session.scorecard.overall.status | string | Yes | Allowed values: pass, needs_review, failed, skipped, pending. |
requestId | string | No |
Response example (200)
{
"ok": true,
"session": {
"status": "opened",
"idType": "passport",
"captureStep": "id_front",
"expiresAt": 1790763900000,
"openedAt": 1790762100000,
"steps": [
"consent",
"id_front",
"id_back",
"selfie",
"document_near_face",
"liveness",
"voice",
"review",
"submit"
],
"configuration": {
"businessName": "Example Bank",
"purpose": "Open your brokerage account",
"privacyUrl": "https://merchant.example.com/privacy",
"supportUrl": "https://merchant.example.com/support",
"returnUrl": "https://merchant.example.com/kyc/return",
"privacyVersion": "2026-09-01",
"documents": [
{
"idType": "passport",
"issuingCountries": [
"GB"
],
"requireBack": false
}
],
"requireSelfie": true,
"requireVoice": false,
"livenessMode": "passive",
"options": {
"allowedDocumentTypes": [
"passport"
],
"allowDeviceUpload": false,
"steps": {
"documentBack": "auto",
"documentNearFace": false,
"activeLiveness": true,
"speechAnalysis": false
}
}
},
"selection": {
"idType": "passport",
"issuingCountry": "GB"
},
"consent": {
"version": "2026-09-01",
"acceptedAt": 1790762110000
},
"evidence": {},
"resume": {
"eligible": true
},
"failureBudget": {
"front": {
"attempts": 0,
"max": 10
},
"back": {
"attempts": 0,
"max": 10
},
"selfie": {
"attempts": 0,
"max": 10
},
"liveness": {
"attempts": 0,
"max": 3
},
"voice": {
"attempts": 0,
"max": 10
},
"upload": {
"attempts": 0,
"max": 3
}
},
"serverNow": 1790762160000,
"sessionExpiresAt": 1790763900000
},
"requestId": "req-test"
}
Example request
curl
curl -X POST "https://collect.noxtica.com/v1/kyc/session-progress" \
-H "Authorization: Bearer $NOXTICA_SESSION_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
Node.js (global fetch, Node 18 or later)
const response = await fetch('https://collect.noxtica.com/v1/kyc/session-progress', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.NOXTICA_SESSION_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({}),
});
console.log(response.status, await response.text());
Python (standard library)
import json
import os
import urllib.request
request = urllib.request.Request(
"https://collect.noxtica.com/v1/kyc/session-progress",
data=json.dumps({}).encode(),
headers={
"Authorization": f"Bearer {os.environ['NOXTICA_SESSION_TOKEN']}",
"Content-Type": "application/json",
},
method="POST",
)
with urllib.request.urlopen(request) as response:
print(response.status, response.read().decode())
Cancel the hosted session (browser-facing)
POST /v1/kyc/session-cancel
Called by the hosted verification page when the end customer confirms they do not want to continue. Moves the session to the terminal status cancelled_by_user, recorded server-side (cancelledAt, optional closed-enum cancelReason); the cancel itself hard-deletes no evidence (evidence retention rules are unchanged).
Capability. A collector that serves and has armed this route advertises it on every session snapshot (GET /v1/kyc/session-view, POST /v1/kyc/session-progress) as session.capabilities.cancel: true. When the field is absent (an older collector, or cancel disarmed) the hosted page never calls this route and shows a client-only end screen instead. An older or unarmed collector answers this route with the same byte-identical V1 404 as any other gated route.
When it is allowed. Cancel is accepted from any capture, consent, selection, or review step (session pending or opened). It is refused with 409 session_not_cancellable once the session is verifying (the single-use submit gate is consumed and the verification is being paid for) or already completed, unverified, error, or review_pending. A session past its deadline answers 410 session_expired.
Idempotency. Repeating the call on an already-cancelled session returns 200 with the ORIGINAL cancelledAt and cancelReason.
After a cancel every other customer route (session-view, session-progress, session-submit, session-evidence/handle, session-evidence/finalize) answers 410 session_cancelled (code: kyc_session_cancelled).
Webhook. The call that actually transitions the session emits ONE kyc.session.cancelled webhook to every enabled endpoint subscribed to that event type (subscription is opt-in per endpoint; nothing is sent to an endpoint that did not list it). The envelope data is {schemaVersion: "1", sessionId, status: "cancelled_by_user", cancelReason (closed enum or null), cancelledAt (Unix ms), occurredAt (= cancelledAt), deliveryId} — plus email and emailVerified when hosted email verification is enabled for the platform AND the session has a stored address (KycSessionCancelledWebhookData) — and carries no other subject data. An idempotent replay, a 409 and an expiry emit nothing. Delivery is at-least-once through the same signed outbox as kyc.session.completed; receivers MUST dedup by deliveryId. A webhook is a notification, not authority: GET /v1/kyc/sessions/{sessionId} confirms the status cancelled_by_user but does NOT return cancelledAt or cancelReason; those are delivered only in this webhook.
Billing. A cancelled session is never billed as a verification: a cancel creates no verification request and no spend reservation, and never records the kyc.verification meter (that meter is recorded only at submit-time spend admission). Only the request-level endpoint.collect.request count applies, as for every hosted session call.
Rate limits: a per-IP limit, then a per-session limit — the same as session-progress.
Authentication: Bearer token (SessionTokenAuth).
Request body
Content type application/json, optional.
| Field | Type | Required | Description |
|---|---|---|---|
reason | string | No | The closed set of reasons an end customer may give when cancelling a hosted session. There is deliberately no free-text reason. Allowed values: not_now, privacy_concern, no_document, technical_problem, other. |
Responses
| Status | Meaning | Error codes | Content |
|---|---|---|---|
200 | The session is cancelled_by_user — either this call cancelled it or it was already cancelled (idempotent replay; the original cancelledAt and cancelReason are returned). | application/json KycSessionCancelResponse | |
400 | invalid_request — invalid body, or reason outside the closed enum. | invalid_request | application/json ErrorEnvelope |
401 | invalid_session — the session token is invalid. | invalid_session | application/json ErrorEnvelope |
403 | origin_not_allowed — missing or disallowed Origin (same origin enforcement as every other hosted route). | origin_not_allowed | application/json ErrorEnvelope |
404 | The byte-identical “V1 endpoint not found” response returned when the route’s module is not enabled for your organisation — indistinguishable from a route that does not exist (anti-enumeration by design; do not treat this as proof the path is wrong). | V1 endpoint not found | application/json |
409 | session_not_cancellable — the session is already verifying, completed, unverified, error, or review_pending; status names which. | session_not_cancellable | application/json KycSessionNotCancellableError |
410 | The session has expired — past its effective expiresAt (the original mint TTL while unopened, or openedAt plus the 30-minute capture window once opened) and never reached a terminal/completed state. The hosted verification page MUST stop polling on this response. | kyc_session_expired, session_cancelled, session_expired | application/json KycSessionExpiredError |
429 | Rate limit exceeded. Wait Retry-After seconds (also retryAfter in the body) before retrying; the response carries no other rate-limit headers. | Rate limit exceeded | application/json |
503 | invalid_request — an unexpected internal fault while handling the cancel (any uncaught error in the handler). The body is the same {error: invalid_request, requestId} envelope as a 400, so clients MUST key off the status code, not the error string, to tell the two apart. The session is not reported as cancelled; it is safe to retry the call (cancel is idempotent). | invalid_request | application/json ErrorEnvelope |
Response headers
| Status | Header | Type | Description |
|---|---|---|---|
429 | Retry-After | integer | Seconds until the bucket allows another request. |
Response fields (200, application/json)
| Field | Type | Always returned | Description |
|---|---|---|---|
ok | boolean | Yes | Allowed values: true. |
status | string | Yes | Allowed values: cancelled_by_user. |
cancelledAt | integer (int64) | Yes | Unix ms of the cancel. An idempotent replay returns the ORIGINAL value, never a fresh timestamp. |
cancelReason | one of KycSessionCancelReason or any, nullable | Yes | The reason recorded by the FIRST successful call (null if none was given). A replay returns the original reason. |
requestId | string | Yes |
Response example (200)
{
"ok": true,
"status": "cancelled_by_user",
"cancelledAt": 1768434600000,
"cancelReason": "not_now",
"requestId": "req-test"
}
Example request
curl
curl -X POST "https://collect.noxtica.com/v1/kyc/session-cancel" \
-H "Authorization: Bearer $NOXTICA_SESSION_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"reason": "not_now"
}'
Node.js (global fetch, Node 18 or later)
const response = await fetch('https://collect.noxtica.com/v1/kyc/session-cancel', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.NOXTICA_SESSION_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
reason: 'not_now',
}),
});
console.log(response.status, await response.text());
Python (standard library)
import json
import os
import urllib.request
request = urllib.request.Request(
"https://collect.noxtica.com/v1/kyc/session-cancel",
data=json.dumps({
"reason": "not_now",
}).encode(),
headers={
"Authorization": f"Bearer {os.environ['NOXTICA_SESSION_TOKEN']}",
"Content-Type": "application/json",
},
method="POST",
)
with urllib.request.urlopen(request) as response:
print(response.status, response.read().decode())
Submit the hosted session for verification (browser-facing, one-shot)
POST /v1/kyc/session-submit
Drives the SAME handleVerifyAction pipeline POST /v1/kyc/verify uses, under the session’s own tenant, with a server-derived payload (evidence object ids resolved exclusively from this session’s own finalized evidence — a browser can never substitute its own payload fields). Single-use: a lost single-use claim on a concurrent resubmit returns verify_gate_consumed, distinct from a stale replay (session_not_submittable). See the closed SUBMIT REFUSAL CODES table below — hardened 2026-09-13 (D8b) so every pre-verification failure returns a distinct, actionable code instead of a shared invalid_request.
Email verification. When the session’s effective email-verification mode is required and the customer has not verified an address, the submit is refused with 409 email_verification_required; the session stays open and can be submitted once the address is verified. Only possible while hosted email verification is enabled for the platform.
Noxtica Verification ID. When the Noxtica Verification ID (NVID) is enabled for the platform, a 200 response also carries nvid — the session’s NVID when the outcome is positive (completed or review_pending), otherwise null. The key is absent while the feature is not enabled.
Authentication: Bearer token (SessionTokenAuth).
Request body
Content type application/json, optional.
Type: object.
Responses
| Status | Meaning | Error codes | Content |
|---|---|---|---|
200 | A resolved verify attempt (verified, unverified, or review_pending). | application/json KycVerifyResponse | |
400 | idempotency_key_invalid or payload_invalid — both new distinct codes as of 2026-09-13. | idempotency_key_invalid, invalid_request, payload_invalid | application/json ErrorEnvelope |
403 | Missing/disallowed Origin, an invalid/expired session token, or evidence_refused with refusal evidence_not_provisioned or residency_unconfigured (the organisation’s raw-evidence storage is no longer provisioned; nothing was consumed and the session stays retryable). | evidence_refused | application/json KycSessionEvidenceError |
404 | The byte-identical “V1 endpoint not found” response returned when the route’s module is not enabled for your organisation — indistinguishable from a route that does not exist (anti-enumeration by design; do not treat this as proof the path is wrong). | V1 endpoint not found | application/json |
409 | session_not_submittable (already terminal/mid-flight), verify_gate_consumed (NEW 2026-09-13 — lost the single-use race), or email_verification_required (NEW 2026-10-01 — the session’s effective email-verification mode is required and no address has been verified yet). | email_verification_required, invalid_request, session_not_submittable, verify_gate_consumed | application/json ErrorEnvelope |
410 | The session can no longer be used by the hosted verification page, in one of two byte-stable shapes told apart by code. kyc_session_expired (error: session_expired) — the session is past its effective expiresAt (the original mint TTL while unopened, or openedAt plus the 30-minute capture window once opened) and never reached a terminal/completed state. kyc_session_cancelled (error: session_cancelled) — the end customer cancelled the session via POST /v1/kyc/session-cancel (terminal status cancelled_by_user). The hosted verification page MUST stop polling on either response. | kyc_session_cancelled, session_cancelled | application/json |
429 | session_rate_limited (the per-session budget, NEW 2026-09-13, distinct from the dispatcher-level 429 below) or the per-IP hosted-page limit. | invalid_request, session_rate_limited | application/json ErrorEnvelope |
503 | nonce_mint_unavailable (NEW), submit_internal_error (NEW), hosted_verify_not_configured, or kyc_legal_basis_receipt_unavailable. | hosted_verify_not_configured, invalid_request, kyc_legal_basis_receipt_unavailable, nonce_mint_unavailable, submit_internal_error | application/json ErrorEnvelope |
Response fields (200, application/json)
| Field | Type | Always returned | Description |
|---|---|---|---|
ok | boolean | Yes | Allowed values: true. |
verdict | string | Yes | error is an infra/provider fault, never a statement about the subject — always HTTP 200 (FR-6). Allowed values: verified, unverified, error. |
verificationLevel | string | Yes | Allowed values: verified, partial, unverified, inconclusive. |
reasonCodes | array of string | Yes | |
verificationId | string (uuid) | Yes | |
requestId | string | Yes | |
legalBasisRef | string | No | Present only when this request’s tenant is running under classified legal-basis receipt gating. |
nfcAssurance | object | No | Present only for eMRTD/passport-chip verification paths. |
deviceTrust | object | No | Present only when the request set deviceContext:true and device-intel evidence is available. ADVISORY ONLY — never fed back into verdict/reasonCodes. |
Response example (200)
{
"ok": true,
"verdict": "verified",
"verificationLevel": "verified",
"reasonCodes": [
"provider_timeout"
],
"verificationId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"requestId": "req-test",
"legalBasisRef": "example-legalBasisRef",
"nfcAssurance": {},
"deviceTrust": {}
}
Example request
curl
curl -X POST "https://collect.noxtica.com/v1/kyc/session-submit" \
-H "Authorization: Bearer $NOXTICA_SESSION_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
Node.js (global fetch, Node 18 or later)
const response = await fetch('https://collect.noxtica.com/v1/kyc/session-submit', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.NOXTICA_SESSION_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({}),
});
console.log(response.status, await response.text());
Python (standard library)
import json
import os
import urllib.request
request = urllib.request.Request(
"https://collect.noxtica.com/v1/kyc/session-submit",
data=json.dumps({}).encode(),
headers={
"Authorization": f"Bearer {os.environ['NOXTICA_SESSION_TOKEN']}",
"Content-Type": "application/json",
},
method="POST",
)
with urllib.request.urlopen(request) as response:
print(response.status, response.read().decode())
Email the person their Noxtica Verification ID (browser-facing, optional, consented)
POST /v1/kyc/session-verification-id/email
Called by the hosted verification page when the person, on the final screen, asks Noxtica to email them their Noxtica Verification ID (NVID) and its public status link. Optional and independent of the verification outcome. The person’s explicit consent is required (consent.version nvid-email-v1, consent.accepted true; the hosted checkbox is unticked by default). Noxtica — not the tenant — is the controller for this address: it is sealed (AES-256-GCM) at rest, used only to send this email, kept only with the verification record, never returned by any API and never disclosed to the tenant.
Only available while the NVID feature is enabled for the platform; otherwise the byte-identical gated 404. Requires an issued, non-revoked NVID on the session (409 verification_id_unavailable otherwise). Limits: per-session, per-IP and per-address limiters, at most 3 sends per session from the hosted page, at least 60 seconds between sends (429 rate_limited).
Authentication: Bearer token (SessionTokenAuth).
Request body
Content type application/json, required.
| Field | Type | Required | Description |
|---|---|---|---|
email | string | Yes | The address to send the NVID to (normalised server-side). At most 254 characters. |
locale | string | Yes | One of the 36 supported UI locales; the email is rendered in it (unsupported falls back to en). |
consent | object | Yes | |
consent.version | string | Yes | Allowed values: nvid-email-v1. |
consent.accepted | boolean | Yes | Allowed values: true. |
Responses
| Status | Meaning | Error codes | Content |
|---|---|---|---|
200 | The email was accepted for delivery. | application/json | |
400 | invalid_email or consent_required. | consent_required, invalid_email | application/json ErrorEnvelope |
403 | Missing/disallowed Origin, or an invalid/expired session token. | origin_not_allowed | application/json ErrorEnvelope |
404 | The byte-identical “V1 endpoint not found” response returned when the route’s module is not enabled for your organisation — indistinguishable from a route that does not exist (anti-enumeration by design; do not treat this as proof the path is wrong). | V1 endpoint not found | application/json |
409 | verification_id_unavailable — the session has no NVID, or it was revoked. | verification_id_unavailable | application/json ErrorEnvelope |
410 | The session can no longer be used by the hosted verification page, in one of two byte-stable shapes told apart by code. kyc_session_expired (error: session_expired) — the session is past its effective expiresAt (the original mint TTL while unopened, or openedAt plus the 30-minute capture window once opened) and never reached a terminal/completed state. kyc_session_cancelled (error: session_cancelled) — the end customer cancelled the session via POST /v1/kyc/session-cancel (terminal status cancelled_by_user). The hosted verification page MUST stop polling on either response. | kyc_session_cancelled, session_cancelled | application/json |
429 | Rate limit exceeded. Wait Retry-After seconds (also retryAfter in the body) before retrying; the response carries no other rate-limit headers. | Rate limit exceeded, rate_limited | application/json |
502 | email_send_failed — the provider refused or failed; the send slot is released. | email_send_failed | application/json ErrorEnvelope |
503 | email_unavailable — the address cannot be sealed right now; nothing was stored. | email_unavailable | application/json ErrorEnvelope |
Response headers
| Status | Header | Type | Description |
|---|---|---|---|
429 | Retry-After | integer | Seconds until the bucket allows another request. |
Response fields (200, application/json)
| Field | Type | Always returned | Description |
|---|---|---|---|
ok | boolean | Yes | Allowed values: true. |
Response example (200)
{
"ok": true
}
Example request
curl
curl -X POST "https://collect.noxtica.com/v1/kyc/session-verification-id/email" \
-H "Authorization: Bearer $NOXTICA_SESSION_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"email": "[email protected]",
"locale": "en",
"consent": {
"version": "nvid-email-v1",
"accepted": true
}
}'
Node.js (global fetch, Node 18 or later)
const response = await fetch('https://collect.noxtica.com/v1/kyc/session-verification-id/email', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.NOXTICA_SESSION_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
email: '[email protected]',
locale: 'en',
consent: {
version: 'nvid-email-v1',
accepted: true,
},
}),
});
console.log(response.status, await response.text());
Python (standard library)
import json
import os
import urllib.request
request = urllib.request.Request(
"https://collect.noxtica.com/v1/kyc/session-verification-id/email",
data=json.dumps({
"email": "[email protected]",
"locale": "en",
"consent": {
"version": "nvid-email-v1",
"accepted": True,
},
}).encode(),
headers={
"Authorization": f"Bearer {os.environ['NOXTICA_SESSION_TOKEN']}",
"Content-Type": "application/json",
},
method="POST",
)
with urllib.request.urlopen(request) as response:
print(response.status, response.read().decode())
Send the email-verification link for the address the customer typed (browser-facing)
POST /v1/kyc/session-email/send
Called by the hosted verification page, never by a tenant backend. Available only while hosted email verification is enabled for the platform AND the session’s effective emailVerification mode (fixed at mint) is optional or required; otherwise the byte-identical gated 404 (platform off) or 409 email_verification_not_enabled (mode off). Sends one confirmation link to the address; the link is valid for 30 minutes or until the session ends, whichever is sooner. Opening the link changes nothing: the customer must press the button on the page it opens, because mail gateways prefetch links.
Limits per session: at most 5 accepted sends, at least 60 seconds between sends (429 cooldown with retryAfterSec, 429 send_limit_reached), no send with less than 60 seconds of session left (409 session_expiring). Per client IP 20 sends an hour and per address 5 sends an hour (429 Rate limit exceeded). A provider failure rolls the send back (502 email_send_failed) so an outage does not spend the customer’s quota. The response carries the masked address only.
Authentication: Bearer token (SessionTokenAuth).
Request body
Content type application/json, required.
| Field | Type | Required | Description |
|---|---|---|---|
email | string | Yes | The address to verify (normalised server-side). At most 254 characters. |
locale | string | No | One of the supported UI locales; the email is rendered in it (an unsupported value falls back to en). |
Responses
| Status | Meaning | Error codes | Content |
|---|---|---|---|
200 | The link was sent. | application/json KycEmailVerificationStateEnvelope | |
400 | invalid_email, or invalid_request (unreadable body). | invalid_email, invalid_request | application/json ErrorEnvelope |
401 | invalid_session — no session bearer, or one that does not resolve to a session. | invalid_session | application/json ErrorEnvelope |
403 | Missing or disallowed Origin. | origin_not_allowed | application/json ErrorEnvelope |
404 | The byte-identical “V1 endpoint not found” response returned when the route’s module is not enabled for your organisation — indistinguishable from a route that does not exist (anti-enumeration by design; do not treat this as proof the path is wrong). | V1 endpoint not found | application/json |
409 | email_verification_not_enabled (effective mode off), already_verified (this address is already confirmed; the body carries the current emailVerification state), session_expiring (under 60 seconds left), or session_not_editable (the session is past capture; the body carries its status). | already_verified, email_verification_not_enabled, session_expiring, session_not_editable | application/json ErrorEnvelope |
410 | The session can no longer be used by the hosted verification page, in one of two byte-stable shapes told apart by code. kyc_session_expired (error: session_expired) — the session is past its effective expiresAt (the original mint TTL while unopened, or openedAt plus the 30-minute capture window once opened) and never reached a terminal/completed state. kyc_session_cancelled (error: session_cancelled) — the end customer cancelled the session via POST /v1/kyc/session-cancel (terminal status cancelled_by_user). The hosted verification page MUST stop polling on either response. | kyc_session_cancelled, session_cancelled | application/json |
413 | body_too_large — the request exceeded this route’s byte cap. | body_too_large | application/json ErrorEnvelope |
429 | cooldown (with retryAfterSec), send_limit_reached, or Rate limit exceeded (per-session, per-IP or per-address bucket; retryAfter). Retry-After is sent for the cooldown and rate-limit cases. | cooldown, send_limit_reached | application/json ErrorEnvelope |
502 | email_send_failed — the mail provider refused or failed; the send was rolled back. | email_send_failed | application/json ErrorEnvelope |
503 | hosted_verify_not_configured, session_backend_unavailable, or invalid_request (a transient lock or database fault); retry after the Retry-After seconds. | hosted_verify_not_configured, invalid_request, session_backend_unavailable | application/json ErrorEnvelope |
Response fields (200, application/json)
| Field | Type | Always returned | Description |
|---|---|---|---|
emailVerification | object (KycEmailVerificationState) | Yes | The customer-facing email-verification state returned by the hosted page’s email routes and embedded in the snapshot as emailVerification. The address is always masked (j***@example.com). |
emailVerification.mode | string | Yes | Allowed values: optional, required. |
emailVerification.email | string, nullable | Yes | The masked address; null before one was entered. |
emailVerification.verified | boolean | Yes | |
emailVerification.verifiedAt | integer (int64), nullable | Yes | |
emailVerification.expiresAt | integer (int64), nullable | Yes | Deadline of the live confirmation link (30 minutes or the session end, whichever is sooner); null when none is live. |
emailVerification.resendAvailableAt | integer (int64), nullable | Yes | Earliest next send while the 60 second cooldown runs; null when a resend is allowed now or the cap is exhausted. |
emailVerification.sendsRemaining | integer | Yes | Sends left of the 5 allowed per session. |
requestId | string | Yes |
Response example (200)
{
"emailVerification": {
"mode": "required",
"email": "j***@example.com",
"verified": false,
"verifiedAt": null,
"expiresAt": 1790764000000,
"resendAvailableAt": 1790762260000,
"sendsRemaining": 4
},
"requestId": "req-test"
}
Example request
curl
curl -X POST "https://collect.noxtica.com/v1/kyc/session-email/send" \
-H "Authorization: Bearer $NOXTICA_SESSION_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"email": "[email protected]",
"locale": "en"
}'
Node.js (global fetch, Node 18 or later)
const response = await fetch('https://collect.noxtica.com/v1/kyc/session-email/send', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.NOXTICA_SESSION_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
email: '[email protected]',
locale: 'en',
}),
});
console.log(response.status, await response.text());
Python (standard library)
import json
import os
import urllib.request
request = urllib.request.Request(
"https://collect.noxtica.com/v1/kyc/session-email/send",
data=json.dumps({
"email": "[email protected]",
"locale": "en",
}).encode(),
headers={
"Authorization": f"Bearer {os.environ['NOXTICA_SESSION_TOKEN']}",
"Content-Type": "application/json",
},
method="POST",
)
with urllib.request.urlopen(request) as response:
print(response.status, response.read().decode())
Poll the email-verification state (browser-facing)
GET /v1/kyc/session-email/status
Called by the hosted verification page while the customer is in their mail app; it builds no snapshot, just the emailVerification state (masked address). Same availability as POST /v1/kyc/session-email/send: the byte-identical gated 404 while the platform has the feature off, 409 email_verification_not_enabled for a session whose mode is off.
Authentication: Bearer token (SessionTokenAuth).
Responses
| Status | Meaning | Error codes | Content |
|---|---|---|---|
200 | The current state. | application/json KycEmailVerificationStateEnvelope | |
401 | invalid_session. | invalid_session | application/json ErrorEnvelope |
403 | Missing or disallowed Origin. | origin_not_allowed | application/json ErrorEnvelope |
404 | The byte-identical “V1 endpoint not found” response returned when the route’s module is not enabled for your organisation — indistinguishable from a route that does not exist (anti-enumeration by design; do not treat this as proof the path is wrong). | V1 endpoint not found | application/json |
409 | email_verification_not_enabled — the session’s effective mode is off. | email_verification_not_enabled | application/json ErrorEnvelope |
410 | The session can no longer be used by the hosted verification page, in one of two byte-stable shapes told apart by code. kyc_session_expired (error: session_expired) — the session is past its effective expiresAt (the original mint TTL while unopened, or openedAt plus the 30-minute capture window once opened) and never reached a terminal/completed state. kyc_session_cancelled (error: session_cancelled) — the end customer cancelled the session via POST /v1/kyc/session-cancel (terminal status cancelled_by_user). The hosted verification page MUST stop polling on either response. | kyc_session_cancelled, session_cancelled | application/json |
429 | Rate limit exceeded. Wait Retry-After seconds (also retryAfter in the body) before retrying; the response carries no other rate-limit headers. | Rate limit exceeded | application/json |
503 | session_backend_unavailable or invalid_request — transient; retry. | invalid_request, session_backend_unavailable | application/json ErrorEnvelope |
Response headers
| Status | Header | Type | Description |
|---|---|---|---|
429 | Retry-After | integer | Seconds until the bucket allows another request. |
Response fields (200, application/json)
| Field | Type | Always returned | Description |
|---|---|---|---|
emailVerification | object (KycEmailVerificationState) | Yes | The customer-facing email-verification state returned by the hosted page’s email routes and embedded in the snapshot as emailVerification. The address is always masked (j***@example.com). |
emailVerification.mode | string | Yes | Allowed values: optional, required. |
emailVerification.email | string, nullable | Yes | The masked address; null before one was entered. |
emailVerification.verified | boolean | Yes | |
emailVerification.verifiedAt | integer (int64), nullable | Yes | |
emailVerification.expiresAt | integer (int64), nullable | Yes | Deadline of the live confirmation link (30 minutes or the session end, whichever is sooner); null when none is live. |
emailVerification.resendAvailableAt | integer (int64), nullable | Yes | Earliest next send while the 60 second cooldown runs; null when a resend is allowed now or the cap is exhausted. |
emailVerification.sendsRemaining | integer | Yes | Sends left of the 5 allowed per session. |
requestId | string | Yes |
Response example (200)
{
"emailVerification": {
"mode": "optional",
"email": null,
"verified": false,
"verifiedAt": null,
"expiresAt": null,
"resendAvailableAt": null,
"sendsRemaining": 5
},
"requestId": "req-test"
}
Example request
curl
curl "https://collect.noxtica.com/v1/kyc/session-email/status" \
-H "Authorization: Bearer $NOXTICA_SESSION_TOKEN"
Node.js (global fetch, Node 18 or later)
const response = await fetch('https://collect.noxtica.com/v1/kyc/session-email/status', {
method: 'GET',
headers: {
Authorization: `Bearer ${process.env.NOXTICA_SESSION_TOKEN}`,
},
});
console.log(response.status, await response.text());
Python (standard library)
import os
import urllib.request
request = urllib.request.Request(
"https://collect.noxtica.com/v1/kyc/session-email/status",
headers={
"Authorization": f"Bearer {os.environ['NOXTICA_SESSION_TOKEN']}",
},
method="GET",
)
with urllib.request.urlopen(request) as response:
print(response.status, response.read().decode())
Confirm the emailed link (browser-facing, link token is the credential)
POST /v1/kyc/email-verification/confirm
Called by the page the emailed link opens, when the customer presses its one button. The link token (kyev.<tenant>.<secret>, 256-bit) is the only credential: no session bearer is read or accepted, and no session token is returned. Single use. A malformed, unknown, already used, expired, wrong-tenant or session-ended token all answer the identical 400 invalid_or_expired (no oracle). The Origin must be on the hosted-verification allow-list. Per client IP 30 confirmations per 10 minutes. Available only while hosted email verification is enabled for the platform; otherwise the byte-identical gated 404.
Authentication: none. This operation does not take a credential.
Request body
Content type application/json, required.
| Field | Type | Required | Description |
|---|---|---|---|
token | string | Yes | The link token, kyev.<tenant>.<secret>. At most 512 characters. |
Responses
| Status | Meaning | Error codes | Content |
|---|---|---|---|
200 | The address is now verified for that session. | application/json | |
400 | invalid_or_expired (every token problem), or invalid_request (unreadable body). | invalid_or_expired, invalid_request | application/json ErrorEnvelope |
403 | Missing or disallowed Origin. | origin_not_allowed | application/json ErrorEnvelope |
404 | The byte-identical “V1 endpoint not found” response returned when the route’s module is not enabled for your organisation — indistinguishable from a route that does not exist (anti-enumeration by design; do not treat this as proof the path is wrong). | V1 endpoint not found | application/json |
413 | body_too_large — the request exceeded this route’s byte cap. | body_too_large | application/json ErrorEnvelope |
429 | Rate limit exceeded. Wait Retry-After seconds (also retryAfter in the body) before retrying; the response carries no other rate-limit headers. | Rate limit exceeded | application/json |
503 | temporarily_unavailable — transient fault; retry after Retry-After seconds. | temporarily_unavailable | application/json ErrorEnvelope |
Response headers
| Status | Header | Type | Description |
|---|---|---|---|
429 | Retry-After | integer | Seconds until the bucket allows another request. |
Response fields (200, application/json)
| Field | Type | Always returned | Description |
|---|---|---|---|
ok | boolean | Yes | Allowed values: true. |
requestId | string | Yes |
Response example (200)
{
"ok": true,
"requestId": "req-test"
}
Example request
curl
curl -X POST "https://collect.noxtica.com/v1/kyc/email-verification/confirm" \
-H "Content-Type: application/json" \
-d '{
"token": "kyev.tenant_42.q3JkT0vN8mYb1xW6cE4aZ7uP2sH9dLfR5gKiA0oXyUw"
}'
Node.js (global fetch, Node 18 or later)
const response = await fetch('https://collect.noxtica.com/v1/kyc/email-verification/confirm', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
token: 'kyev.tenant_42.q3JkT0vN8mYb1xW6cE4aZ7uP2sH9dLfR5gKiA0oXyUw',
}),
});
console.log(response.status, await response.text());
Python (standard library)
import json
import urllib.request
request = urllib.request.Request(
"https://collect.noxtica.com/v1/kyc/email-verification/confirm",
data=json.dumps({
"token": "kyev.tenant_42.q3JkT0vN8mYb1xW6cE4aZ7uP2sH9dLfR5gKiA0oXyUw",
}).encode(),
headers={
"Content-Type": "application/json",
},
method="POST",
)
with urllib.request.urlopen(request) as response:
print(response.status, response.read().decode())
Read the public status of a Noxtica Verification ID (public, no authentication)
GET /v1/kyc/verification-status
Public lookup behind the status link https://verify.<env>/v/<NVID> that a person can share. No bearer token. Returns only the status of the ID, its dates and the display name of the business that requested the verification — never a name, document data, image, email address or tenant identifier. An NVID confirms status for a business, not identity, and is not a credential: it cannot be used to sign in or to skip a verification.
status is revoked (a Backoffice revocation; wins over everything), pending (the verification awaits a review decision), expired (365 days after completion) or verified. An unknown, erased or malformed ID — including one whose check symbol is wrong — answers the same 404 not_found (no existence oracle). The Origin must be on the hosted-verification allow-list (403 origin_not_allowed); per-IP limit 60 requests per 60 seconds (429). Responses are Cache-Control: no-store. Only available while the NVID feature is enabled for the platform; otherwise the byte-identical gated 404.
Authentication: none. This operation does not take a credential.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | query | string | Yes | The NVID, canonical form NVID-XXXX-XXXX-XXXX-XXXX (the collector also accepts the lower-case, unhyphenated and O/I/L-confusable spellings). |
Responses
| Status | Meaning | Error codes | Content |
|---|---|---|---|
200 | The ID’s public status. | application/json KycVerificationIdStatusResponse | |
403 | Missing or disallowed Origin. | origin_not_allowed | application/json ErrorEnvelope |
404 | not_found — unknown, erased or malformed ID (identical bodies), or the feature is not enabled for the platform (the gated 404 shape). | not_found | application/json |
429 | Per-IP limit exceeded (60 requests per 60 seconds). Retry-After carries the seconds. | rate_limited | application/json |
503 | Temporarily unavailable; retry after Retry-After seconds (2). | temporarily_unavailable | application/json |
Response headers
| Status | Header | Type | Description |
|---|---|---|---|
200 | Cache-Control | string | |
429 | Retry-After | integer | |
503 | Retry-After | integer |
Response fields (200, application/json)
| Field | Type | Always returned | Description |
|---|---|---|---|
ok | boolean | Yes | Allowed values: true. |
verification | object | Yes | |
verification.id | string | Yes | Canonical NVID. |
verification.status | string | Yes | Public status of a Noxtica Verification ID. revoked wins over everything; pending = the verification awaits a review decision; expired = 365 days after completion; otherwise verified. Allowed values: verified, pending, revoked, expired. |
verification.verifiedAt | integer (int64), nullable | Yes | Unix ms of completion; null while pending. |
verification.expiresAt | integer (int64), nullable | Yes | Unix ms, completion + 365 days; null while pending. |
verification.revokedAt | integer (int64), nullable | Yes | Unix ms of the revocation; null unless revoked. |
verification.requestedBy | string, nullable | Yes | The requesting business’s display name; null when none is configured. |
Response example (200)
{
"ok": true,
"verification": {
"id": "NVID-7K3M-Q9XA-T2RW-4HZD",
"status": "verified",
"verifiedAt": 1790762445000,
"expiresAt": 1822298445000,
"revokedAt": null,
"requestedBy": "Example Bank"
}
}
Example request
curl
curl "https://collect.noxtica.com/v1/kyc/verification-status?id=NVID-7K3M-Q9XA-T2RW-4HZD"
Node.js (global fetch, Node 18 or later)
const response = await fetch('https://collect.noxtica.com/v1/kyc/verification-status?id=NVID-7K3M-Q9XA-T2RW-4HZD', {
method: 'GET',
});
console.log(response.status, await response.text());
Python (standard library)
import urllib.request
request = urllib.request.Request(
"https://collect.noxtica.com/v1/kyc/verification-status?id=NVID-7K3M-Q9XA-T2RW-4HZD",
method="GET",
)
with urllib.request.urlopen(request) as response:
print(response.status, response.read().decode())
Mint an evidence-upload handle (browser-facing)
POST /v1/kyc/session-evidence/handle
Token-authenticated sibling of POST /v1/kyc/evidence/handle, delegating to the same evidence pipeline — no parallel evidence store.
Authentication: Bearer token (SessionTokenAuth).
Request body
Content type application/json, required.
Type: object.
Responses
| Status | Meaning | Error codes | Content |
|---|---|---|---|
200 | Handle minted. | application/json | |
400 | The request failed validation. error is a route-specific code such as invalid_request; read the operation description for the exact values it can return. | invalid_request | application/json ErrorEnvelope |
403 | Missing/disallowed Origin, an invalid/expired session token, device_upload_disabled, or evidence_refused with a closed refusal: evidence_not_provisioned (the organisation’s raw-evidence storage was never armed), residency_unconfigured (armed, but no evidence residency of TR, EU or US), residency_refused (the residency route refused the data class and jurisdiction pair, the only true region refusal), binding_unprovisioned, evidence_storage_disabled or verification_not_admitted. Treat an unknown refusal as a generic unavailable answer; never as a regional restriction. | evidence_refused | application/json KycSessionEvidenceError |
404 | The byte-identical “V1 endpoint not found” response returned when the route’s module is not enabled for your organisation — indistinguishable from a route that does not exist (anti-enumeration by design; do not treat this as proof the path is wrong). | V1 endpoint not found | application/json |
410 | The session can no longer be used by the hosted verification page, in one of two byte-stable shapes told apart by code. kyc_session_expired (error: session_expired) — the session is past its effective expiresAt (the original mint TTL while unopened, or openedAt plus the 30-minute capture window once opened) and never reached a terminal/completed state. kyc_session_cancelled (error: session_cancelled) — the end customer cancelled the session via POST /v1/kyc/session-cancel (terminal status cancelled_by_user). The hosted verification page MUST stop polling on either response. | kyc_session_cancelled, session_cancelled | application/json |
429 | Rate limit exceeded. Wait Retry-After seconds (also retryAfter in the body) before retrying; the response carries no other rate-limit headers. | Rate limit exceeded | application/json |
Response headers
| Status | Header | Type | Description |
|---|---|---|---|
429 | Retry-After | integer | Seconds until the bucket allows another request. |
Response fields (200, application/json)
Type: object.
Example request
curl
curl -X POST "https://collect.noxtica.com/v1/kyc/session-evidence/handle" \
-H "Authorization: Bearer $NOXTICA_SESSION_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
Node.js (global fetch, Node 18 or later)
const response = await fetch('https://collect.noxtica.com/v1/kyc/session-evidence/handle', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.NOXTICA_SESSION_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({}),
});
console.log(response.status, await response.text());
Python (standard library)
import json
import os
import urllib.request
request = urllib.request.Request(
"https://collect.noxtica.com/v1/kyc/session-evidence/handle",
data=json.dumps({}).encode(),
headers={
"Authorization": f"Bearer {os.environ['NOXTICA_SESSION_TOKEN']}",
"Content-Type": "application/json",
},
method="POST",
)
with urllib.request.urlopen(request) as response:
print(response.status, response.read().decode())
Upload sealed ciphertext and finalize (browser-facing)
POST /v1/kyc/session-evidence/finalize
Token-authenticated sibling of POST /v1/kyc/evidence/finalize.
Authentication: Bearer token (SessionTokenAuth).
Request body
Content type application/json, required.
Type: object.
Responses
| Status | Meaning | Error codes | Content |
|---|---|---|---|
200 | Terminal inspection outcome. | application/json | |
400 | The request failed validation. error is a route-specific code such as invalid_request; read the operation description for the exact values it can return. | invalid_request | application/json ErrorEnvelope |
403 | Missing/disallowed Origin, an invalid/expired session token, or a failed handle verification. | invalid_request | application/json ErrorEnvelope |
404 | The byte-identical “V1 endpoint not found” response returned when the route’s module is not enabled for your organisation — indistinguishable from a route that does not exist (anti-enumeration by design; do not treat this as proof the path is wrong). | V1 endpoint not found | application/json |
410 | The session can no longer be used by the hosted verification page, in one of two byte-stable shapes told apart by code. kyc_session_expired (error: session_expired) — the session is past its effective expiresAt (the original mint TTL while unopened, or openedAt plus the 30-minute capture window once opened) and never reached a terminal/completed state. kyc_session_cancelled (error: session_cancelled) — the end customer cancelled the session via POST /v1/kyc/session-cancel (terminal status cancelled_by_user). The hosted verification page MUST stop polling on either response. | kyc_session_cancelled, session_cancelled | application/json |
429 | Rate limit exceeded. Wait Retry-After seconds (also retryAfter in the body) before retrying; the response carries no other rate-limit headers. | Rate limit exceeded | application/json |
Response headers
| Status | Header | Type | Description |
|---|---|---|---|
429 | Retry-After | integer | Seconds until the bucket allows another request. |
Response fields (200, application/json)
Type: object.
Example request
curl
curl -X POST "https://collect.noxtica.com/v1/kyc/session-evidence/finalize" \
-H "Authorization: Bearer $NOXTICA_SESSION_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
Node.js (global fetch, Node 18 or later)
const response = await fetch('https://collect.noxtica.com/v1/kyc/session-evidence/finalize', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.NOXTICA_SESSION_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({}),
});
console.log(response.status, await response.text());
Python (standard library)
import json
import os
import urllib.request
request = urllib.request.Request(
"https://collect.noxtica.com/v1/kyc/session-evidence/finalize",
data=json.dumps({}).encode(),
headers={
"Authorization": f"Bearer {os.environ['NOXTICA_SESSION_TOKEN']}",
"Content-Type": "application/json",
},
method="POST",
)
with urllib.request.urlopen(request) as response:
print(response.status, response.read().decode())
Create a right-to-erasure request for an attested subject
POST /v1/kyc/erasure/self-request
Device-callable FR-15 erasure intake. The subject is resolved server-side from an opaque attestationId — a caller can never supply a subjectId directly (no cross-subject erasure oracle). Always resolves 200; a resolution miss, an inactive subject, or an infra fault all collapse to the SAME {ok:true, requested:false, status:'subject_not_attested'} shape.
Authentication: Bearer token (ApiKeyAuth), scope write:identity.
Request body
Content type application/json, required.
| Field | Type | Required | Description |
|---|---|---|---|
attestationId | string | Yes |
Responses
| Status | Meaning | Error codes | Content |
|---|---|---|---|
200 | Always 200 (fail-closed discipline). | application/json | |
401 | The Authorization: Bearer sk_... header is missing or malformed, or the key is unknown or revoked. The body is deliberately identical for every one of those causes. | invalid_api_key | application/json KycSessionError |
403 | The presented key lacks the required scope. | API key does not have write:identity scope | application/json |
404 | The byte-identical “V1 endpoint not found” response returned when the route’s module is not enabled for your organisation — indistinguishable from a route that does not exist (anti-enumeration by design; do not treat this as proof the path is wrong). | V1 endpoint not found | application/json |
429 | Rate limit exceeded. Wait Retry-After seconds (also retryAfter in the body) before retrying; the response carries no other rate-limit headers. | Rate limit exceeded | application/json |
503 | Noxtica could not read the key’s record. This is a temporary fault on Noxtica’s side and says nothing about your key. Retry after the Retry-After delay; do not treat it as a revoked key. | api_key_auth_unavailable | application/json |
Response headers
| Status | Header | Type | Description |
|---|---|---|---|
429 | Retry-After | integer | Seconds until the bucket allows another request. |
503 | Retry-After | integer | Seconds to wait before retrying. |
Response fields (200, application/json)
| Field | Type | Always returned | Description |
|---|---|---|---|
ok | boolean | No | Allowed values: true. |
requested | boolean | No | |
status | string | No | Allowed values: subject_not_attested, requested, erasure_request_failed. |
requestId | string | No |
Response example (200)
{
"ok": true,
"requested": true,
"status": "subject_not_attested",
"requestId": "req-test"
}
Example request
curl
curl -X POST "https://collect.noxtica.com/v1/kyc/erasure/self-request" \
-H "Authorization: Bearer $NOXTICA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"attestationId": "att_9c6b2f7e4a3d4c8e9f1a2b3c4d5e6f70"
}'
Node.js (global fetch, Node 18 or later)
const response = await fetch('https://collect.noxtica.com/v1/kyc/erasure/self-request', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.NOXTICA_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
attestationId: 'att_9c6b2f7e4a3d4c8e9f1a2b3c4d5e6f70',
}),
});
console.log(response.status, await response.text());
Python (standard library)
import json
import os
import urllib.request
request = urllib.request.Request(
"https://collect.noxtica.com/v1/kyc/erasure/self-request",
data=json.dumps({
"attestationId": "att_9c6b2f7e4a3d4c8e9f1a2b3c4d5e6f70",
}).encode(),
headers={
"Authorization": f"Bearer {os.environ['NOXTICA_API_KEY']}",
"Content-Type": "application/json",
},
method="POST",
)
with urllib.request.urlopen(request) as response:
print(response.status, response.read().decode())
Transactionally activate a proofed-unbound mobile capability
POST /v1/kyc/activate
Composes a signed attestation projection, an atomic edge consent receipt, and a typed device-identity proof into one transactional activation. Two opaque references only (attestationId, receiptId) — never a subjectId or tenantId (both are resolved server-side). Available only when mobile activation is enabled for your organisation (a separate enablement from /v1/kyc/verify); otherwise the route answers the GatedNotFound 404.
Authentication: Bearer token (ApiKeyAuth), scope write:identity.
Request body
Content type application/json, required.
| Field | Type | Required | Description |
|---|---|---|---|
attestationId | string | Yes | |
receiptId | string | Yes |
Responses
| Status | Meaning | Error codes | Content |
|---|---|---|---|
200 | Always 200 (fail-closed discipline) — a false activated:true is never returned. | application/json | |
401 | The Authorization: Bearer sk_... header is missing or malformed, or the key is unknown or revoked. The body is deliberately identical for every one of those causes. | invalid_api_key | application/json KycSessionError |
403 | The presented key lacks the required scope. | API key does not have write:identity scope | application/json |
404 | The byte-identical “V1 endpoint not found” response returned when the route’s module is not enabled for your organisation — indistinguishable from a route that does not exist (anti-enumeration by design; do not treat this as proof the path is wrong). | V1 endpoint not found | application/json |
429 | Rate limit exceeded. Wait Retry-After seconds (also retryAfter in the body) before retrying; the response carries no other rate-limit headers. | Rate limit exceeded | application/json |
503 | Noxtica could not read the key’s record. This is a temporary fault on Noxtica’s side and says nothing about your key. Retry after the Retry-After delay; do not treat it as a revoked key. | api_key_auth_unavailable | application/json |
Response headers
| Status | Header | Type | Description |
|---|---|---|---|
429 | Retry-After | integer | Seconds until the bucket allows another request. |
503 | Retry-After | integer | Seconds to wait before retrying. |
Response fields (200, application/json)
| Field | Type | Always returned | Description |
|---|---|---|---|
ok | boolean | No | Allowed values: true. |
activated | boolean | No | |
reason | string | No | Present when activated is false (e.g. an unresolvable/foreign/stale attestation or receipt). |
capability | object | No | Present only when activated is true. |
capability.proofStatus | string | No | Allowed values: proofed_unbound. |
capability.verificationLevel | string | No | Allowed values: verified, partial, unverified, inconclusive. |
capability.receiptId | string | No | |
capability.activatedAt | integer | No | |
requestId | string | No |
Response example (200)
{
"ok": true,
"activated": true,
"reason": "user_requested",
"capability": {
"proofStatus": "proofed_unbound",
"verificationLevel": "verified",
"receiptId": "receipt_2f6a8b3c9d4e5f60718293a4b5c6d7e8",
"activatedAt": 1
},
"requestId": "req-test"
}
Example request
curl
curl -X POST "https://collect.noxtica.com/v1/kyc/activate" \
-H "Authorization: Bearer $NOXTICA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"attestationId": "att_9c6b2f7e4a3d4c8e9f1a2b3c4d5e6f70",
"receiptId": "receipt_2f6a8b3c9d4e5f60718293a4b5c6d7e8"
}'
Node.js (global fetch, Node 18 or later)
const response = await fetch('https://collect.noxtica.com/v1/kyc/activate', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.NOXTICA_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
attestationId: 'att_9c6b2f7e4a3d4c8e9f1a2b3c4d5e6f70',
receiptId: 'receipt_2f6a8b3c9d4e5f60718293a4b5c6d7e8',
}),
});
console.log(response.status, await response.text());
Python (standard library)
import json
import os
import urllib.request
request = urllib.request.Request(
"https://collect.noxtica.com/v1/kyc/activate",
data=json.dumps({
"attestationId": "att_9c6b2f7e4a3d4c8e9f1a2b3c4d5e6f70",
"receiptId": "receipt_2f6a8b3c9d4e5f60718293a4b5c6d7e8",
}).encode(),
headers={
"Authorization": f"Bearer {os.environ['NOXTICA_API_KEY']}",
"Content-Type": "application/json",
},
method="POST",
)
with urllib.request.urlopen(request) as response:
print(response.status, response.read().decode())
Read the identity details of a verification
GET /v1/kyc/verifications/{verificationId}/identity
Server-to-server. Returns the identity details Noxtica extracted from the document captured for one of your own verifications: the document type, number and expiry, the holder’s names, date of birth, sex and nationality, the machine-readable-zone check results, and a summary of the verification checks. It never returns images or biometric data.
Opt-in. The key needs the read:identity_details scope, and the route answers
403 identity_read_not_enabled until a tenant administrator enables identity details
access for your organisation in the Backoffice (KYC settings). A key keeps its scopes
until a tenant administrator changes them (Edit scopes on the key in the Backoffice, API
Configuration): add read:identity_details to the key, or create a new key that carries it.
Which id. verificationId is the id returned by POST /v1/kyc/verify (a UUID) or the
verificationId of a hosted session (hosted: followed by 64 lower-case hexadecimal
characters); see KycVerificationId for the formats and the 128-character maximum. The id
of a hosted session and the evidenceGroupId of a verification made from your own capture
screens are accepted too and resolve to the same verification. An id that is malformed,
unknown, or belongs to another organisation gets the same byte-identical 404 as an
unknown path.
Outcome and review. verification.status is verified, unverified, failed,
error or pending, and verification.source says who produced it: pipeline is the
automated verdict, review means a reviewer’s approve decision was applied over it (the
same outcome the hosted session status read reports). verification.review is null while
no reviewer has acted, otherwise {state, decision, version, decidedAt} (see
KycTenantReview). verification.level is the automated verification level and a review
does not change it. A verification you created through the API has no session, so this
read is where you see what a reviewer decided about it.
Review decision event. A reviewer’s approve or override on a verification is announced by the kyc.review.decided event; read this route again after it arrives to see the resulting verification and review. A decision recorded by your own back office through POST /v1/kyc/verifications/{verificationId}/review-decision is announced in the same way.
Selected and detected document type. document.selectedType is the document type the
person chose in the hosted flow (passport, national_id, driving_license or
residence_permit), or null for a verification created through the API or a session
that recorded no choice. document.type is the document type the stored extraction is
for. Compare the two: they differ when the extraction ran against another document type
than the person presented.
Extraction: what was read, and why not. extraction answers, field by field, whether the
identity fields a relying party needs were read from the document, so a null in
person or document.number always comes with a reason instead of an empty value. It is
present on every 200. Which of the five fields are required is yours to set (see “Required
identity fields” below); a field that is not required is still reported, with its reason.
| Key | Values |
|---|---|
extraction.status | Computed over the required fields only. complete: every required field was read (also when none is required). partial: some were. unreadable: none was. |
extraction.fields[] | Always five entries, in this order: person.surname, person.givenNames, person.dateOfBirth, person.personalNumber, document.number. Each has status, reason, source and required; person.surname and person.givenNames also carry foldedValue and, when they apply, uncertainty, uncertainPositions, alternatives, script and latinForm. |
fields[].status | read: the field has a value. It is released, or withheld by the masking rules below, so read does not mean the value is in this response. unreadable: no value, see reason. not_expected: this document does not carry the field and your configuration does not require it. person.personalNumber is expected on a Turkish (TR) national ID card or driving licence, where it is the T.C. Kimlik No. |
fields[].required | true when the field counts towards extraction.status and identity_fields_unreadable; false when it is reported but does not. An unreadable field with required: false keeps its reason and does not lower extraction.status. |
fields[].reason | Only with unreadable. label_not_found: the printed label or numbered marker was not found in the recognised text. value_not_found: the label was found, with no value beside or below it. value_invalid: a value was found that is not a valid name, date or number. checksum_invalid: the personal number was read but its check digits fail; the value is withheld and never returned. layout_unrecognised: the front of the document matched no known layout, or nothing was recognised. low_confidence: a date was read below the recognition confidence floor and is withheld. not_recorded: the record was written before reasons were kept. |
fields[].source | Where the value came from: viz (the printed text of the document), mrz or barcode. null unless status is read. |
fields[].uncertainty | Only on person.surname and person.givenNames, and only when it applies: the key is absent otherwise. A list of advisory caveats on a read value. diacritic_uncertain: the value is the best reading, but a letter in it may carry a mark that the reading did not show, or the reverse: a Turkish Ş shown as S, a Polish Ł as L. It is set when the reader saw the marked and the plain letter as close candidates, when two readers of the printed text differed only in a mark, or when the value comes from the machine-readable zone or a barcode, which cannot print marks. mrz_transliteration_conflict: the printed name and the name in the machine-readable zone fit together only if a plain letter of the printed name carries a mark (printed MULLER, zone MUELLER); it can come alone or together with diacritic_uncertain. Both are advisory: status stays read, and they change neither extraction.status nor the verdict. What to do with each is in “Names, diacritics and alphabets” below. |
fields[].uncertainPositions | Only with diacritic_uncertain. The 0-based Unicode code point positions in the name (person.surname or person.givenNames, spaces counted), ascending, of the letters that may carry a different mark. For a name read from the printed text these are the letters the reader had doubt about; for a name taken from the machine-readable zone or a barcode they are every letter that has a marked form in the alphabet of the issuing country (for TR: C, G, I, O, S, U). |
fields[].alternatives | Up to four complete spellings of the name, most likely first, made only by swapping the flagged letters for their marked or plain forms. None equals the value and none repeats. With mrz_transliteration_conflict the list includes the spelling the machine-readable zone points to. Absent when there is no reading-based doubt, in particular for a name taken from the machine-readable zone or a barcode: that source cannot show a mark, so there is nothing to rank. The value is never replaced by an alternative. |
fields[].foldedValue | On every read person.surname and person.givenNames, with or without an uncertainty: the value with its marks removed as ICAO Doc 9303 Part 3 section 6.A lists them (one base letter such as Ş to S, İ to I, Ä to A, Ñ to N; the expansions ß to SS, Æ to AE, Œ to OE, Ø to OE, Þ to TH, IJ to IJ). The case of the value is kept, and it equals the value when no letter folds. It is a key for tolerant comparison, not a spelling of the name to store. |
fields[].script | The writing system the name was read in, as an ISO 15924 code: Latn, Cyrl, Grek, Arab, Hebr, Geor, Armn, Deva, Thai, Hani, Jpan or Kore. Absent when it was not determined. |
fields[].latinForm | {value, source}: a Latin-letter form of the name when one is available. source is viz (printed on the document) or mrz (the name in the machine-readable zone, which uses the ICAO transliteration). Absent when there is none. |
extraction.reasonCodes | identity_fields_unreadable when a required field is unreadable. document_type_mismatch_selection when documentType.contradictsSelection is true. issuing_country_mismatch_selection when issuingCountry.contradictsSelection is true. diacritic_uncertain or mrz_transliteration_conflict when person.surname or person.givenNames carries that uncertainty. An empty array when none applies. The first three codes are also added to the reasonCodes of the verification and of the session; the two name codes appear only here. All are informational and change neither the verdict nor the retake prompts. |
extraction.documentType | selected: the type the person chose (the same value as document.selectedType). classified: the type the classifier named, only when it is confident (0.72 or higher), otherwise null. contradictsSelection: true when the classified type is a different identity document type than the selection, false when they agree, null when they cannot be compared. |
extraction.issuingCountry | selected: the country the person chose in the hosted flow (ISO 3166-1 alpha-2), or null. detected: the country the document shows, or null when nothing readable named one. source: mrz (the issuing state of the machine-readable zone, used only when none of its check digits failed) or viz (the printed text), null exactly when detected is null. contradictsSelection: true when both are known and differ, false when they are equal, null when they cannot be compared; an unknown is never reported as a contradiction. |
Required identity fields. By default person.surname, person.givenNames,
person.dateOfBirth and document.number are required, and so is person.personalNumber
on a Turkish national ID card or driving licence. A document counts as Turkish when it shows
Türkiye (document.issuingCountry, see extraction.issuingCountry), or, when no country could be read
from it, when the person selected Türkiye. For those two Turkish document types one
rule relaxes this: when the T.C. Kimlik No was read and passes its check digits,
document.number is not required, so an unreadable document number is still reported (with
its reason and required: false) but no longer raises identity_fields_unreadable. You can set the list per document type
with requiredIdentityFields on documents in your hosted configuration (organisation
default or one domain; see GET /v1/kyc/config). A non-empty list is exactly the set that is
required, replaces the Turkish rule, and may name personalNumber for any country; an empty
list requires nothing. The list that applied is recorded with the verification. A verification
made before this was recorded is read with the default rules at read time, while its stored
session reasonCodes stay as they were recorded.
identity_fields_unreadable is informational. It does not decide whether a session is reviewed:
every hosted verification currently ends unverified with provider_manual_review until a
reviewer decides, whatever the extraction says.
Selected and detected issuing country. extraction.issuingCountry compares the country
the person selected with the country the document shows. Only Türkiye can be recognised from
printed text (the Turkish country heading, or a Turkish layout with a T.C. Kimlik No that
passes its check digits); for any other country detected is set only when the
machine-readable zone names it. The words of a document alone, such as “SÜRÜCÜ BELGESİ”, are not
evidence of a country. A dependent territory selected against its sovereign state (for example
GF and FR) is not a contradiction. document.issuingCountry is the country the document shows
when one was read, otherwise the country the person selected, otherwise null. The hosted page
starts with the first country of the list selected, so narrow documents[].issuingCountries
in your hosted configuration (or hostedOptions.allowedIssuingCountries on the invitation) to
the countries you accept; an example of a contradiction is
{selected: AD, detected: TR, source: viz, contradictsSelection: true}.
A personal number is returned (when fields names personalNumber) only if it passes the
official T.C. Kimlik No check-digit rule; a number that fails it is null and its field
reads unreadable with checksum_invalid. Dates are validated against the calendar before
they are returned. A value that could not be validated is reported unreadable, never
guessed.
Names, diacritics and alphabets. person.surname and person.givenNames hold the
name as the document prints it; on a document where the printed text and the
machine-readable zone name the same person, the printed spelling is used. A text reader can
show a letter with a mark as its plain form (a Turkish Ş as S, a German Ü as U) or
the other way round, a letter can occasionally be missing, and the machine-readable zone
cannot print marks at all. The name fields therefore carry advisory data next to the value
and never instead of it: the value is not changed to a letter the printed read did not
produce, and every other spelling is offered as an alternative for you to check.
| You see | It means | What to do |
|---|---|---|
No uncertainty | The reader found no doubt on this name. This is not a confirmation of each letter. | Compare tolerantly (below) and keep a review path for a mismatch. |
diacritic_uncertain with alternatives | The name was read from the printed text and the letters in uncertainPositions may carry a different mark. | Check the value against the registry first, then each entry of alternatives in order, then foldedValue against registries that compare folded names only. If none matches, ask for a new capture or send the case to manual review. |
diacritic_uncertain without alternatives | The name comes from the machine-readable zone or a barcode, which cannot show marks. uncertainPositions lists the letters that could have one; nothing says which. | Do not make up spellings. Compare foldedValue with registries that compare folded names, otherwise ask the person to confirm the spelling, ask for a capture of the printed side, or send the case to manual review. |
foldedValue | The value with marks removed. Present on every read name. | Compare the foldedValue of both sides when you want a tolerant match (below). Do not store it as the name. |
mrz_transliteration_conflict | The printed name and the machine-readable zone do not fit together unless a plain letter of the printed name carries a mark. alternatives holds that spelling. | Check the spelling from alternatives against the registry before foldedValue; if it matches, have the person or a reviewer confirm it. If nothing matches, ask for a new capture or send the case to manual review. |
What the machine-readable zone can and cannot show. The zone holds the letters A to
Z, digits and < only. ICAO Doc 9303 Part 3 section 6.A lists how an issuing state
writes other letters there: Ä as AE or A, Ö as OE or O, Ü as UE, UXX or
U, Å as AA or A, Ø as OE, Æ as AE, ß as SS, Ñ as N or NXX, Þ as
TH. Turkish letters have no such expansion: Ş, Ç, Ğ, İ, Ö and Ü always become
S, C, G, I, O and U, so a Turkish zone cannot confirm which letter was printed
(SAHIN fits ŞAHİN, SAHİN and SAHIN). A zone that uses an expansion can: printed
MÜLLER with zone MUELLER agrees only when the printed letter is Ü, so that letter is
dropped from uncertainPositions. Printed MULLER with zone MUELLER is a conflict: the
zone spelling needs a mark that the printed reading lacks, and
mrz_transliteration_conflict is set. An ordinary difference between the two names is not
a conflict and is not flagged here.
What a missing flag means, and how often a flag is right. A name without uncertainty
means that the reader found no doubt on it. It is not a confirmation of each letter, and
the name can still contain a wrong one: measured on synthetic test documents, many wrong
letters are read with full confidence and carry no flag. For the printed name on Turkish
national ID card and driving licence fronts, the flag appears on roughly one in ten names.
About one in three flagged names is actually wrong (the others are already right), and for
most wrong flagged names the true spelling is among alternatives. So treat a flag as a
reason to try the alternatives, not as a verdict, and keep the tolerant comparison and the
review path for a mismatch whether the flag is present or not.
Check a name against a registry (Turkish national registry example).
- Always send the
valuefirst, upper-cased with Turkish rules (itoİ,ıtoI); a plain upper-case turnsiintoIand breaks the match. - On a mismatch while
diacritic_uncertainis present, send each entry ofalternativesin order, upper-cased the same way. - If nothing matches, ask the person for a new capture or send the case to manual review.
A registry that compares folded names can be sent
foldedValueinstead of steps 1 and 2. - Do not write an alternative or the folded value into your records as the legal name without a confirmation by the person, a reviewer or the registry answer.
Tolerant matching. To compare a name with a name you hold without a registry, fold both
sides the same way and compare the folded strings: use foldedValue for the name read from
the document and apply the same ICAO folding (and one case convention, for example Turkish
upper case) to your own value. A match of the folded strings says the two names agree up to
marks; it does not say which marks are right, so keep the review path for names that matter.
Scope. The fields are optional and additive: a verification recorded before this
release can lack alternatives, uncertainPositions, script, latinForm and
mrz_transliteration_conflict, and a finished verification is not read again, so treat each
of them as optional. Doubts about the printed read, and so alternatives, exist for the
Turkish national ID card and driving licence layouts. Flags for a name taken from the
machine-readable zone or a barcode exist for the countries whose alphabet Noxtica lists,
for example TR, DE, AT, LI and PL; foldedValue is on every read name. For a name
in a non-Latin script, script says which one and latinForm gives a Latin form when
there is one; the value stays as read.
Turkish driving licence number. The document number of a Turkish driving licence is the
value of field 5. on the front: six digits, with leading zeros kept; it has no check digit
that could be verified. Spaces, dots and stray marks inside the number (123 456,
12|3456) are dropped. A letter read in place of a look-alike digit (for example O for
0 or I for 1) is corrected only when the value has six characters and at least five
of them are digits; a value of any other shape (six to twelve letters and digits with at
least one digit, such as the older A123456 form) is accepted exactly as read and is never
repaired. A number that is not printed, or that cannot be recognised, stays unreadable
(value_not_found or value_invalid) and is never guessed. Unless you list documentNumber
in requiredIdentityFields, an unreadable licence number does not raise
identity_fields_unreadable while the T.C. Kimlik No of the licence was read.
Check results. checks.documentAuthenticity, checks.faceMatch, checks.liveness and
checks.selfie are verdicts (pass, review, fail, skipped, pending, or null when
no automated result was recorded for that check). They are evidence for review and for your
own risk rules, not a decision. What each check measures, the reason codes behind it and the
publishable thresholds are described under “Check results” on
GET /v1/kyc/sessions/{sessionId}. Why liveness and selfie read at best review is
explained on Liveness & PAD policy.
Masked by default. Names, date of birth, expiry date, nationality, sex, issuing
country, ID type and document type are returned in full. The document number is returned
masked (its last characters only), and the personal number and the raw
machine-readable-zone lines are left out. Name them in fields (comma separated) to
receive them: documentNumber, personalNumber, mrzLines. Every masked or missing
item is listed in redactions with the fields value that returns it; data Noxtica does
not extract (the address today) is listed with the reason not_extracted. A person.surname,
person.givenNames or person.dateOfBirth that is null is listed with the reason
not_available. An unknown fields value is a 400 invalid_fields that lists the accepted
values.
Availability. Details exist once a verification has finished and its fields were
extracted. A verification without stored details answers 409 identity_not_available
with reason not_completed (not finished yet) or not_extracted (nothing was
extracted or stored: evidence storage is not enabled for your organisation, or the
verification predates extraction). A verification you deleted answers
410 verification_deleted.
Audit and handling. Each successful read is recorded for your organisation with the
key that made it and the names of the fields returned, never their values. Every response
of this route carries Cache-Control: no-store, private: do not cache or log the body.
Identity details are never sent in webhooks.
Rate limit. A per-key limit applies to the identity details routes together; a
limited request answers 429 with Retry-After.
Authentication: Bearer token (ApiKeyAuth), scope read:identity_details.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
verificationId | path | string | Yes | The verificationId of the verification. Pattern ^[A-Za-z0-9:_-]{1,128}$. |
fields | query | array of string | No | Comma-separated list of withheld values to include: documentNumber (the full document number), personalNumber, mrzLines (the raw machine-readable-zone lines). Omit it for the masked default. |
Responses
| Status | Meaning | Error codes | Content |
|---|---|---|---|
200 | The identity details of the verification. | application/json KycVerificationIdentity | |
400 | invalid_fields or invalid_request — fields names a value this route does not accept (allowed lists the accepted values), or the request could not be parsed. Nothing was read. | invalid_fields, invalid_request | application/json IdentityInvalidFieldsError |
401 | The Authorization: Bearer sk_... header is missing or malformed, or the key is unknown or revoked. The body is deliberately identical for every one of those causes. | invalid_api_key | application/json KycSessionError |
403 | identity_read_not_enabled — a tenant administrator has not enabled identity details access for your organisation, so nothing was read; or the key lacks the read:identity_details scope (error is API key does not have read:identity_details scope). | identity_read_not_enabled | application/json IdentityForbiddenError |
404 | The byte-identical “V1 endpoint not found” response, returned for a malformed id, an id that does not exist, an id that belongs to another organisation, or a route that is not available for your organisation — indistinguishable from an unknown path by design. | V1 endpoint not found | application/json |
409 | identity_not_available — the verification exists but has no stored identity details. reason is not_completed (it has not finished) or not_extracted (nothing was extracted or stored for it). | identity_not_available | application/json IdentityNotAvailableError |
410 | verification_deleted — you deleted this verification; its details no longer exist. | verification_deleted | application/json ErrorEnvelope |
429 | Rate limit exceeded. Wait Retry-After seconds (also retryAfter in the body) before retrying; the response carries no other rate-limit headers. | Rate limit exceeded | application/json |
500 | API-key authentication is not configured on this deployment (server_misconfigured). Retrying does not help; contact support with the requestId. | server_misconfigured | application/json KycSessionError |
503 | identity_audit_unavailable or identity_unavailable — retryable. The access could not be recorded, so nothing was released; or the stored details could not be read right now. | identity_audit_unavailable, identity_unavailable | application/json ErrorEnvelope |
503 | Noxtica could not read the key’s record. This is a temporary fault on Noxtica’s side and says nothing about your key. Retry after the Retry-After delay; do not treat it as a revoked key. | api_key_auth_unavailable | application/json |
Response headers
| Status | Header | Type | Description |
|---|---|---|---|
200 | Cache-Control | string | Always no-store, private. Never cache or store a response of an identity details route or of the review decision route. |
400 | Cache-Control | string | Always no-store, private. Never cache or store a response of an identity details route or of the review decision route. |
403 | Cache-Control | string | Always no-store, private. Never cache or store a response of an identity details route or of the review decision route. |
404 | Cache-Control | string | Always no-store, private. Never cache or store a response of an identity details route or of the review decision route. |
409 | Cache-Control | string | Always no-store, private. Never cache or store a response of an identity details route or of the review decision route. |
410 | Cache-Control | string | Always no-store, private. Never cache or store a response of an identity details route or of the review decision route. |
429 | Retry-After | integer | Seconds until the bucket allows another request. |
503 | Cache-Control | string | Always no-store, private. Never cache or store a response of an identity details route or of the review decision route. |
503 | Retry-After | integer | Seconds to wait before retrying. |
Response fields (200, application/json)
| Field | Type | Always returned | Description |
|---|---|---|---|
ok | boolean | Yes | Allowed values: true. |
verificationId | string | Yes | The identifier of one verification. Treat it as an opaque string and store it in a column that holds at least 128 characters. The API contract fixes two limits that hold for every id: it is never longer than 128 characters, and it only contains the characters A-Z, a-z, 0-9, :, _ and -. Today an id has one of two shapes. A verification created through POST /v1/kyc/verify (including one made from an evidenceGroupId, and each item of a batch) is a lower-case UUID: 36 characters, for example 3fa85f64-5717-4562-b3fc-2c963f66afa6. The verification a hosted session runs when the customer submits is hosted: followed by 64 lower-case hexadecimal characters: 71 characters in all. Do not parse an id or rely on its prefix; a new shape may appear, but always within the two limits above. At least 1 character, at most 128 characters. Pattern ^[A-Za-z0-9:_-]{1,128}$. |
sourceEvidenceGroupId | string, nullable | Yes | The evidenceGroupId the verification was made from, or null when it did not come from your own capture screens. |
extractedAt | string (date-time) | Yes | When the details were extracted and stored. |
verification | object | Yes | |
verification.status | string | Yes | The outcome of a verification. verified, unverified, failed and error are the verdict words of the automated verification; pending means the verification has no verdict yet. A reviewer’s approve decision turns the reported status into verified (see KycVerificationStatusSource); no other decision changes it. Allowed values: verified, unverified, failed, error, pending. |
verification.source | string | Yes | Who produced the reported status. pipeline — the automated verdict exactly as it was recorded. review — a reviewer’s approve decision was applied over it. An override decision never produces review. Allowed values: pipeline, review. |
verification.level | string, nullable | Yes | The automated verification level, or null when none was assigned. A review does not change it. Allowed values: verified, partial, unverified, inconclusive, null. |
verification.review | one of KycTenantReview or any, nullable | Yes | The reviewer’s work on the verification; null while no reviewer has acted. |
verification.review{KycTenantReview} | object (KycTenantReview) | No | What a reviewer did with a verification. No reviewer identity, no reviewer note and no reviewer reason codes are returned. |
verification.review{KycTenantReview}.state | string | Yes | State of a reviewer’s work on a verification. in_review — a reviewer escalated the case without deciding (no decision yet). approved — a reviewer approved it. overridden — a reviewer replaced the automated verdict. open and closed belong to the vocabulary but no Backoffice review action writes them today; handle an unknown or unexpected value as “not decided”. Allowed values: open, in_review, approved, overridden, closed. |
verification.review{KycTenantReview}.decision | one of KycReviewDecision or any, nullable | Yes | The reviewer’s decision, or null while the state is in_review. |
verification.review{KycTenantReview}.version | integer | Yes | Number of review writes for this verification, starting at 1 and increasing by one with every write (an escalation counts). Minimum 1. |
verification.review{KycTenantReview}.decidedAt | integer (int64), nullable | Yes | Unix ms of the last review write that carried a decision, or null while there is none. |
document | object | Yes | |
document.type | string | Yes | The document type the stored extraction is for. Allowed values: national_id, driving_license, passport, residence_permit, unknown. |
document.selectedType | string, nullable | Yes | The document type the person chose in the hosted flow, or null for a verification created through the API or a session that recorded no choice. Differs from type when the extraction ran against another document type than the person presented. Allowed values: national_id, passport, driving_license, residence_permit, null. |
document.side | string | Yes | Allowed values: front, back, data_page, unknown. |
document.issuingCountry | string, nullable | Yes | The country the document shows when one was read (extraction.issuingCountry), otherwise the country the person selected in the hosted flow, otherwise null. |
document.number | string, nullable | Yes | The document number, masked (its last characters only) unless fields names documentNumber. |
document.expiryDate | string (date), nullable | Yes | |
document.idType | string, nullable | Yes | |
person | object | Yes | |
person.fullName | string, nullable | Yes | |
person.surname | string, nullable | Yes | |
person.givenNames | string, nullable | Yes | |
person.dateOfBirth | string (date), nullable | Yes | |
person.sex | string, nullable | Yes | As read from the document, usually M, F or X. |
person.nationality | string, nullable | Yes | |
person.personalNumber | string, nullable | Yes | Left out (null) unless fields names personalNumber. |
address | object, nullable | Yes | Always null today: Noxtica does not extract an address from identity documents. The key is present so a later release can fill it without changing the shape. |
mrz | object, nullable | Yes | The machine-readable zone of the document, or null when none was found. |
mrz.format | string, nullable | Yes | Allowed values: TD1, TD2, TD3, null. |
mrz.fieldChecks | array of object | Yes | The check-digit result of each MRZ field. Structural results only. |
mrz.fieldChecks[] | object | No | |
mrz.fieldChecks[].field | string | Yes | |
mrz.fieldChecks[].checkDigitValid | boolean | Yes | |
mrz.lines | array of string | No | The raw MRZ lines. Present only when fields names mrzLines. |
checks | object (KycIdentityChecks) | Yes | A summary of the verification checks as verdicts. No scores or provider details are included. |
checks.documentAuthenticity | string, nullable | Yes | One check family’s verdict, or null when the verification has no such check. Allowed values: pass, review, fail, skipped, pending, null. |
checks.faceMatch | string, nullable | Yes | One check family’s verdict, or null when the verification has no such check. Allowed values: pass, review, fail, skipped, pending, null. |
checks.liveness | string, nullable | Yes | One check family’s verdict, or null when the verification has no such check. Allowed values: pass, review, fail, skipped, pending, null. |
checks.selfie | string, nullable | Yes | One check family’s verdict, or null when the verification has no such check. Allowed values: pass, review, fail, skipped, pending, null. |
checks.crossValidation | object, nullable | Yes | Whether the values read from the different parts of the document agree. The agreement is returned, not the values compared; null when no cross-check ran. |
checks.crossValidation.overall | string | Yes | Allowed values: match, mismatch, unavailable. |
checks.crossValidation.fields | array of object | Yes | |
checks.crossValidation.fields[] | object | No | |
checks.crossValidation.fields[].field | string | Yes | The name of the compared value. |
checks.crossValidation.fields[].agreement | string | Yes | Allowed values: match, mismatch, unavailable. |
extraction | object (KycIdentityExtraction) | Yes | Whether the identity fields a relying party needs were read from the document, field by field, with the reason for every field that was not. |
extraction.status | string | Yes | complete — every required field was read (also when none is required). partial — some were. unreadable — none was. Allowed values: complete, partial, unreadable. |
extraction.reasonCodes | array of string | Yes | identity_fields_unreadable when a required field is unreadable; document_type_mismatch_selection when documentType.contradictsSelection is true; issuing_country_mismatch_selection when issuingCountry.contradictsSelection is true; diacritic_uncertain or mrz_transliteration_conflict when person.surname or person.givenNames carries that uncertainty (informational; unlike the other three, not added to the reasonCodes of the verification and of the session). |
extraction.fields | array of KycIdentityFieldRead | Yes | Always five entries, in this order: person.surname, person.givenNames, person.dateOfBirth, person.personalNumber, document.number. At least 5 items, at most 5 items. |
extraction.fields[] | object (KycIdentityFieldRead) | No | The outcome of reading one identity field from the document: whether it was read and, when it was not, why. |
extraction.fields[].field | string | Yes | The path of the field in this response. Allowed values: person.surname, person.givenNames, person.dateOfBirth, person.personalNumber, document.number. |
extraction.fields[].status | string | Yes | read — the field has a value (released, or withheld by the masking rules; read does not mean the value is in the response). unreadable — no value, see reason. not_expected — this document does not carry the field and your configuration does not require it. Allowed values: read, unreadable, not_expected. |
extraction.fields[].reason | string, nullable | Yes | Only with unreadable, otherwise null. label_not_found, value_not_found, value_invalid, checksum_invalid (the personal number’s check digits fail; the value is withheld), layout_unrecognised, low_confidence (a date read below the recognition confidence floor is withheld) or not_recorded (a record written before reasons were kept). Allowed values: label_not_found, value_not_found, value_invalid, checksum_invalid, layout_unrecognised, low_confidence, not_recorded, null. |
extraction.fields[].source | string, nullable | Yes | Where the value came from, or null unless status is read. Allowed values: viz, mrz, barcode, null. |
extraction.fields[].uncertainty | array of string | No | Only on person.surname and person.givenNames, and only when it applies (the key is absent otherwise). Advisory caveats on a read value. diacritic_uncertain — the value is the best reading, but a letter in it may carry a mark that the reading did not show, or the reverse (a Turkish Ş shown as S): the reader saw the marked and the plain letter as close candidates, two readers of the printed text differed only in a mark, or the value comes from the machine-readable zone or a barcode, which cannot print marks. mrz_transliteration_conflict — the printed name and the machine-readable zone fit together only if a plain letter of the printed name carries a mark (printed MULLER, zone MUELLER); it can come with or without diacritic_uncertain. status stays read; check alternatives and foldedValue as described under “Names, diacritics and alphabets” on GET /v1/kyc/verifications/{verificationId}/identity. A field without this key can still contain a wrong letter. |
extraction.fields[].uncertainPositions | array of integer | No | Only with diacritic_uncertain. 0-based Unicode code point positions in the name (person.surname or person.givenNames, spaces counted), ascending, of the letters that may carry a different mark. For a name read from the printed text: the letters the reader had doubt about. For a name taken from the machine-readable zone or a barcode: every letter that has a marked form in the alphabet of the issuing country. |
extraction.fields[].alternatives | array of string | No | Up to four complete spellings of the name, most likely first, made only by swapping the flagged letters for their marked or plain forms. None equals the value and none repeats. With mrz_transliteration_conflict the list includes the spelling the machine-readable zone points to. Absent when there is no reading-based doubt, in particular for a name taken from the machine-readable zone or a barcode. The value is never replaced by an alternative; do not store one as the legal name without a confirmation. At most 4 items. |
extraction.fields[].foldedValue | string | No | On every read person.surname and person.givenNames, with or without an uncertainty: the value with its marks removed as ICAO Doc 9303 Part 3 section 6.A lists them (one base letter such as Ş to S; the expansions ß to SS, Æ to AE, Œ to OE, Ø to OE, Þ to TH, IJ to IJ). The case of the value is kept. Use it as a key for tolerant comparison, not as the spelling of the name. |
extraction.fields[].script | string | No | The writing system the name was read in, as an ISO 15924 code. Absent when it was not determined. Allowed values: Latn, Cyrl, Grek, Arab, Hebr, Geor, Armn, Deva, Thai, Hani, Jpan, Kore. |
extraction.fields[].latinForm | object | No | A Latin-letter form of the name, when one is available. |
extraction.fields[].latinForm.value | string | Yes | |
extraction.fields[].latinForm.source | string | Yes | viz — printed on the document. mrz — the name in the machine-readable zone, which uses the ICAO transliteration. Allowed values: viz, mrz. |
extraction.fields[].required | boolean | Yes | true when the field counts towards extraction.status and the reason code identity_fields_unreadable; false when it is reported but does not. An unreadable field with required: false keeps its reason. |
extraction.documentType | object | Yes | |
extraction.documentType.selected | string, nullable | Yes | The document type the person chose in the hosted flow (the same value as document.selectedType), or null. Allowed values: national_id, passport, driving_license, residence_permit, null. |
extraction.documentType.classified | string, nullable | Yes | The document type the classifier named, only when it is confident (0.72 or higher); otherwise null. Allowed values: national_id, driving_license, passport, residence_permit, null. |
extraction.documentType.contradictsSelection | boolean, nullable | Yes | true when the classified type is a different identity document type than the selection, false when they agree, null when they cannot be compared. |
extraction.issuingCountry | object | Yes | Whether the country the person selected agrees with the country the document shows. |
extraction.issuingCountry.selected | string, nullable | Yes | The issuing country the person chose in the hosted flow, as an ISO 3166-1 alpha-2 code (a CountryCode value), or null for a verification created through the API, a session that recorded no choice, or a record written before the selection was kept. Pattern ^[A-Z]{2}$. |
extraction.issuingCountry.detected | string, nullable | Yes | The country the document shows, as an ISO 3166-1 alpha-2 code, or null when nothing readable named one. Pattern ^[A-Z]{2}$. |
extraction.issuingCountry.source | string, nullable | Yes | Where detected came from: mrz (the issuing state of the machine-readable zone, used only when none of its check digits failed) or viz (the printed text of the front). null exactly when detected is null. Allowed values: mrz, viz, null. |
extraction.issuingCountry.contradictsSelection | boolean, nullable | Yes | true when both are known and differ, false when they are equal, null when they cannot be compared (no selection, nothing read, or an older record). An unknown is never reported as a contradiction; a dependent territory against its sovereign state is not one. |
redactions | array of IdentityRedaction | Yes | |
redactions[] | object (IdentityRedaction) | No | One item the response masked, left out or could not provide. It tells “not there” apart from “left out on purpose”. |
redactions[].field | string | Yes | Path of the item in the response, for example document.number, person.surname, person.givenNames, person.dateOfBirth, person.personalNumber, mrz, mrz.lines, address or documents[].extractions. |
redactions[].reason | string | Yes | masked_by_default — the value is returned masked (its last characters only). omitted_by_default — the value is left out. not_extracted — Noxtica does not extract this item, or did not for this record. not_available — the item is not available for this record. Allowed values: masked_by_default, omitted_by_default, not_extracted, not_available. |
redactions[].include | string | No | The fields value that returns the full item. Absent when no fields value does. |
requestId | string | Yes |
Response example (200)
{
"ok": true,
"verificationId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"sourceEvidenceGroupId": null,
"extractedAt": "2026-10-28T14:22:11.000Z",
"verification": {
"status": "verified",
"source": "pipeline",
"level": "verified",
"review": null
},
"document": {
"type": "national_id",
"selectedType": null,
"side": "front",
"issuingCountry": "GB",
"number": "••••4821",
"expiryDate": "2031-05-17",
"idType": "national_id"
},
"person": {
"fullName": "ADA EXAMPLE",
"surname": "EXAMPLE",
"givenNames": "ADA",
"dateOfBirth": "1990-01-31",
"sex": "F",
"nationality": "GB",
"personalNumber": null
},
"address": null,
"mrz": {
"format": "TD1",
"fieldChecks": [
{
"field": "document_number",
"checkDigitValid": true
},
{
"field": "birth_date",
"checkDigitValid": true
},
{
"field": "expiry_date",
"checkDigitValid": true
}
]
},
"checks": {
"documentAuthenticity": "pass",
"faceMatch": "pass",
"liveness": "review",
"selfie": "review",
"crossValidation": {
"overall": "match",
"fields": [
{
"field": "dateOfBirth",
"agreement": "match"
},
{
"field": "documentNumber",
"agreement": "match"
}
]
}
},
"extraction": {
"status": "complete",
"reasonCodes": [],
"fields": [
{
"field": "person.surname",
"status": "read",
"reason": null,
"source": "mrz",
"required": true,
"foldedValue": "EXAMPLE",
"script": "Latn"
},
{
"field": "person.givenNames",
"status": "read",
"reason": null,
"source": "mrz",
"required": true,
"foldedValue": "ADA",
"script": "Latn"
},
{
"field": "person.dateOfBirth",
"status": "read",
"reason": null,
"source": "mrz",
"required": true
},
{
"field": "person.personalNumber",
"status": "not_expected",
"reason": null,
"source": null,
"required": false
},
{
"field": "document.number",
"status": "read",
"reason": null,
"source": "mrz",
"required": true
}
],
"documentType": {
"selected": null,
"classified": "national_id",
"contradictsSelection": null
},
"issuingCountry": {
"selected": null,
"detected": "GB",
"source": "mrz",
"contradictsSelection": null
}
},
"redactions": [
{
"field": "document.number",
"reason": "masked_by_default",
"include": "documentNumber"
},
{
"field": "person.personalNumber",
"reason": "omitted_by_default",
"include": "personalNumber"
},
{
"field": "mrz.lines",
"reason": "omitted_by_default",
"include": "mrzLines"
},
{
"field": "address",
"reason": "not_extracted"
}
],
"requestId": "req-test"
}
Example request
curl
curl "https://collect.noxtica.com/v1/kyc/verifications/example-verificationId/identity" \
-H "Authorization: Bearer $NOXTICA_API_KEY"
Node.js (global fetch, Node 18 or later)
const response = await fetch('https://collect.noxtica.com/v1/kyc/verifications/example-verificationId/identity', {
method: 'GET',
headers: {
Authorization: `Bearer ${process.env.NOXTICA_API_KEY}`,
},
});
console.log(response.status, await response.text());
Python (standard library)
import os
import urllib.request
request = urllib.request.Request(
"https://collect.noxtica.com/v1/kyc/verifications/example-verificationId/identity",
headers={
"Authorization": f"Bearer {os.environ['NOXTICA_API_KEY']}",
},
method="GET",
)
with urllib.request.urlopen(request) as response:
print(response.status, response.read().decode())
Record a review decision for a verification
POST /v1/kyc/verifications/{verificationId}/review-decision
Server-to-server. Lets your own back office record the decision on a verification that is waiting for review: the same decision a Backoffice reviewer records in the review panel, written by the same decision service, with the same effect on the status Noxtica reports, the same kyc.review.decided event and the same audit trail. Nothing else changes: the automated verdict of the verification is never rewritten, and a decision is never made for you.
Opt-in at two levels. The route is off for every organisation until Noxtica turns the review decision API on for the platform and a tenant administrator of your organisation enables it in the Backoffice (KYC → Invitations → Settings tab, card “Review decision API”, confirmed with step-up multi-factor authentication; it is not on the Settings page of your own account). While the platform has it off, a request answers the byte-identical 404 of an unknown path; while your organisation has not enabled it, 403 review_decision_not_enabled. The key needs the write:kyc_review_decision scope: no other scope reaches this route, and write:identity does not imply it. A key keeps its scopes until a tenant administrator changes them (Edit scopes on the key in the Backoffice, API Configuration): add the scope to the key, or create a new key that carries it. The tenant administrator can switch the route off again at any time; the decisions already written stay.
Which id. verificationId is the id returned by POST /v1/kyc/verify (a UUID) or the verificationId of a hosted session (hosted: followed by 64 lower-case hexadecimal characters); see KycVerificationId for the formats and the 128-character maximum. An id that is malformed, unknown, or belongs to another organisation gets the same byte-identical 404 as an unknown path, so the route cannot be used to find out whether a verification exists.
Decisions. There are two, the two affirmative decisions of the review panel, and no other:
decision | Meaning | Reported status afterwards | Allowed reasonCode |
|---|---|---|---|
approve | Your reviewer affirmed the verification. | verified, with verification.source review (see GET /v1/kyc/verifications/{verificationId}/identity and GET /v1/kyc/sessions/{sessionId}). approve is the only decision that reports a verification as verified. | identity_confirmed, registry_crosscheck_passed, name_variant_accepted, manual_document_check_passed, other |
override | Your reviewer replaced the automated verdict. It is not an approval. | The automated verdict is reported as it is; override never makes the status verified. | identity_mismatch, registry_crosscheck_failed, document_invalid, suspected_fraud, liveness_not_accepted, other |
There is no reject decision: the review panel has none, and override is the action that replaces the automated verdict. Escalating a case is a state of the review, not a decision, and is not offered here. A reasonCode that does not belong to the decision of the same request is a 400 invalid_reason_code. other means none of the listed reasons; keep your own reason text in your own system, keyed by reviewer and the delivery id of the kyc.review.decided event.
Who decided. reviewer is your own identifier of the person who took the decision (for example the id of the reviewer in your back office): 1 to 128 visible characters, no surrounding spaces and no control characters. Noxtica stores it with the decision, next to the API key that sent the request, and returns it in this response. Use an internal id, not a name or an email address. The optional note (at most 2000 characters) is stored with the decision. Neither the reviewer, the key nor the note is ever sent in a webhook or returned by a read.
Pending reviews only. A decision is accepted while the review of the verification is still pending: no reviewer has acted on it, or a reviewer escalated it without deciding. A verification that already holds a decision, from the panel or from this route, answers 409 review_not_pending and nothing is written; use the Backoffice to change a decision that was already recorded. As for the panel, the verification must have finished and have an active legal basis recorded for it (409 legal_basis_missing otherwise). A review that was closed answers 409 review_closed.
What a decision does. The decision is written together with its audit row (action kyc_review_decision_recorded, source tenant_api) in one transaction. A hosted session that was review_pending is completed by it, exactly as a panel decision completes it, and the kyc.review.decided event is sent to the endpoints that subscribe to it with data.actor.type api and the closed data.reviewReasonCode. Hosted verifications keep ending unverified with the reason provider_manual_review until a decision is recorded: the automated outcome never approves, and a decision recorded here is the only thing that changes what is reported. Read verification and review on GET /v1/kyc/verifications/{verificationId}/identity (scope read:identity_details) or GET /v1/kyc/sessions/{sessionId} afterwards. The response of this route already carries the resulting state, version and decidedAt.
Idempotency. Idempotency-Key is required: 8 to 128 characters (a UUID is fine), a new value for every new decision. Send the same key again with the same body (a retry after a timeout) and you get the first answer back: 200 with the header Idempotent-Replayed: true and replayed: true. Nothing is written a second time, so no second event is sent. The same key with a different verification or a different body answers 422 idempotency_key_reuse. A key is scoped to your organisation. A 409 conflict means another decision for the same verification was written at the same moment and yours was not: read the verification, and send a new request only if it is still pending.
Audit and handling. Every decision is recorded for your organisation with the key that made it and the reviewer you sent. Every response of this route carries Cache-Control: no-store, private.
Rate limit. 30 requests per 60 seconds for each key; a limited request answers 429 with Retry-After. The route is available only while the KYC module is enabled for your organisation; otherwise it answers the GatedNotFound 404.
Authentication: Bearer token (ApiKeyAuth), scope write:kyc_review_decision.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
verificationId | path | string | Yes | The verificationId of the verification to decide. Pattern ^[A-Za-z0-9:_-]{1,128}$. |
Idempotency-Key | header | string | Yes | A replayed key returns the byte-identical stored response — no second provider call, no double charge. At least 8 characters, at most 128 characters. |
Request body
Content type application/json, required.
| Field | Type | Required | Description |
|---|---|---|---|
decision | string | Yes | A reviewer’s decision. approve — the reviewer affirmed the verification; the reported status becomes verified. override — the reviewer replaced the automated verdict; it is reported as it is but is not an approval and never makes the status verified. There is no reject decision today. An escalation is not a decision: it leaves the decision null. Allowed values: approve, override. |
reasonCode | string | Yes | The closed reason code of a decision made through the API, never free text. Each decision accepts its own subset. approve: identity_confirmed, registry_crosscheck_passed, name_variant_accepted, manual_document_check_passed, other. override: identity_mismatch, registry_crosscheck_failed, document_invalid, suspected_fraud, liveness_not_accepted, other. other means none of the listed reasons. A code outside the subset of the decision is refused with invalid_reason_code. Allowed values: identity_confirmed, registry_crosscheck_passed, name_variant_accepted, manual_document_check_passed, identity_mismatch, registry_crosscheck_failed, document_invalid, suspected_fraud, liveness_not_accepted, other. |
reviewer | string | Yes | Your own identifier of the person who took the decision: 1 to 128 visible characters, no surrounding spaces, no control characters. Stored with the decision and returned in the response; never sent in a webhook. Use an internal id rather than a name or an email address. At least 1 character, at most 128 characters. |
note | string, nullable | No | Optional free text, at most 2000 characters. Stored with the decision; never sent in a webhook and never returned. At most 2000 characters. |
Responses
| Status | Meaning | Error codes | Content |
|---|---|---|---|
200 | The decision is recorded and now stands (replayed: false), or the same request was already recorded and its first answer is returned (replayed: true, header Idempotent-Replayed: true). | application/json KycReviewDecisionResult | |
400 | The request is not valid; nothing was written. error is idempotency_key_invalid (the Idempotency-Key header is missing or not 8 to 128 characters), invalid_body (not a JSON object, or it has a member this route does not accept), invalid_decision, invalid_reason_code (not in the list of the decision), invalid_reviewer or invalid_note. message says what to fix and never repeats your values. | idempotency_key_invalid, invalid_reason_code | application/json KycReviewDecisionError |
401 | The Authorization: Bearer sk_... header is missing or malformed, or the key is unknown or revoked. The body is deliberately identical for every one of those causes. | invalid_api_key | application/json KycSessionError |
403 | review_decision_not_enabled — a tenant administrator has not enabled the review decision API for your organisation, so nothing was written; or the key lacks the write:kyc_review_decision scope (error is API key does not have write:kyc_review_decision scope). A tenant administrator enables the API in the Backoffice, KYC → Invitations → Settings tab, card “Review decision API”. | review_decision_not_enabled | application/json KycReviewDecisionError |
404 | The byte-identical “V1 endpoint not found” response, returned for a malformed id, an id that does not exist, an id that belongs to another organisation, or a route that is not available (the review decision API is off for the platform, or the KYC module is not enabled for your organisation) — indistinguishable from an unknown path by design. | V1 endpoint not found | application/json |
409 | Nothing was written. review_not_pending — the verification already holds a decision. review_closed — the review was closed. legal_basis_missing — the verification has not finished, or no active legal basis is recorded for it. conflict — another decision for this verification was written at the same moment; read the verification and send a new request only if it is still pending. | conflict, legal_basis_missing, review_closed, review_not_pending | application/json KycReviewDecisionError |
413 | invalid_body — the body is larger than 16 KiB. Nothing was written. | invalid_body | application/json KycReviewDecisionError |
415 | invalid_body — the Content-Type is not application/json. Nothing was written. | invalid_body | application/json KycReviewDecisionError |
422 | idempotency_key_reuse — the Idempotency-Key was already used for a different verification or a different body. Nothing was written. Use a new key for a new decision. | idempotency_key_reuse | application/json KycReviewDecisionError |
429 | Rate limit exceeded. Wait Retry-After seconds (also retryAfter in the body) before retrying; the response carries no other rate-limit headers. | Rate limit exceeded | application/json |
500 | API-key authentication is not configured on this deployment (server_misconfigured). Retrying does not help; contact support with the requestId. | server_misconfigured | application/json KycSessionError |
503 | Retryable. Retry with the same Idempotency-Key: you get the decision written by an earlier try as a replay, or the decision is written now. review_decision_unavailable — the decision, or the setting that enables the route, could not be read or written right now. tenant_module_gate_unavailable — the module check could not be confirmed. api_key_auth_unavailable — the key could not be checked. Every one of them carries Retry-After. | tenant_module_gate_unavailable | application/json KycReviewDecisionError |
503 | Noxtica could not read the key’s record. This is a temporary fault on Noxtica’s side and says nothing about your key. Retry after the Retry-After delay; do not treat it as a revoked key. | api_key_auth_unavailable | application/json |
Response headers
| Status | Header | Type | Description |
|---|---|---|---|
200 | Cache-Control | string | Always no-store, private. Never cache or store a response of an identity details route or of the review decision route. |
200 | Idempotent-Replayed | string | true only when the answer is the stored answer of an earlier request with the same Idempotency-Key. Absent otherwise. |
400 | Cache-Control | string | Always no-store, private. Never cache or store a response of an identity details route or of the review decision route. |
403 | Cache-Control | string | Always no-store, private. Never cache or store a response of an identity details route or of the review decision route. |
404 | Cache-Control | string | Always no-store, private. Never cache or store a response of an identity details route or of the review decision route. |
409 | Cache-Control | string | Always no-store, private. Never cache or store a response of an identity details route or of the review decision route. |
413 | Cache-Control | string | Always no-store, private. Never cache or store a response of an identity details route or of the review decision route. |
415 | Cache-Control | string | Always no-store, private. Never cache or store a response of an identity details route or of the review decision route. |
422 | Cache-Control | string | Always no-store, private. Never cache or store a response of an identity details route or of the review decision route. |
429 | Retry-After | integer | Seconds until the bucket allows another request. |
503 | Cache-Control | string | Always no-store, private. Never cache or store a response of an identity details route or of the review decision route. |
503 | Retry-After | integer | Seconds to wait before retrying. |
Response fields (200, application/json)
| Field | Type | Always returned | Description |
|---|---|---|---|
requestId | string | Yes | |
verificationId | string | Yes | The decided verification. Format and limits of a verification id are in KycVerificationId. |
decision | string | Yes | A reviewer’s decision. approve — the reviewer affirmed the verification; the reported status becomes verified. override — the reviewer replaced the automated verdict; it is reported as it is but is not an approval and never makes the status verified. There is no reject decision today. An escalation is not a decision: it leaves the decision null. Allowed values: approve, override. |
reasonCode | string | Yes | The closed reason code of a decision made through the API, never free text. Each decision accepts its own subset. approve: identity_confirmed, registry_crosscheck_passed, name_variant_accepted, manual_document_check_passed, other. override: identity_mismatch, registry_crosscheck_failed, document_invalid, suspected_fraud, liveness_not_accepted, other. other means none of the listed reasons. A code outside the subset of the decision is refused with invalid_reason_code. Allowed values: identity_confirmed, registry_crosscheck_passed, name_variant_accepted, manual_document_check_passed, identity_mismatch, registry_crosscheck_failed, document_invalid, suspected_fraud, liveness_not_accepted, other. |
reviewer | string | Yes | The reviewer of the request. |
state | string | Yes | The review state after the decision, approved for approve and overridden for override. Allowed values: approved, overridden. |
version | integer | Yes | The review version after the decision (review.version on the reads, data.decisionVersion of the kyc.review.decided event). Minimum 1. |
decidedAt | integer (int64) | Yes | Unix ms the decision was written. |
replayed | boolean | Yes | true when this is the stored answer of an earlier request with the same Idempotency-Key (the response also carries Idempotent-Replayed: true); false when this request wrote the decision. |
Response example (200)
{
"requestId": "req-test",
"verificationId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"decision": "approve",
"reasonCode": "registry_crosscheck_passed",
"reviewer": "reviewer-17",
"state": "approved",
"version": 1,
"decidedAt": 1790762445000,
"replayed": false
}
Example request
curl
curl -X POST "https://collect.noxtica.com/v1/kyc/verifications/example-verificationId/review-decision" \
-H "Authorization: Bearer $NOXTICA_API_KEY" \
-H "Idempotency-Key: example-Idempotency-Key" \
-H "Content-Type: application/json" \
-d '{
"decision": "approve",
"reasonCode": "registry_crosscheck_passed",
"reviewer": "reviewer-17",
"note": "Name matches the registry answer."
}'
Node.js (global fetch, Node 18 or later)
const response = await fetch('https://collect.noxtica.com/v1/kyc/verifications/example-verificationId/review-decision', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.NOXTICA_API_KEY}`,
'Idempotency-Key': 'example-Idempotency-Key',
'Content-Type': 'application/json',
},
body: JSON.stringify({
decision: 'approve',
reasonCode: 'registry_crosscheck_passed',
reviewer: 'reviewer-17',
note: 'Name matches the registry answer.',
}),
});
console.log(response.status, await response.text());
Python (standard library)
import json
import os
import urllib.request
request = urllib.request.Request(
"https://collect.noxtica.com/v1/kyc/verifications/example-verificationId/review-decision",
data=json.dumps({
"decision": "approve",
"reasonCode": "registry_crosscheck_passed",
"reviewer": "reviewer-17",
"note": "Name matches the registry answer.",
}).encode(),
headers={
"Authorization": f"Bearer {os.environ['NOXTICA_API_KEY']}",
"Idempotency-Key": "example-Idempotency-Key",
"Content-Type": "application/json",
},
method="POST",
)
with urllib.request.urlopen(request) as response:
print(response.status, response.read().decode())
Read the current verification capacity and a start / wait recommendation
GET /v1/kyc/load
Server-to-server. Tells your backend how busy Noxtica’s verification capacity is right now
and whether this is a good moment to start a new verification. Call it before you send a
customer into a capture flow or submit a verification, when you would rather queue or delay
the customer than let them wait on a busy platform. The answer is advisory: it never
reserves, admits or blocks anything, a verification started while the answer is defer is
still processed, and every verification route keeps its own limits.
What the numbers mean. capacity is counted in concurrent inference slots, not in
sessions. One slot runs one inference job at a time, and one verification uses a slot for
each of its steps (a step typically takes 25 ms to 3 s; a verification has about three to
five steps). capacity: 8 therefore means eight inference jobs can run at the same moment,
not that eight customers can be verified at the same moment. inUse is the number of busy
slots, queueDepth the number of jobs waiting for a slot, estimatedWaitMs how long a
job that arrives now would wait before it starts, and processingMs the p50 and p95 duration
of recently completed jobs. Everything except yours is a platform-wide aggregate shared by
all customers; yours.inFlight is the number of verifications your own organisation has
running right now.
Recommendation.
recommendation | Meaning | Suggested action |
|---|---|---|
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 flow tolerates extra latency; otherwise show a “taking longer than usual” message. |
defer | The platform is saturated or has no ready capacity; a new job is likely to wait several seconds or be refused with an overload answer. | Queue the customer, show a wait message, and ask again later with backoff, using estimatedWaitMs as a hint. |
recommendation is derived from reasons: any reason from the first group below gives
defer, otherwise any reason from the second group gives degraded, otherwise accept.
reasons is ordered (the defer reasons first) and never repeats a value.
reasons value | Group | Present when |
|---|---|---|
no_ready_instance | defer | No inference capacity is ready to take work (capacity is 0). |
queue_wait_high | defer | estimatedWaitMs is at least 6000 ms, or at least the platform’s own queue-wait limit when that is lower. |
queue_full | defer | queueDepth is greater than or equal to capacity. |
recent_overload | defer | A request was refused for overload, queue timeout or memory pressure within the last 30 seconds. |
high_utilization | degraded | inUse / capacity is at least 0.75. |
queue_building | degraded | queueDepth is greater than 0. |
wait_elevated | degraded | estimatedWaitMs is at least 1500 ms. |
reduced_capacity | degraded | Part of the platform’s inference capacity is out of service, so capacity is lower than normal. |
escalation_unavailable | degraded | The escalation stage that re-examines difficult cases is unavailable; verifications continue without it. |
escalation_saturated | degraded | The escalation stage is running at full capacity, so difficult cases may 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 at least 10000 ms over at least 20 samples. |
load_unavailable | degraded | status is unknown: the load could not be read. |
status: unknown. If Noxtica cannot read its own load (a timeout, an unreachable
service, an invalid answer), the route still answers 200, never a 5xx, with
status: unknown, recommendation: degraded, reasons: ["load_unavailable"] and null in
capacity, inUse, queueDepth, estimatedWaitMs and processingMs. It is degraded,
not defer, on purpose: a monitoring glitch must not make you stop every verification. Keep
starting verifications; if the verification service is really down, the verification call
itself reports it. An unknown answer is cached for 5 seconds, then the load is read again.
Caching and polling. The platform part of the answer is computed once and shared by all
customers; ttlSeconds is how long that snapshot is kept (8 seconds, or 5 for unknown),
and the response carries Cache-Control: private, max-age=5. Polling faster than every 10
seconds only returns the same snapshot. Poll from your backend on a timer (or when a customer
is about to start), cache the answer on your server and share it between your users; do not
call this route once per end-user page view. yours.inFlight is computed on each request.
The route is rate limited per API key (30 requests per 60 seconds); a limited call answers
429 with Retry-After.
Authentication. Authorization: Bearer sk_... with the write:identity scope, the same
scope as GET /v1/kyc/sessions/{sessionId}; there is no separate read scope. A key without
it gets 403. The route is available only when the KYC module is enabled for your
organisation; otherwise it answers the GatedNotFound 404, indistinguishable from an unknown path.
Privacy. The answer carries aggregated platform numbers and your own in-flight count only. It contains no personal data, no other customer’s data, and nothing about how or where the capacity is hosted.
Authentication: Bearer token (ApiKeyAuth), scope write:identity.
Responses
| Status | Meaning | Error codes | Content |
|---|---|---|---|
200 | The capacity snapshot. Also the answer, with status: unknown, when the platform could not read its own load; this route does not answer 5xx for that. | application/json KycLoadStatus | |
401 | The Authorization: Bearer sk_... header is missing or malformed, or the key is unknown or revoked. The body is deliberately identical for every one of those causes. | invalid_api_key | application/json KycSessionError |
403 | The presented key lacks the required scope. | API key does not have write:identity scope | application/json |
404 | The byte-identical “V1 endpoint not found” response returned when the route’s module is not enabled for your organisation — indistinguishable from a route that does not exist (anti-enumeration by design; do not treat this as proof the path is wrong). | V1 endpoint not found | application/json |
429 | Rate limit exceeded. Wait Retry-After seconds (also retryAfter in the body) before retrying; the response carries no other rate-limit headers. | Rate limit exceeded | application/json |
500 | API-key authentication is not configured on this deployment (server_misconfigured). Retrying does not help; contact support with the requestId. | server_misconfigured | application/json KycSessionError |
503 | Retryable (Retry-After: 2). tenant_module_gate_unavailable: whether the KYC module is enabled for your organisation could not be confirmed. Retry shortly. | tenant_module_gate_unavailable | application/json KycSessionError |
503 | Noxtica could not read the key’s record. This is a temporary fault on Noxtica’s side and says nothing about your key. Retry after the Retry-After delay; do not treat it as a revoked key. | api_key_auth_unavailable | application/json |
Response headers
| Status | Header | Type | Description |
|---|---|---|---|
200 | Cache-Control | string | The answer may be reused by the calling backend for up to 5 seconds. |
429 | Retry-After | integer | Seconds until the bucket allows another request. |
503 | Retry-After | integer | Seconds to wait before retrying. |
Response fields (200, application/json)
| Field | Type | Always returned | Description |
|---|---|---|---|
ok | boolean | Yes | Allowed values: true. |
status | string | Yes | ok: the load was read and the numbers below are real. unknown: the load could not be read; the numbers are null and recommendation is degraded. Both are HTTP 200. Allowed values: ok, unknown. |
recommendation | string | Yes | accept: start the verification. degraded: it will be accepted but may be slower than usual. defer: the platform is saturated; queue the customer and ask again later. Allowed values: accept, degraded, defer. |
reasons | array of string | Yes | Why recommendation is not accept; empty when it is accept. Ordered with the defer reasons first; no value repeats. Treat an unrecognised value as informational. |
capacity | integer, nullable | Yes | Concurrent inference slots the platform has ready right now. One slot runs one inference job at a time and a verification uses a slot for each of its steps, so this is not a number of sessions. 0 when no capacity is ready; null when status is unknown. Minimum 0. |
inUse | integer, nullable | Yes | Slots that are running a job right now, platform-wide (your jobs and everyone else’s). null when status is unknown. Minimum 0. |
queueDepth | integer, nullable | Yes | Jobs, platform-wide, that are waiting for a slot. null when status is unknown. Minimum 0. |
estimatedWaitMs | integer, nullable | Yes | Estimated milliseconds a job that arrives now would wait before it starts running (not including its own processing time). 0 when a slot is free. null when status is unknown, or when no capacity is ready. Minimum 0. |
processingMs | one of KycLoadProcessingMs or any, nullable | Yes | How long recently completed inference jobs took, in milliseconds (the job itself, not the queue wait). null when status is unknown or no job has completed recently. |
processingMs{KycLoadProcessingMs} | object (KycLoadProcessingMs) | No | Percentiles of the duration of recently completed inference jobs. Each value is the upper bound of the duration bucket that holds the percentile (25, 50, 100, 200, 400, 800, 1500, 3000, 6000, 12000 or 20000 ms), so it is a conservative figure; durations above 20000 ms read as 20000. |
processingMs{KycLoadProcessingMs}.p50 | integer | Yes | Median job duration in milliseconds. Minimum 0. |
processingMs{KycLoadProcessingMs}.p95 | integer | Yes | 95th-percentile job duration in milliseconds. Minimum 0. |
processingMs{KycLoadProcessingMs}.samples | integer | Yes | How many recently completed jobs the percentiles are computed from. Minimum 1. |
yours | object | Yes | The only values that are specific to your organisation. |
yours.inFlight | integer, nullable | Yes | Verifications of your own organisation that are running right now (submitted to the verification service within the last 90 seconds and not yet finished). Computed on each request, never cached. null when it could not be counted; this never fails the request. Minimum 0. |
asOf | string (date-time) | Yes | The instant, in ISO 8601 UTC, the platform snapshot was taken (for unknown, when the failed read was recorded). |
ttlSeconds | integer | Yes | How long Noxtica keeps the platform snapshot before reading the load again: 8 seconds, or 5 for an unknown answer. Polling more often than this only returns the same snapshot. |
requestId | string | Yes |
Response example (200)
{
"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": "req-test"
}
Example request
curl
curl "https://collect.noxtica.com/v1/kyc/load" \
-H "Authorization: Bearer $NOXTICA_API_KEY"
Node.js (global fetch, Node 18 or later)
const response = await fetch('https://collect.noxtica.com/v1/kyc/load', {
method: 'GET',
headers: {
Authorization: `Bearer ${process.env.NOXTICA_API_KEY}`,
},
});
console.log(response.status, await response.text());
Python (standard library)
import os
import urllib.request
request = urllib.request.Request(
"https://collect.noxtica.com/v1/kyc/load",
headers={
"Authorization": f"Bearer {os.environ['NOXTICA_API_KEY']}",
},
method="GET",
)
with urllib.request.urlopen(request) as response:
print(response.status, response.read().decode())
Report one observed merchant transaction signal
POST /v1/kym/transactions
Notify-only. Records exactly one transaction signal that feeds scheduled alert-rule evaluation; this endpoint never calls a payment or execution API. The target caseId must already exist under KYM monitoring (created in the Backoffice KYB/KYM case surface).
Authentication: Bearer token (ApiKeyAuth), scope write:kym.
Request body
Content type application/json, required.
| Field | Type | Required | Description |
|---|---|---|---|
caseId | string | Yes | At most 512 characters. |
idempotencyKey | string | Yes | At most 255 characters. |
amountMinor | integer | Yes | Amount in the currency’s minor unit (e.g. cents). Minimum 0. |
currency | string | Yes | Pattern ^[A-Z]{3}$. |
counterpartyRef | string, nullable | No | |
occurredAt | string (date-time) | Yes |
Responses
| Status | Meaning | Error codes | Content |
|---|---|---|---|
200 | A replayed idempotencyKey — the original row (deduplicated). | application/json KymTransactionResponse | |
201 | A new transaction-event row was recorded. | application/json KymTransactionResponse | |
400 | invalid_case_id / invalid_idempotency_key / invalid_amount / invalid_currency / invalid_occurred_at. | invalid_case_id, invalid_request | application/json ErrorEnvelope |
401 | The Authorization: Bearer sk_... header is missing or malformed, or the key is unknown or revoked. The body is deliberately identical for every one of those causes. | invalid_api_key | application/json KycSessionError |
403 | The presented key lacks the required scope. | API key does not have write:identity scope | application/json |
404 | The byte-identical gated-route 404, OR case_not_found (the caseId does not exist for this tenant). | invalid_request | application/json ErrorEnvelope |
429 | Rate limit exceeded. Wait Retry-After seconds (also retryAfter in the body) before retrying; the response carries no other rate-limit headers. | Rate limit exceeded | application/json |
503 | write_failed — a persistence fault. | invalid_request, write_failed | application/json ErrorEnvelope |
503 | Noxtica could not read the key’s record. This is a temporary fault on Noxtica’s side and says nothing about your key. Retry after the Retry-After delay; do not treat it as a revoked key. | api_key_auth_unavailable | application/json |
Response headers
| Status | Header | Type | Description |
|---|---|---|---|
429 | Retry-After | integer | Seconds until the bucket allows another request. |
503 | Retry-After | integer | Seconds to wait before retrying. |
Response fields (200, application/json)
| Field | Type | Always returned | Description |
|---|---|---|---|
ok | boolean | No | Allowed values: true. |
eventId | string | No | |
deduplicated | boolean | No | |
requestId | string | No |
Response example (200)
{
"ok": true,
"eventId": "evt_3c9b2f7e4a3d4c8e9f1a2b3c4d5e6f70",
"deduplicated": false,
"requestId": "req-test"
}
Response fields (201, application/json)
| Field | Type | Always returned | Description |
|---|---|---|---|
ok | boolean | No | Allowed values: true. |
eventId | string | No | |
deduplicated | boolean | No | |
requestId | string | No |
Response example (201)
{
"ok": true,
"eventId": "evt_3c9b2f7e4a3d4c8e9f1a2b3c4d5e6f70",
"deduplicated": false,
"requestId": "req-test"
}
Example request
curl
curl -X POST "https://collect.noxtica.com/v1/kym/transactions" \
-H "Authorization: Bearer $NOXTICA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"caseId": "example-caseId",
"idempotencyKey": "example-idempotencyKey",
"amountMinor": 0,
"currency": "USD",
"counterpartyRef": "example-counterpartyRef",
"occurredAt": "2026-09-13T12:00:00.000Z"
}'
Node.js (global fetch, Node 18 or later)
const response = await fetch('https://collect.noxtica.com/v1/kym/transactions', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.NOXTICA_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
caseId: 'example-caseId',
idempotencyKey: 'example-idempotencyKey',
amountMinor: 0,
currency: 'USD',
counterpartyRef: 'example-counterpartyRef',
occurredAt: '2026-09-13T12:00:00.000Z',
}),
});
console.log(response.status, await response.text());
Python (standard library)
import json
import os
import urllib.request
request = urllib.request.Request(
"https://collect.noxtica.com/v1/kym/transactions",
data=json.dumps({
"caseId": "example-caseId",
"idempotencyKey": "example-idempotencyKey",
"amountMinor": 0,
"currency": "USD",
"counterpartyRef": "example-counterpartyRef",
"occurredAt": "2026-09-13T12:00:00.000Z",
}).encode(),
headers={
"Authorization": f"Bearer {os.environ['NOXTICA_API_KEY']}",
"Content-Type": "application/json",
},
method="POST",
)
with urllib.request.urlopen(request) as response:
print(response.status, response.read().decode())
This page is generated from the Noxtica OpenAPI specification release v0.16.1: kyc.yaml version 1.14.1-2026-11-07 (SHA-256 dc9e4b53d94c). Do not edit it by hand.