Identity verification API
Verify identities, run hosted verification sessions, batch and re-verify, store evidence, and receive signed KYC, KYB and KYM webhooks.
| Environment | Base URL |
|---|---|
| Production | https://collect.noxtica.com |
Credential: ApiKeyAuth, SessionTokenAuth. See Authentication.
- Verify an identity
- Cross-match a self-uploaded proof-of-address document
- Submit a batch verification manifest
- Read a batch job’s aggregate status
- Read a batch job’s per-item outcomes
- Cancel every still-pending item of a batch
- Re-check an already-established identity’s device/verification level
- Mint a single-use ciphertext-upload handle
- Upload sealed ciphertext and finalize the evidence object
- Mint a Noxtica-hosted KYC verification-session link
- Read a hosted session’s current status (tenant backend read)
- Read the current hosted session (browser-facing)
- Checkpoint hosted-flow progress (browser-facing)
- Cancel the hosted session (browser-facing)
- Submit the hosted session for verification (browser-facing, one-shot)
- Email the person their Noxtica Verification ID (browser-facing, optional, consented)
- Send the email-verification link for the address the customer typed (browser-facing)
- Poll the email-verification state (browser-facing)
- Confirm the emailed link (browser-facing, link token is the credential)
- Read the public status of a Noxtica Verification ID (public, no authentication)
- Mint an evidence-upload handle (browser-facing)
- Upload sealed ciphertext and finalize (browser-facing)
- Create a right-to-erasure request for an attested subject
- Transactionally activate a proofed-unbound mobile capability
- Report one observed merchant transaction signal
KYC Verification
1 operation
- POST /v1/kyc/verify Verify an identity
KYC Cross-Validation
1 operation
- POST /v1/kyc/verify/{verificationId}/address-document Cross-match a self-uploaded proof-of-address document
KYC Batch
4 operations
- POST /v1/kyc/verify/batch Submit a batch verification manifest
- GET /v1/kyc/verify/batch/{batchId}/status Read a batch job's aggregate status
- GET /v1/kyc/verify/batch/{batchId}/results Read a batch job's per-item outcomes
- POST /v1/kyc/verify/batch/{batchId}/cancel Cancel every still-pending item of a batch
KYC Reverify
1 operation
- POST /v1/kyc/reverify Re-check an already-established identity's device/verification level
KYC Evidence
2 operations
- POST /v1/kyc/evidence/handle Mint a single-use ciphertext-upload handle
- POST /v1/kyc/evidence/finalize Upload sealed ciphertext and finalize the evidence object
KYC Hosted Sessions
13 operations
- POST /v1/kyc/sessions Mint a Noxtica-hosted KYC verification-session link
- GET /v1/kyc/sessions/{sessionId} Read a hosted session's current status (tenant backend read)
- GET /v1/kyc/session-view Read the current hosted session (browser-facing)
- POST /v1/kyc/session-progress Checkpoint hosted-flow progress (browser-facing)
- POST /v1/kyc/session-cancel Cancel the hosted session (browser-facing)
- POST /v1/kyc/session-submit Submit the hosted session for verification (browser-facing, one-shot)
- POST /v1/kyc/session-verification-id/email Email the person their Noxtica Verification ID (browser-facing, optional, consented)
- POST /v1/kyc/session-email/send Send the email-verification link for the address the customer typed (browser-facing)
- GET /v1/kyc/session-email/status Poll the email-verification state (browser-facing)
- POST /v1/kyc/email-verification/confirm Confirm the emailed link (browser-facing, link token is the credential)
- GET /v1/kyc/verification-status Read the public status of a Noxtica Verification ID (public, no authentication)
- POST /v1/kyc/session-evidence/handle Mint an evidence-upload handle (browser-facing)
- POST /v1/kyc/session-evidence/finalize Upload sealed ciphertext and finalize (browser-facing)
KYC Compliance
2 operations
- POST /v1/kyc/erasure/self-request Create a right-to-erasure request for an attested subject
- POST /v1/kyc/activate Transactionally activate a proofed-unbound mobile capability
KYM
1 operation
- POST /v1/kym/transactions Report one observed merchant transaction signal
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.
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. |
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). |
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 | application/json |
409 | A conflicting in-flight or idempotency-key-bound request (action:'verify' only). | example-code, idempotency_conflict | application/json ConflictError |
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 |
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]. |
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 |
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. |
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 |
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 | |
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 |
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 |
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 |
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 |
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) | Yes | |
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 |
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 | |
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. |
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 |
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. |
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())
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.
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. An unopened link keeps expiresInSeconds (default 24 hours). Once the
customer first opens it the deadline tightens to min(expiresAt, openedAt + 30 minutes);
the customer must finish capture inside that window. expiresAt in every response is
Unix time in milliseconds.
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 the case (allow up to about 5 minutes after the
decision), and a review_pending session never expires. 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.
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. At most 2048 characters. Pattern ^https://. |
purpose | string | No | Shown to the customer on the consent screen. Falls back 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. |
hostedOptions.allowedDocumentTypes | array of string | Yes | At least 1 item, at most 4 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. |
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, or steps.emailVerification is not one of off, optional, required), return_url_not_allowed, or email_verification_unavailable (an explicit steps.emailVerification of optional or required was requested but hosted email verification is not enabled for the platform). | email_verification_unavailable, invalid_hosted_options, invalid_id_type, invalid_request, return_url_not_allowed | 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 key lacks the write:identity scope; or kyc_legal_basis_receipt_required (no active hosted-capture legal-basis receipt for your organisation); 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, kyc_legal_basis_receipt_required | application/json KycSessionError |
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 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 |
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. |
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.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. |
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,
"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"
],
"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',
],
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",
],
"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.* webhook |
|---|---|---|
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; it fires later as completed |
completed | A business decision exists: a reviewer approved or overrode the case (or, by design, a machine verified). | 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. | 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; allow up to about 5 minutes between the decision and the delivery. Until then the
only way to observe review_pending is this read. A rejected end state is not currently
expressible by the review vocabulary.
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 emits no webhook at all. 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.
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 |
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. |
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.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. 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 (reported by the status read; no webhook). 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 first opened the link; null until then. |
session.expiresAt | integer (int64) | Yes | The effective deadline. Equal to the mint-time deadline while the link is unopened; once the customer opens it, tightened to min(expiresAt, openedAt + 1800000) (the 30-minute capture window). A session in review_pending does not expire. |
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. |
session.reasonCodes | array of string | Yes | Closed reason-code catalog. provider_manual_review means the case is waiting for a manual review. |
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. |
session.selectedIssuingCountry | string, nullable | Yes | The issuing country the customer chose. |
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: completed to approved, unverified to rejected, review_pending to review_pending, error to error. |
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.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",
"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
},
"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": "pass",
"score": 0.91,
"checks": [],
"reasonCodes": []
},
"liveness": {
"status": "pass",
"score": 0.93,
"checks": [],
"reasonCodes": []
},
"voice": {
"status": "skipped",
"score": 0,
"checks": [],
"reasonCodes": [
"not_required"
]
},
"cross_validation": {
"status": "pass",
"score": 0.99,
"checks": [],
"reasonCodes": []
},
"overall": {
"status": "pass",
"score": 0.94,
"checks": [],
"reasonCodes": []
}
},
"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 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. 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 (reported by the status read; no webhook). 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 tenant’s hosted setup filtered by this session’s options; null when the tenant has no hosted configuration (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 | |
session.configuration{KycHostedSessionConfiguration}.documents[].requireBack | boolean | Yes | |
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. |
session.configuration{KycHostedSessionConfiguration}.options.allowedDocumentTypes | array of string | Yes | At least 1 item, at most 4 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 | |
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: completed to approved, unverified to rejected, review_pending to review_pending, error to error. |
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. 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 (reported by the status read; no webhook). 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 tenant’s hosted setup filtered by this session’s options; null when the tenant has no hosted configuration (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 | |
session.configuration{KycHostedSessionConfiguration}.documents[].requireBack | boolean | Yes | |
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. |
session.configuration{KycHostedSessionConfiguration}.options.allowedDocumentTypes | array of string | Yes | At least 1 item, at most 4 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 | |
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: completed to approved, unverified to rejected, review_pending to review_pending, error to error. |
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, 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 | 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": "jane.doe@example.com",
"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: 'jane.doe@example.com',
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": "jane.doe@example.com",
"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": "jane.doe@example.com",
"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: 'jane.doe@example.com',
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": "jane.doe@example.com",
"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, 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)
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 |
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 | 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 |
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 | 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())
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 |
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 | 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.4.0: kyc.yaml version 1.2.0-2026-10-27 (SHA-256 c91a695cfde0). Do not edit it by hand.