Le nguqulelo yenziwe ngumatshini yaye isalinde ukuphononongwa.Tshintshela esiNgesini
Kweli phepha

Identity verification API

Verify identities, run hosted verification sessions, batch and re-verify, store evidence, read extracted identity details, and receive signed KYC, KYB and KYM webhooks.

EnvironmentBase URL
Productionhttps://collect.noxtica.com

Credential: ApiKeyAuth, SessionTokenAuth. See Authentication.

Verify an identity

POST /v1/kyc/verify

Two branches, selected by the body’s action field (verify is the implicit default when action is omitted — every pre-existing caller is unaffected):

action:'verify' admits, screens, policy-enforces, and finalizes a single identity verification against the tenant’s configured provider. The admission → spend-reservation → provider-call → finalize pipeline is shared byte-for-byte with the hosted-session submit path (POST /v1/kyc/session-submit) and the batch processor — there is exactly one verify pipeline in this system. It requires a nonce obtained from a prior action:'mint' call to the SAME endpoint (the only route that legitimately issues one) or a valid hosted-session-derived nonce.

action:'mint' issues that admission nonce: a fresh, tenant-scoped, single-use nonce bound in KV to the calling tenant (TTL 300s). It is the ONLY way a server-to-server caller can obtain a nonce a subsequent action:'verify' call will accept — without it, evidence-handle/cross-match/reverify are unreachable S2S, since every one of those requires an admitted verificationId a real verify call produces. Mint runs the SAME auth/module-gate/legal-basis preflight as verify, but NEVER meters, NEVER reserves spend, and makes ZERO provider calls.

Your own capture screens. If your application captures the document and selfie itself, mint with evidenceGroup: true. The response also carries an evidenceGroupId. Upload each capture into that group with POST /v1/kyc/evidence/handle (send evidenceGroupId instead of verificationId) and POST /v1/kyc/evidence/finalize, then verify with payload: {evidenceGroupId} and a fresh nonce. Noxtica resolves the uploaded evidence from the group — you never name evidence object ids — and the verificationId of the result equals the evidenceGroupId. A group accepts up to 16 evidence objects, closes after one hour, and serves exactly one verification (a retake needs a new group). A verify that names a group is refused before anything is admitted when the group is unknown (404 evidence_group_not_found, also for another organisation’s group), just expired (410 evidence_group_expired), incomplete (409 evidence_incomplete, with missingEvidence), or malformed (400); no nonce is spent and nothing is billed.

Authentication: Bearer token (ApiKeyAuth), scope write:identity.

Parameters

NameInTypeRequiredDescription
Idempotency-KeyheaderstringYesA 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

FieldTypeRequiredDescription
actionstringYesSelects the mint branch of this endpoint. Allowed values: mint.
evidenceGroupbooleanNoWhen true, the response also carries an evidenceGroupId you upload your own captures into before verifying (see POST /v1/kyc/evidence/handle). Needs evidence storage enabled for your organisation; otherwise the call is a 404. Omitted or false returns the plain nonce. Default false.

Variant KycVerifyVerifyRequest

FieldTypeRequiredDescription
actionstringNoOptional; verify is the implicit default when this field is omitted — every pre-existing caller is unaffected. Allowed values: verify. Default verify.
noncestringYesA fresh, tenant-scoped single-use admission nonce (minted by your own prior client-side flow / SDK step).
id_typestringYesDocument/verification type hint for the resolved provider adapter (aliases: idType).
purposestringNoOne of the closed evidence-purpose catalog values. Default identity_verification.
audiencestringNoDefaults to the caller’s own tenant id.
deviceContextbooleanNoWhen true and device-intel is armed for the tenant, the response carries an ADVISORY deviceTrust block. Never influences the KYC verdict itself.
payloadobjectNoProvider-specific verification payload (never logged or persisted verbatim). verifiedDateOfBirth and minimumAge keys are always stripped server-side before reaching any adapter — supplying them has no effect (defense-in-depth against self-attested age/DOB). With evidenceGroupId the uploaded evidence is resolved from the group; supplying evidenceObjectIds next to it is a 400.
payload.evidenceGroupIdstring (uuid)NoThe evidenceGroupId returned by action:'mint' with evidenceGroup:true, after the document and selfie images were uploaded into it. The result’s verificationId equals this id.
payload.jurisdictionstringNoOptional with evidenceGroupId; must equal your organisation’s evidence residency. Allowed values: TR, EU, US.
payload.operationstringNoOptional with evidenceGroupId; only identity is available through a group. Allowed values: identity.

Responses

StatusMeaningError codesContent
200action:'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
400The 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_requestapplication/json ErrorEnvelope
401The 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_keyapplication/json KycSessionError
402spend_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_reachedapplication/json
403Either 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 scopeapplication/json
404The byte-identical “V1 endpoint not found” response returned when the route’s module is not enabled for your organisation — indistinguishable from a route that does not exist (anti-enumeration by design; do not treat this as proof the path is wrong).V1 endpoint not found, evidence_group_not_foundapplication/json
409action:'verify' only: a conflicting in-flight or idempotency-key-bound request, or — when payload.evidenceGroupId names a group that does not yet hold every required capture — evidence_incomplete with the missing roles in missingEvidence (front, back, and/or selfie). The second form is refused before admission: the nonce is not spent, so retry with the same nonce once the evidence is uploaded.evidence_incompleteapplication/json ConflictError
410action:'verify' with payload.evidenceGroupId: the evidence group has just expired (evidence_group_expired). Groups close after one hour; once the group record is gone the answer is the same 404 as for an unknown group. Mint a new group and upload again.evidence_group_expiredapplication/json KycEvidenceGroupRefusal
413body_too_large — the request exceeded this route’s byte cap.body_too_largeapplication/json ErrorEnvelope
429Rate limit exceeded. Wait Retry-After seconds (also retryAfter in the body) before retrying; the response carries no other rate-limit headers.Rate limit exceededapplication/json
503action:'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_requestapplication/json ErrorEnvelope
503Noxtica could not read the key’s record. This is a temporary fault on Noxtica’s side and says nothing about your key. Retry after the Retry-After delay; do not treat it as a revoked key.api_key_auth_unavailableapplication/json

Response headers

StatusHeaderTypeDescription
429Retry-AfterintegerSeconds until the bucket allows another request.
503Retry-AfterintegerSeconds until a retry is likely to succeed (action:'verify' faults only).

Response fields (200, application/json)

Variant KycVerifyMintResponse

FieldTypeAlways returnedDescription
okbooleanYesAllowed values: true.
noncestring (uuid)YesPass back verbatim as nonce on the follow-up action:'verify' (or omitted-action) call, from the SAME tenant, before it expires.
ttlSecondsintegerYesAbsolute 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].
evidenceGroupIdstring (uuid)NoPresent only when the request set evidenceGroup:true. Pass it as evidenceGroupId to POST /v1/kyc/evidence/handle, then as payload.evidenceGroupId on the verify call.
evidenceGroupExpiresAtintegerNoPresent only with evidenceGroupId. Unix milliseconds at which the group closes.
evidenceGroupTtlSecondsintegerNoPresent only with evidenceGroupId. Group lifetime from mint (3600).

Variant KycVerifyResponse

FieldTypeAlways returnedDescription
okbooleanYesAllowed values: true.
verdictstringYeserror is an infra/provider fault, never a statement about the subject — always HTTP 200 (FR-6). Allowed values: verified, unverified, error.
verificationLevelstringYesAllowed values: verified, partial, unverified, inconclusive.
reasonCodesarray of stringYes
verificationIdstring (uuid)Yes
requestIdstringYes
legalBasisRefstringNoPresent only when this request’s tenant is running under classified legal-basis receipt gating.
nfcAssuranceobjectNoPresent only for eMRTD/passport-chip verification paths.
deviceTrustobjectNoPresent 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

NameInTypeRequiredDescription
verificationIdpathstring (uuid)Yes

Request body

Content type application/json, required.

FieldTypeRequiredDescription
evidenceObjectIdstringYesThe 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

StatusMeaningError codesContent
200A resolved cross-match verdict.application/json
400The 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_requestapplication/json ErrorEnvelope
401The 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_keyapplication/json KycSessionError
403The presented key lacks the required scope.API key does not have write:identity scopeapplication/json
404The 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 foundapplication/json
409The verification has no captured front-document evidence to cross-match against.invalid_requestapplication/json ErrorEnvelope
413body_too_large — the request exceeded this route’s byte cap.body_too_largeapplication/json ErrorEnvelope
429Rate limit exceeded. Wait Retry-After seconds (also retryAfter in the body) before retrying; the response carries no other rate-limit headers.Rate limit exceededapplication/json
503Evidence unavailable, or the owned OCR inference service could not be reached.invalid_requestapplication/json ErrorEnvelope
503Noxtica could not read the key’s record. This is a temporary fault on Noxtica’s side and says nothing about your key. Retry after the Retry-After delay; do not treat it as a revoked key.api_key_auth_unavailableapplication/json

Response headers

StatusHeaderTypeDescription
429Retry-AfterintegerSeconds until the bucket allows another request.
503Retry-AfterintegerSeconds to wait before retrying.

Response fields (200, application/json)

FieldTypeAlways returnedDescription
okbooleanYesAllowed values: true.
verdictstringYesAllowed values: pass, review, reject.
disclosurestringYesThe fixed, always-identical non-authoritative-result disclosure string.
requestIdstringYes

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

NameInTypeRequiredDescription
Idempotency-KeyheaderstringYesA 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.

FieldTypeRequiredDescription
itemsarray of objectYesAt most 500 items.
items[]objectNo
items[].id_typestringYes
items[].purposestringNoDefault identity_verification.
items[].payloadobjectNo

Content type text/csv, required.

Type: string.

Responses

StatusMeaningError codesContent
200A replayed idempotency key — the original batch job, unchanged.application/json KycBatchSubmitResponse
202Accepted; items begin admitting asynchronously.application/json KycBatchSubmitResponse
400The 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_requestapplication/json ErrorEnvelope
401The 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_keyapplication/json KycSessionError
403The presented key lacks the required scope.API key does not have write:identity scopeapplication/json
404The 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 foundapplication/json
413body_too_large — the request exceeded this route’s byte cap.body_too_largeapplication/json ErrorEnvelope
415Unrecognized content type.invalid_requestapplication/json ErrorEnvelope
429Rate limit exceeded. Wait Retry-After seconds (also retryAfter in the body) before retrying; the response carries no other rate-limit headers.Rate limit exceededapplication/json
503Batch admission is temporarily unavailable (digest secret unset).invalid_requestapplication/json ErrorEnvelope
503Noxtica could not read the key’s record. This is a temporary fault on Noxtica’s side and says nothing about your key. Retry after the Retry-After delay; do not treat it as a revoked key.api_key_auth_unavailableapplication/json

Response headers

StatusHeaderTypeDescription
429Retry-AfterintegerSeconds until the bucket allows another request.
503Retry-AfterintegerSeconds to wait before retrying.

Response fields (200, application/json)

FieldTypeAlways returnedDescription
okbooleanNo
batchIdstringNo
statusstringNoAllowed values: processing, completed, cancelled.
itemCountintegerNo
itemsarray of KycBatchSubmittedItemNo
items[]object (KycBatchSubmittedItem)No
items[].rowIndexintegerNo
items[].statusstringNoAllowed values: pending, admitted, evaluating, completed, rejected, cancelled.
items[].errorCodeone of KycBatchItemErrorCode or any, nullableNo
requestIdstringNo

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)

FieldTypeAlways returnedDescription
okbooleanNo
batchIdstringNo
statusstringNoAllowed values: processing, completed, cancelled.
itemCountintegerNo
itemsarray of KycBatchSubmittedItemNo
items[]object (KycBatchSubmittedItem)No
items[].rowIndexintegerNo
items[].statusstringNoAllowed values: pending, admitted, evaluating, completed, rejected, cancelled.
items[].errorCodeone of KycBatchItemErrorCode or any, nullableNo
requestIdstringNo

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

NameInTypeRequiredDescription
batchIdpathstringYes

Responses

StatusMeaningError codesContent
200The job-level aggregate.application/json KycBatchStatusResponse
401The 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_keyapplication/json KycSessionError
403The presented key lacks the required scope.API key does not have write:identity scopeapplication/json
404The 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 foundapplication/json
503Noxtica could not read the key’s record. This is a temporary fault on Noxtica’s side and says nothing about your key. Retry after the Retry-After delay; do not treat it as a revoked key.api_key_auth_unavailableapplication/json

Response headers

StatusHeaderTypeDescription
503Retry-AfterintegerSeconds to wait before retrying.

Response fields (200, application/json)

FieldTypeAlways returnedDescription
okbooleanNo
batchIdstringNo
statusstringNoAllowed values: processing, completed, cancelled.
itemCountintegerNo
pendingCountintegerNo
admittedCountintegerNo
completedCountintegerNo
rejectedCountintegerNo
cancelledCountintegerNo
requestIdstringNo

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

NameInTypeRequiredDescription
batchIdpathstringYes

Responses

StatusMeaningError codesContent
200Every row’s outcome, malformed or not — never silently dropped.application/json KycBatchResultsResponse
401The 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_keyapplication/json KycSessionError
403The presented key lacks the required scope.API key does not have write:identity scopeapplication/json
404The 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 foundapplication/json
503Noxtica could not read the key’s record. This is a temporary fault on Noxtica’s side and says nothing about your key. Retry after the Retry-After delay; do not treat it as a revoked key.api_key_auth_unavailableapplication/json

Response headers

StatusHeaderTypeDescription
503Retry-AfterintegerSeconds to wait before retrying.

Response fields (200, application/json)

FieldTypeAlways returnedDescription
okbooleanNo
batchIdstringNo
itemsarray of objectNo
items[]objectNo
items[].rowIndexintegerNo
items[].statusstringNoAllowed values: pending, admitted, evaluating, completed, rejected, cancelled.
items[].verificationIdstring, nullableNo
items[].verdictstring, nullableNoAllowed values: verified, unverified, error.
items[].errorCodeone of KycBatchItemErrorCode or any, nullableNo
requestIdstringNo

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

NameInTypeRequiredDescription
batchIdpathstringYes

Responses

StatusMeaningError codesContent
200Cancellation applied to every still-pending row.application/json
401The 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_keyapplication/json KycSessionError
403The presented key lacks the required scope.API key does not have write:identity scopeapplication/json
404The 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 foundapplication/json
503Noxtica could not read the key’s record. This is a temporary fault on Noxtica’s side and says nothing about your key. Retry after the Retry-After delay; do not treat it as a revoked key.api_key_auth_unavailableapplication/json

Response headers

StatusHeaderTypeDescription
503Retry-AfterintegerSeconds to wait before retrying.

Response fields (200, application/json)

FieldTypeAlways returnedDescription
okbooleanNo
batchIdstringNo
cancelledCountintegerNo
requestIdstringNo

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.

FieldTypeRequiredDescription
subjectIdstringYes
requiredLevelstringNoAllowed values: verified, partial, unverified, inconclusive.
stepUpReferencestringNoOptional biometric step-up claim reference (single-use). At most 128 characters. Pattern ^[a-zA-Z0-9][a-zA-Z0-9._:-]{0,127}$.

Responses

StatusMeaningError codesContent
200Always 200 (FR-6 fail-closed discipline) — a subject-not-found or infra fault is a verified:false body, never a 5xx.application/json
401The 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_keyapplication/json KycSessionError
403The presented key lacks the required scope.API key does not have write:identity scopeapplication/json
404The 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 foundapplication/json
503Noxtica could not read the key’s record. This is a temporary fault on Noxtica’s side and says nothing about your key. Retry after the Retry-After delay; do not treat it as a revoked key.api_key_auth_unavailableapplication/json

Response headers

StatusHeaderTypeDescription
503Retry-AfterintegerSeconds to wait before retrying.

Response fields (200, application/json)

FieldTypeAlways returnedDescription
okbooleanNo
verifiedbooleanNo
verificationLevelstringNoAllowed values: verified, partial, unverified, inconclusive.
stepUpstringNoAllowed values: satisfied, unconfigured, reference_required, reference_replayed, no_match, error.
requestIdstringNo

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.

FieldTypeRequiredDescription
verificationIdstring (uuid)NoThe verification the evidence belongs to. Exactly one of this and evidenceGroupId is required; naming both, or neither, is a 400.
evidenceGroupIdstring (uuid)NoThe group minted by POST /v1/kyc/verify with action:'mint' and evidenceGroup:true, for captures you upload before the verification exists. Only the image roles document_front, document_back, passport_page and selfie_image are accepted; a group holds at most 16 objects. An unknown, expired, or another organisation’s group is the same 404.
jurisdictionstringYesMust equal the tenant’s configured evidence residency; any mismatch is a 404 (no residency-probe oracle). Allowed values: TR, EU, US.
evidenceKindstringYesClosed 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.
purposestringYesClosed evidence-purpose catalog (e.g. identity_verification, address_verification).
contentTypestringYes
contentLengthintegerYesMinimum 1.

Responses

StatusMeaningError codesContent
200Handle minted.application/json
400The 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_requestapplication/json ErrorEnvelope
401The 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_keyapplication/json KycSessionError
403The presented key lacks the required scope.API key does not have write:identity scopeapplication/json
404The 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 foundapplication/json
413body_too_large — the request exceeded this route’s byte cap.body_too_largeapplication/json ErrorEnvelope
429Rate limit exceeded. Wait Retry-After seconds (also retryAfter in the body) before retrying; the response carries no other rate-limit headers.Rate limit exceededapplication/json
503Evidence storage temporarily unavailable (handle secret or KEK unresolved).invalid_requestapplication/json ErrorEnvelope
503Noxtica could not read the key’s record. This is a temporary fault on Noxtica’s side and says nothing about your key. Retry after the Retry-After delay; do not treat it as a revoked key.api_key_auth_unavailableapplication/json

Response headers

StatusHeaderTypeDescription
429Retry-AfterintegerSeconds until the bucket allows another request.
503Retry-AfterintegerSeconds to wait before retrying.

Response fields (200, application/json)

FieldTypeAlways returnedDescription
okbooleanNo
handlestringNoOpaque signed single-use upload handle.
dekstringNoBase64 plaintext DEK — hold only in memory
wrappedDekstringNoThe same DEK wrapped under the active KEK
envelopeobjectNo
envelope.envelope_versionintegerNo
envelope.cipher_suitestringNo
envelope.kek_kidstringNo
uploadTargetobjectNo
uploadTarget.objectIdstringNo
uploadTarget.schemaVersionintegerNo
expiresAtintegerNoUnix ms handle expiry.
evidenceGroupIdstring (uuid)NoEcho of the request’s evidenceGroupId; present only for an evidence-group handle.
requestIdstringNo

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.

FieldTypeRequiredDescription
handlestringYesThe opaque handle from the mint step.
objectIdstringYes
commitmentobjectYes
commitment.total_chunksintegerYesMinimum 1.
commitment.total_plaintext_lengthintegerYesMinimum 0.
chunksarray of stringYesChunked-AEAD-sealed ciphertext chunks (schema version carried in the mint-step envelope).

Responses

StatusMeaningError codesContent
200Terminal inspection outcome.application/json
400The 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_requestapplication/json ErrorEnvelope
401The 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_keyapplication/json KycSessionError
403The presented handle failed verification (signature, expiry, manifest, or binding mismatch) — a single uniform refusal, no probe oracle.invalid_requestapplication/json ErrorEnvelope
404The 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 foundapplication/json
413body_too_large — the request exceeded this route’s byte cap.body_too_largeapplication/json ErrorEnvelope
429Rate limit exceeded. Wait Retry-After seconds (also retryAfter in the body) before retrying; the response carries no other rate-limit headers.Rate limit exceededapplication/json
503Evidence storage temporarily unavailable.invalid_requestapplication/json ErrorEnvelope
503Noxtica could not read the key’s record. This is a temporary fault on Noxtica’s side and says nothing about your key. Retry after the Retry-After delay; do not treat it as a revoked key.api_key_auth_unavailableapplication/json

Response headers

StatusHeaderTypeDescription
429Retry-AfterintegerSeconds until the bucket allows another request.
503Retry-AfterintegerSeconds to wait before retrying.

Response fields (200, application/json)

FieldTypeAlways returnedDescription
okbooleanYesAllowed values: true.
statestringYesAllowed values: usable, rejected.
reasonCodestringNoPresent only when state is rejected. Allowed values: evidence_kind_forbidden, evidence_mime_mismatch, evidence_size_mismatch, evidence_pixels_invalid, evidence_unreadable.
objectIdstringYes
requestIdstringYes

Response example (200)

{
  "ok": true,
  "state": "usable",
  "reasonCode": "evidence_kind_forbidden",
  "objectId": "example-objectId",
  "requestId": "req-test"
}

Example request

curl

curl -X POST "https://collect.noxtica.com/v1/kyc/evidence/finalize" \
  -H "Authorization: Bearer $NOXTICA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "handle": "example-handle",
  "objectId": "example-objectId",
  "commitment": {
    "total_chunks": 1,
    "total_plaintext_length": 0
  },
  "chunks": [
    "example-chunks"
  ]
}'

Node.js (global fetch, Node 18 or later)

const response = await fetch('https://collect.noxtica.com/v1/kyc/evidence/finalize', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.NOXTICA_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    handle: 'example-handle',
    objectId: 'example-objectId',
    commitment: {
      total_chunks: 1,
      total_plaintext_length: 0,
    },
    chunks: [
      'example-chunks',
    ],
  }),
});
console.log(response.status, await response.text());

Python (standard library)

import json
import os
import urllib.request

request = urllib.request.Request(
    "https://collect.noxtica.com/v1/kyc/evidence/finalize",
    data=json.dumps({
        "handle": "example-handle",
        "objectId": "example-objectId",
        "commitment": {
            "total_chunks": 1,
            "total_plaintext_length": 0,
        },
        "chunks": [
            "example-chunks",
        ],
    }).encode(),
    headers={
        "Authorization": f"Bearer {os.environ['NOXTICA_API_KEY']}",
        "Content-Type": "application/json",
    },
    method="POST",
)
with urllib.request.urlopen(request) as response:
    print(response.status, response.read().decode())

List the evidence images of a verification

GET /v1/kyc/verifications/{verificationId}/evidence

Server-to-server. Lists the images a person captured during one of your own verifications, so that your own operators can look at them: the front and back of an identity document, a passport page, a proof of address, a selfie, a capture of the document held next to the face, and supporting documents. Only still images whose storage finished successfully are listed. Videos, voice recordings, the identity details Noxtica extracted (read those with GET /v1/kyc/verifications/{verificationId}/identity) and anything Noxtica derived from the images are never listed and cannot be opened through these routes. The response holds no image bytes: ask for one with POST /v1/kyc/verifications/{verificationId}/evidence/{itemId}/access, then fetch the signed URL it returns.

Opt-in. The key needs the read:evidence scope, and every evidence image route answers 403 evidence_read_not_enabled until a tenant administrator enables evidence image access for your organisation in the Backoffice (KYC → Invitations → Settings tab, card “Evidence image API”): the administrator records the legal basis for receiving these images, Article 6 and Article 9 of the GDPR, with step-up multi-factor authentication. Identity document images and selfies are special-category personal data: enable the access only for the operators who need it. Evidence storage must also be armed for your organisation (card “Evidence storage” on the same tab), otherwise the answer is 403 evidence_storage_not_armed. While Noxtica has the evidence image API off for the platform, every route answers the byte-identical 404 of an unknown path. A key keeps its scopes until a tenant administrator changes them (Edit scopes on the key in the Backoffice, API Configuration): add read:evidence to the key, or create a new key that carries it. When a tenant administrator withdraws the enablement, the very next request is refused, a signed URL that has not expired yet included.

Which id. verificationId is the id returned by POST /v1/kyc/verify (a UUID) or the verificationId of a hosted session (hosted: followed by 64 lower-case hexadecimal characters); see KycVerificationId for the formats and the 128-character maximum. The id of a hosted session and the evidenceGroupId of a verification made from your own capture screens are accepted too and resolve to the same verification. An id that is malformed, unknown, or belongs to another organisation gets the same byte-identical 404 as an unknown path.

What is listed. Newest first, at most 100 items, only images that are ready to be opened. type and side tell the images apart:

typesideThe image shows
documentfront or backOne side of an identity document.
passport_pagenullA page of a passport.
proof_of_addressnullA proof of address.
selfienullA selfie of the person.
document_near_facenullThe document held next to the face.
supporting_documentnullA supporting document the person added.

contentType is image/jpeg, image/png or image/webp, size is the size in bytes and capturedAt is when Noxtica received the image. An item has no hash, purpose or storage detail.

Audit and handling. Listing does not release an image and is not itself recorded in the access log; asking for access and fetching an image are (see kycVerificationEvidenceAccess and kycEvidenceContent). Every response of this route carries Cache-Control: no-store, private. Images are never sent in webhooks.

Rate limit. A per-key limit applies to this route, the access route and the access log together; a limited request answers 429 with Retry-After. Fetching a signed URL has a separate per-key limit.

Authentication: Bearer token (ApiKeyAuth), scope read:evidence.

Parameters

NameInTypeRequiredDescription
verificationIdpathstringYesThe verificationId of the verification. Pattern ^[A-Za-z0-9:_-]{1,128}$.

Responses

StatusMeaningError codesContent
200The images of the verification that can be opened, newest first.application/json KycEvidenceList
400invalid_request — the request could not be parsed, or a query value is out of range. Nothing was read.invalid_requestapplication/json ErrorEnvelope
401The 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_keyapplication/json KycSessionError
403evidence_read_not_enabled — a tenant administrator has not enabled evidence image access for your organisation (or withdrew it), so nothing was read; evidence_storage_not_armed — evidence storage is not on for your organisation; or the key lacks the read:evidence scope (error is API key does not have read:evidence scope). A tenant administrator fixes the first two in the Backoffice, KYC → Invitations → Settings tab (cards “Evidence image API” and “Evidence storage”).evidence_read_not_enabled, evidence_storage_not_armedapplication/json KycEvidenceForbiddenError
404The byte-identical “V1 endpoint not found” response, returned for a malformed id, an id that does not exist, an id or an image that belongs to another organisation or to another verification, an item that is not a readable image, or a route that is not available for your organisation: indistinguishable from an unknown path by design.V1 endpoint not foundapplication/json
410verification_deleted — you deleted this verification; its images no longer exist.verification_deletedapplication/json ErrorEnvelope
429Rate limit exceeded. Wait Retry-After seconds (also retryAfter in the body) before retrying; the response carries no other rate-limit headers.Rate limit exceededapplication/json
500API-key authentication is not configured on this deployment (server_misconfigured). Retrying does not help; contact support with the requestId.server_misconfiguredapplication/json KycSessionError
503evidence_audit_unavailable or evidence_unavailable — retryable. The access could not be recorded, so nothing was released; or the stored images could not be read right now.evidence_audit_unavailable, evidence_unavailableapplication/json ErrorEnvelope
503Noxtica could not read the key’s record. This is a temporary fault on Noxtica’s side and says nothing about your key. Retry after the Retry-After delay; do not treat it as a revoked key.api_key_auth_unavailableapplication/json

Response headers

StatusHeaderTypeDescription
200Cache-ControlstringAlways no-store, private. Never cache or store a response of an evidence image route.
400Cache-ControlstringAlways no-store, private. Never cache or store a response of an evidence image route.
403Cache-ControlstringAlways no-store, private. Never cache or store a response of an evidence image route.
404Cache-ControlstringAlways no-store, private. Never cache or store a response of an evidence image route.
410Cache-ControlstringAlways no-store, private. Never cache or store a response of an evidence image route.
429Retry-AfterintegerSeconds until the bucket allows another request.
503Cache-ControlstringAlways no-store, private. Never cache or store a response of an evidence image route.
503Retry-AfterintegerSeconds to wait before retrying.

Response fields (200, application/json)

FieldTypeAlways returnedDescription
okbooleanYesAllowed values: true.
verificationIdstringYesThe identifier of one verification. Treat it as an opaque string and store it in a column that holds at least 128 characters. The API contract fixes two limits that hold for every id: it is never longer than 128 characters, and it only contains the characters A-Z, a-z, 0-9, :, _ and -. Today an id has one of two shapes. A verification created through POST /v1/kyc/verify (including one made from an evidenceGroupId, and each item of a batch) is a lower-case UUID: 36 characters, for example 3fa85f64-5717-4562-b3fc-2c963f66afa6. The verification a hosted session runs when the customer submits is hosted: followed by 64 lower-case hexadecimal characters: 71 characters in all. Do not parse an id or rely on its prefix; a new shape may appear, but always within the two limits above. At least 1 character, at most 128 characters. Pattern ^[A-Za-z0-9:_-]{1,128}$.
itemsarray of KycEvidenceItemYesAt most 100 items.
items[]object (KycEvidenceItem)NoOne image a person captured during a verification. It holds no image bytes, hash or storage detail.
items[].itemIdstringYesThe opaque id of the image. Use it as itemId of the access route. Store it in a column that holds at least 128 characters. Pattern ^[A-Za-z0-9_-]{1,128}$.
items[].typestringYesWhat the image shows. Videos, voice recordings and extracted data are never listed. Allowed values: document, passport_page, proof_of_address, selfie, document_near_face, supporting_document.
items[].sidestring, nullableYesfront or back for a document; null for every other type. Allowed values: front, back, null.
items[].contentTypestringYesThe type of the image. Allowed values: image/jpeg, image/png, image/webp.
items[].sizeintegerYesThe size of the image in bytes. Minimum 0.
items[].capturedAtstring (date-time)YesWhen Noxtica received the image.
requestIdstringYes

Response example (200)

{
  "ok": true,
  "verificationId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "items": [
    {
      "itemId": "evd_example_0003",
      "type": "selfie",
      "side": null,
      "contentType": "image/png",
      "size": 95311,
      "capturedAt": "2026-11-05T10:12:40.000Z"
    },
    {
      "itemId": "evd_example_0002",
      "type": "document",
      "side": "back",
      "contentType": "image/jpeg",
      "size": 171903,
      "capturedAt": "2026-11-05T10:11:58.000Z"
    },
    {
      "itemId": "evd_example_0001",
      "type": "document",
      "side": "front",
      "contentType": "image/jpeg",
      "size": 183422,
      "capturedAt": "2026-11-05T10:11:12.000Z"
    }
  ],
  "requestId": "req-test"
}

Example request

curl

curl "https://collect.noxtica.com/v1/kyc/verifications/example-verificationId/evidence" \
  -H "Authorization: Bearer $NOXTICA_API_KEY"

Node.js (global fetch, Node 18 or later)

const response = await fetch('https://collect.noxtica.com/v1/kyc/verifications/example-verificationId/evidence', {
  method: 'GET',
  headers: {
    Authorization: `Bearer ${process.env.NOXTICA_API_KEY}`,
  },
});
console.log(response.status, await response.text());

Python (standard library)

import os
import urllib.request

request = urllib.request.Request(
    "https://collect.noxtica.com/v1/kyc/verifications/example-verificationId/evidence",
    headers={
        "Authorization": f"Bearer {os.environ['NOXTICA_API_KEY']}",
    },
    method="GET",
)
with urllib.request.urlopen(request) as response:
    print(response.status, response.read().decode())

Get a short-lived signed URL for one evidence image

POST /v1/kyc/verifications/{verificationId}/evidence/{itemId}/access

Server-to-server. Returns a signed URL for one image listed by GET /v1/kyc/verifications/{verificationId}/evidence. Hand the URL to the operator’s browser, or fetch it from your backend and pass the bytes on. The URL is the credential: GET /v1/kyc/evidence/content needs no Authorization header. The key needs the read:evidence scope, and the opt-in described on kycVerificationEvidenceList applies.

The URL. It is valid for 120 seconds from the grant (see expiresAt). The lifetime is fixed: it cannot be extended or requested longer. Within that time the URL can be fetched more than once, for example when a browser reloads the image; every fetch is recorded and rate limited. Anyone who holds the URL can fetch the image until it expires, so treat it as a secret: give it only to the operator who asked to open the image, never log or store it, and never put it on a page that loads content from other sites. Ask for a new URL instead of keeping one. The URL has the host you called, and its query holds an opaque token: do not parse, shorten or rebuild it.

What stops a URL early. A URL stops working at once when the key that asked for it is revoked or loses the read:evidence scope, when a tenant administrator withdraws the enablement, and when the image no longer exists.

Which ids. verificationId is accepted in the same forms as on kycVerificationEvidenceList. itemId is the itemId of an item of that list. A malformed or unknown id, an item that belongs to another verification or another organisation, and an item that is not a readable image (a video, a voice recording, extracted data) all get the same byte-identical 404 as an unknown path.

Availability. An item of this verification that is an image but whose storage did not finish successfully answers 409 evidence_not_available with reason not_usable. A verification you deleted answers 410 verification_deleted.

Audit and handling. Each grant is recorded for your organisation before the URL is released: the key, the verification, the type of image, the time and, as Noxtica saw them, the caller address and user agent; never the image. If the record cannot be written, nothing is released and the answer is 503 evidence_audit_unavailable. Read the record with GET /v1/kyc/verifications/{verificationId}/evidence/access-log. Every response of this route carries Cache-Control: no-store, private: do not cache or log the body.

Request body. None is needed. An empty JSON object is accepted and ignored.

Rate limit. The same per-key limit as kycVerificationEvidenceList.

Authentication: Bearer token (ApiKeyAuth), scope read:evidence.

Parameters

NameInTypeRequiredDescription
verificationIdpathstringYesThe verificationId of the verification. Pattern ^[A-Za-z0-9:_-]{1,128}$.
itemIdpathstringYesThe itemId of an item of the evidence list of that verification. Pattern ^[A-Za-z0-9_-]{1,128}$.

Request body

Content type application/json, optional.

Type: object.

Responses

StatusMeaningError codesContent
200A signed URL for the image. It is valid until expiresAt, 120 seconds after the grant.application/json KycEvidenceAccess
400invalid_request — the request could not be parsed, or a query value is out of range. Nothing was read.invalid_requestapplication/json ErrorEnvelope
401The 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_keyapplication/json KycSessionError
403evidence_read_not_enabled — a tenant administrator has not enabled evidence image access for your organisation (or withdrew it), so nothing was read; evidence_storage_not_armed — evidence storage is not on for your organisation; or the key lacks the read:evidence scope (error is API key does not have read:evidence scope). A tenant administrator fixes the first two in the Backoffice, KYC → Invitations → Settings tab (cards “Evidence image API” and “Evidence storage”).evidence_read_not_enabledapplication/json KycEvidenceForbiddenError
404The byte-identical “V1 endpoint not found” response, returned for a malformed id, an id that does not exist, an id or an image that belongs to another organisation or to another verification, an item that is not a readable image, or a route that is not available for your organisation: indistinguishable from an unknown path by design.V1 endpoint not foundapplication/json
409evidence_not_available — the item is an image of this verification, but its storage did not finish successfully, so it cannot be opened. reason is not_usable.evidence_not_availableapplication/json KycEvidenceNotAvailableError
410verification_deleted — you deleted this verification; its images no longer exist.verification_deletedapplication/json ErrorEnvelope
429Rate limit exceeded. Wait Retry-After seconds (also retryAfter in the body) before retrying; the response carries no other rate-limit headers.Rate limit exceededapplication/json
500API-key authentication is not configured on this deployment (server_misconfigured). Retrying does not help; contact support with the requestId.server_misconfiguredapplication/json KycSessionError
503evidence_audit_unavailable or evidence_unavailable — retryable. The access could not be recorded, so nothing was released; or the stored images could not be read right now.evidence_audit_unavailable, evidence_unavailableapplication/json ErrorEnvelope
503Noxtica could not read the key’s record. This is a temporary fault on Noxtica’s side and says nothing about your key. Retry after the Retry-After delay; do not treat it as a revoked key.api_key_auth_unavailableapplication/json

Response headers

StatusHeaderTypeDescription
200Cache-ControlstringAlways no-store, private. Never cache or store a response of an evidence image route.
400Cache-ControlstringAlways no-store, private. Never cache or store a response of an evidence image route.
403Cache-ControlstringAlways no-store, private. Never cache or store a response of an evidence image route.
404Cache-ControlstringAlways no-store, private. Never cache or store a response of an evidence image route.
409Cache-ControlstringAlways no-store, private. Never cache or store a response of an evidence image route.
410Cache-ControlstringAlways no-store, private. Never cache or store a response of an evidence image route.
429Retry-AfterintegerSeconds until the bucket allows another request.
503Cache-ControlstringAlways no-store, private. Never cache or store a response of an evidence image route.
503Retry-AfterintegerSeconds to wait before retrying.

Response fields (200, application/json)

FieldTypeAlways returnedDescription
okbooleanYesAllowed values: true.
verificationIdstringYesThe identifier of one verification. Treat it as an opaque string and store it in a column that holds at least 128 characters. The API contract fixes two limits that hold for every id: it is never longer than 128 characters, and it only contains the characters A-Z, a-z, 0-9, :, _ and -. Today an id has one of two shapes. A verification created through POST /v1/kyc/verify (including one made from an evidenceGroupId, and each item of a batch) is a lower-case UUID: 36 characters, for example 3fa85f64-5717-4562-b3fc-2c963f66afa6. The verification a hosted session runs when the customer submits is hosted: followed by 64 lower-case hexadecimal characters: 71 characters in all. Do not parse an id or rely on its prefix; a new shape may appear, but always within the two limits above. At least 1 character, at most 128 characters. Pattern ^[A-Za-z0-9:_-]{1,128}$.
itemIdstringYesThe image the URL opens. Pattern ^[A-Za-z0-9_-]{1,128}$.
urlstring (uri)YesThe signed URL, on the host you called. Fetch it with GET and no Authorization header. Treat it as a secret; do not log or store it.
expiresAtstring (date-time)YesWhen the URL stops working.
expiresInSecondsintegerYesThe fixed lifetime of the URL, in seconds. Allowed values: 120.
contentTypestringYesThe type of the image the URL returns. Allowed values: image/jpeg, image/png, image/webp.
sizeintegerYesThe size of the image in bytes. Minimum 0.
requestIdstringYes

Response example (200)

{
  "ok": true,
  "verificationId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "itemId": "evd_example_0001",
  "url": "https://collect.noxtica.com/v1/kyc/evidence/content?token=v1.EXAMPLE_PAYLOAD.EXAMPLE_SIGNATURE",
  "expiresAt": "2026-11-05T10:13:12.000Z",
  "expiresInSeconds": 120,
  "contentType": "image/jpeg",
  "size": 183422,
  "requestId": "req-test"
}

Example request

curl

curl -X POST "https://collect.noxtica.com/v1/kyc/verifications/example-verificationId/evidence/example-itemId/access" \
  -H "Authorization: Bearer $NOXTICA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{}'

Node.js (global fetch, Node 18 or later)

const response = await fetch('https://collect.noxtica.com/v1/kyc/verifications/example-verificationId/evidence/example-itemId/access', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.NOXTICA_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({}),
});
console.log(response.status, await response.text());

Python (standard library)

import json
import os
import urllib.request

request = urllib.request.Request(
    "https://collect.noxtica.com/v1/kyc/verifications/example-verificationId/evidence/example-itemId/access",
    data=json.dumps({}).encode(),
    headers={
        "Authorization": f"Bearer {os.environ['NOXTICA_API_KEY']}",
        "Content-Type": "application/json",
    },
    method="POST",
)
with urllib.request.urlopen(request) as response:
    print(response.status, response.read().decode())

Fetch an evidence image with a signed URL

GET /v1/kyc/evidence/content

Returns the bytes of one image. The signed URL that kycVerificationEvidenceAccess returned is the credential: send no Authorization header, and use the URL exactly as it was given.

The response. 200 carries the image itself. Content-Type is the type of the stored image (image/jpeg, image/png or image/webp), Content-Disposition is inline; filename="evidence-<itemId>.<jpg|png|webp>" and Content-Length is the exact size. Every response of this route, errors included, carries Cache-Control: no-store, private, X-Content-Type-Options: nosniff and Referrer-Policy: no-referrer: do not cache or store the response.

Checks on every fetch. A URL that was altered, is malformed or has expired answers 401 invalid_or_expired_url; the body is the same for every one of those causes. The same 401 follows when the key that asked for the URL has been revoked or no longer carries the read:evidence scope. The tenant administrator’s enablement and evidence storage must still be in place, otherwise the answer is 403 evidence_read_not_enabled or 403 evidence_storage_not_armed. An image that no longer exists, or that is not a readable image of the verification the URL was granted for, answers the byte-identical 404.

Audit. Each fetch is recorded for your organisation before any byte is released, with the key, the verification, the type of image, the time and, as Noxtica saw them, the caller address and user agent. If the record cannot be written, nothing is released and the answer is 503 evidence_audit_unavailable. A request that is refused before that point is not recorded. The record is readable with GET /v1/kyc/verifications/{verificationId}/evidence/access-log.

Stored image check. Noxtica checks that the bytes are an image of the type they were stored as before serving them. If they are not, nothing is served and the answer is 502 evidence_unreadable.

Rate limit. A per-key limit applies to fetches, separate from the one on the other evidence image routes; a limited request answers 429 with Retry-After.

Authentication: none. This operation does not take a credential.

Parameters

NameInTypeRequiredDescription
tokenquerystringYesThe signed token of the URL returned by kycVerificationEvidenceAccess. Use the URL as returned; do not build or change the token. At least 1 character.

Responses

StatusMeaningError codesContent
200The image.image/jpeg, image/png, image/webp
401invalid_or_expired_url — the URL was altered, is malformed or has expired, or the key that asked for it is revoked or no longer carries the read:evidence scope. The body is the same for every cause.invalid_or_expired_urlapplication/json ErrorEnvelope
403evidence_read_not_enabled — a tenant administrator has withdrawn (or never gave) the enablement for your organisation; or evidence_storage_not_armed — evidence storage is not on for your organisation. Nothing was released. A tenant administrator fixes either in the Backoffice, KYC → Invitations → Settings tab (cards “Evidence image API” and “Evidence storage”).evidence_read_not_enabled, evidence_storage_not_armedapplication/json ErrorEnvelope
404The byte-identical “V1 endpoint not found” response, returned for a malformed id, an id that does not exist, an id or an image that belongs to another organisation or to another verification, an item that is not a readable image, or a route that is not available for your organisation: indistinguishable from an unknown path by design.V1 endpoint not foundapplication/json
429Rate limit exceeded. Wait Retry-After seconds (also retryAfter in the body) before retrying; the response carries no other rate-limit headers.Rate limit exceededapplication/json
502evidence_unreadable — the stored bytes are not an image of the type they were stored as, so nothing was served. The fetch record had already been written.evidence_unreadableapplication/json ErrorEnvelope
503evidence_audit_unavailable or evidence_unavailable — retryable. The access could not be recorded, so nothing was released; or the stored images could not be read right now.evidence_audit_unavailable, evidence_unavailableapplication/json ErrorEnvelope

Response headers

StatusHeaderTypeDescription
200Cache-ControlstringAlways no-store, private. Never cache or store a response of an evidence image route.
200Content-DispositionstringAlways inline, with the file name evidence-<itemId>.<jpg|png|webp>; the extension follows the image type.
200Content-LengthintegerThe exact size of the image in bytes.
200X-Content-Type-OptionsstringAlways nosniff.
401Cache-ControlstringAlways no-store, private. Never cache or store a response of an evidence image route.
403Cache-ControlstringAlways no-store, private. Never cache or store a response of an evidence image route.
404Cache-ControlstringAlways no-store, private. Never cache or store a response of an evidence image route.
429Retry-AfterintegerSeconds until the bucket allows another request.
502Cache-ControlstringAlways no-store, private. Never cache or store a response of an evidence image route.
503Cache-ControlstringAlways no-store, private. Never cache or store a response of an evidence image route.

Example request

curl

curl "https://collect.noxtica.com/v1/kyc/evidence/content?token=v1.EXAMPLE_PAYLOAD.EXAMPLE_SIGNATURE"

Node.js (global fetch, Node 18 or later)

const response = await fetch('https://collect.noxtica.com/v1/kyc/evidence/content?token=v1.EXAMPLE_PAYLOAD.EXAMPLE_SIGNATURE', {
  method: 'GET',
});
console.log(response.status, await response.text());

Python (standard library)

import urllib.request

request = urllib.request.Request(
    "https://collect.noxtica.com/v1/kyc/evidence/content?token=v1.EXAMPLE_PAYLOAD.EXAMPLE_SIGNATURE",
    method="GET",
)
with urllib.request.urlopen(request) as response:
    print(response.status, response.read().decode())

List who opened the evidence images of a verification

GET /v1/kyc/verifications/{verificationId}/evidence/access-log

Server-to-server. Lists the openings of the images of one of your own verifications, newest first: one row for each access grant and one for each fetch of a signed URL. Use it to answer who looked at an image, from where and when. A row never holds image bytes, a signed URL or its token. The key needs the read:evidence scope.

After the enablement is withdrawn. Unlike the other evidence image routes, this one does not need the tenant administrator’s enablement to be in place, nor evidence storage to be on, so you can still read the history after the enablement was withdrawn. It is not available while evidence image access is not available for your organisation (404).

Which id. verificationId is accepted in the same forms as on kycVerificationEvidenceList; a malformed, unknown or foreign id gets the same byte-identical 404. A verification you deleted answers 410 verification_deleted.

Rows. event is grant (a signed URL was issued) or fetch (the image was served). itemId is the image, apiKeyId the key that asked for the URL, and at the time. ip and userAgent are what Noxtica saw on the request (the address the request came from and its User-Agent header, shortened to 256 characters) and are null when there was none.

Paging. limit is 1 to 100 (default 50). When more rows exist, nextBefore is a cursor in Unix milliseconds: pass it unchanged as before to read the next page. It is null on the last page. A limit or before that is repeated, not a whole number or out of range answers 400 invalid_request.

Handling. Every response of this route carries Cache-Control: no-store, private.

Rate limit. The same per-key limit as kycVerificationEvidenceList.

Authentication: Bearer token (ApiKeyAuth), scope read:evidence.

Parameters

NameInTypeRequiredDescription
verificationIdpathstringYesThe verificationId of the verification. Pattern ^[A-Za-z0-9:_-]{1,128}$.
limitqueryintegerNoThe most rows to return. Minimum 1, maximum 100. Default 50.
beforequeryinteger (int64)NoA cursor in Unix milliseconds. Only rows older than it are returned. Use nextBefore of the previous page unchanged. Minimum 1.

Responses

StatusMeaningError codesContent
200The recorded openings of the images of the verification, newest first.application/json KycEvidenceAccessLog
400invalid_request — the request could not be parsed, or a query value is out of range. Nothing was read.invalid_requestapplication/json ErrorEnvelope
401The 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_keyapplication/json KycSessionError
403evidence_read_not_enabled — a tenant administrator has not enabled evidence image access for your organisation (or withdrew it), so nothing was read; evidence_storage_not_armed — evidence storage is not on for your organisation; or the key lacks the read:evidence scope (error is API key does not have read:evidence scope). A tenant administrator fixes the first two in the Backoffice, KYC → Invitations → Settings tab (cards “Evidence image API” and “Evidence storage”).evidence_read_not_enabledapplication/json KycEvidenceForbiddenError
404The byte-identical “V1 endpoint not found” response, returned for a malformed id, an id that does not exist, an id or an image that belongs to another organisation or to another verification, an item that is not a readable image, or a route that is not available for your organisation: indistinguishable from an unknown path by design.V1 endpoint not foundapplication/json
410verification_deleted — you deleted this verification; its images no longer exist.verification_deletedapplication/json ErrorEnvelope
429Rate limit exceeded. Wait Retry-After seconds (also retryAfter in the body) before retrying; the response carries no other rate-limit headers.Rate limit exceededapplication/json
500API-key authentication is not configured on this deployment (server_misconfigured). Retrying does not help; contact support with the requestId.server_misconfiguredapplication/json KycSessionError
503evidence_audit_unavailable or evidence_unavailable — retryable. The access could not be recorded, so nothing was released; or the stored images could not be read right now.evidence_audit_unavailable, evidence_unavailableapplication/json ErrorEnvelope
503Noxtica could not read the key’s record. This is a temporary fault on Noxtica’s side and says nothing about your key. Retry after the Retry-After delay; do not treat it as a revoked key.api_key_auth_unavailableapplication/json

Response headers

StatusHeaderTypeDescription
200Cache-ControlstringAlways no-store, private. Never cache or store a response of an evidence image route.
400Cache-ControlstringAlways no-store, private. Never cache or store a response of an evidence image route.
403Cache-ControlstringAlways no-store, private. Never cache or store a response of an evidence image route.
404Cache-ControlstringAlways no-store, private. Never cache or store a response of an evidence image route.
410Cache-ControlstringAlways no-store, private. Never cache or store a response of an evidence image route.
429Retry-AfterintegerSeconds until the bucket allows another request.
503Cache-ControlstringAlways no-store, private. Never cache or store a response of an evidence image route.
503Retry-AfterintegerSeconds to wait before retrying.

Response fields (200, application/json)

FieldTypeAlways returnedDescription
okbooleanYesAllowed values: true.
verificationIdstringYesThe identifier of one verification. Treat it as an opaque string and store it in a column that holds at least 128 characters. The API contract fixes two limits that hold for every id: it is never longer than 128 characters, and it only contains the characters A-Z, a-z, 0-9, :, _ and -. Today an id has one of two shapes. A verification created through POST /v1/kyc/verify (including one made from an evidenceGroupId, and each item of a batch) is a lower-case UUID: 36 characters, for example 3fa85f64-5717-4562-b3fc-2c963f66afa6. The verification a hosted session runs when the customer submits is hosted: followed by 64 lower-case hexadecimal characters: 71 characters in all. Do not parse an id or rely on its prefix; a new shape may appear, but always within the two limits above. At least 1 character, at most 128 characters. Pattern ^[A-Za-z0-9:_-]{1,128}$.
eventsarray of KycEvidenceAccessEventYes
events[]object (KycEvidenceAccessEvent)NoOne recorded opening of an image. It never holds image bytes, a signed URL or its token.
events[].idstringYesThe id of the record.
events[].atstring (date-time)YesWhen it happened.
events[].eventstringYesgrant — a signed URL was issued. fetch — the image was served. Allowed values: grant, fetch.
events[].itemIdstringYesThe image.
events[].apiKeyIdstringYesThe id of the API key that asked for the URL.
events[].ipstring, nullableYesThe address the request came from as Noxtica saw it; null when there was none.
events[].userAgentstring, nullableYesThe User-Agent of the request, at most 256 characters; null when there was none. At most 256 characters.
nextBeforeinteger (int64), nullableYesA cursor in Unix milliseconds. Pass it unchanged as before to read the next page; null on the last page.
requestIdstringYes

Response example (200)

{
  "ok": true,
  "verificationId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "events": [
    {
      "id": "req-example-0002",
      "at": "2026-11-05T10:11:40.000Z",
      "event": "fetch",
      "itemId": "evd_example_0001",
      "apiKeyId": "key_example_01",
      "ip": "203.0.113.7",
      "userAgent": "ExampleBackend/1.0"
    },
    {
      "id": "req-example-0001",
      "at": "2026-11-05T10:11:12.000Z",
      "event": "grant",
      "itemId": "evd_example_0001",
      "apiKeyId": "key_example_01",
      "ip": "203.0.113.7",
      "userAgent": "ExampleBackend/1.0"
    }
  ],
  "nextBefore": null,
  "requestId": "req-test"
}

Example request

curl

curl "https://collect.noxtica.com/v1/kyc/verifications/example-verificationId/evidence/access-log" \
  -H "Authorization: Bearer $NOXTICA_API_KEY"

Node.js (global fetch, Node 18 or later)

const response = await fetch('https://collect.noxtica.com/v1/kyc/verifications/example-verificationId/evidence/access-log', {
  method: 'GET',
  headers: {
    Authorization: `Bearer ${process.env.NOXTICA_API_KEY}`,
  },
});
console.log(response.status, await response.text());

Python (standard library)

import os
import urllib.request

request = urllib.request.Request(
    "https://collect.noxtica.com/v1/kyc/verifications/example-verificationId/evidence/access-log",
    headers={
        "Authorization": f"Bearer {os.environ['NOXTICA_API_KEY']}",
    },
    method="GET",
)
with urllib.request.urlopen(request) as response:
    print(response.status, response.read().decode())

Mint a Noxtica-hosted KYC verification-session link

POST /v1/kyc/sessions

Server-to-server. Creates a hosted verification session and returns a one-shot, opaque URL to hand to your end customer (redirect, email, SMS or QR). The URL embeds a bearer session token that is never returned again by any route: treat it as a secret, do not log it and do not put it in an analytics URL. Minting is free; the verification is counted when the customer submits.

Before you can mint. (1) The identity verification module must be enabled for your organisation; until it is, this route answers the same byte-identical 404 as an unknown path. (2) Your sk_ key needs the write:identity scope (a key created with the default scope is refused with 403). (3) A tenant administrator must have recorded an active legal-basis receipt for hosted document capture in the Backoffice; without it the mint is refused with 403 kyc_legal_basis_receipt_required. Noxtica never selects a legal basis for you. (4) Your organisation needs a hosted configuration (business name, privacy URL, supported documents) in the Backoffice, or the customer sees “Verification isn’t ready yet”; the mint itself does not check this. (5) Your organisation’s raw-evidence storage must be provisioned in the Backoffice: evidence storage armed and an evidence residency of TR, EU or US set. A link for an organisation that cannot store the customer’s capture would fail after the first photo, so the mint is refused before any session exists with 403 evidence_not_provisioned and a reason (raw_storage_not_armed or residency_unconfigured). Retrying does not help until the organisation is provisioned.

Request. subjectRef is required. The expiry field is expiresInSeconds: a key named ttlSeconds is not read and is silently ignored (the link then lasts the 24 hour default). Unknown keys are ignored. The snake_case spellings subject_ref, return_url, id_type, expires_in_seconds and hosted_options are also accepted; the camelCase names are the contract. Locale, customer name, email and phone are not request fields: the page language follows the customer’s browser and cannot be forced, and the webhook endpoint is registered once for your organisation in the Backoffice, not per session.

Lifetime and expiry. expiresAt is Unix time in milliseconds in every response. At mint it is now + expiresInSeconds: 24 hours (86400 s) by default, or the 5 minutes (300 s) to 7 days (604800 s) you ask for; a value outside that range is refused with 400 invalid_request and is never clamped. An unopened link keeps that whole validity. The first customer action the platform records on the hosted page (for example accepting the privacy notice) opens the session: openedAt is stamped and expiresAt becomes min(expiresAt, openedAt + 30 minutes), so the customer must finish capture inside that window. The deadline only ever moves earlier: a link minted for 10 minutes still ends 10 minutes after mint, and a link minted for 24 hours ends 30 minutes after the customer first acts. It is never extended and there is no renewal call: when a customer needs more time, mint a new session. Each timed capture step (document front or back, selfie, document near face, liveness, voice) also has its own 10 minute limit; a step that runs out restarts and the session carries on until its own deadline. When the deadline passes the session is expired: the platform records it on the next read or on its expiry sweep (every 5 minutes), the hosted page answers 410 session_expired, GET /v1/kyc/sessions/{sessionId} keeps answering 200 with status: "expired", a session that was never submitted is never billed, and kyc.session.completed is never sent. A session in review_pending never expires: it waits for a Backoffice reviewer.

Using the link. The hosted page cannot be embedded in an iframe (X-Frame-Options: DENY); open it in a new tab, or redirect the customer to it. The page never posts a message to the opener or parent window and never redirects on its own. If you minted with a returnUrl, the page shows a plain “Return to <business>” link to exactly that URL (nothing is appended) on the completion and status screens only for a completed outcome, and on the screen shown when a step cannot continue. Sessions created from Backoffice invitations carry no returnUrl. Correlate by the sessionId you stored at mint, and never treat a visit to your return page as proof of completion.

Outcome. Learn the outcome with the kyc.session.completed webhook or by polling GET /v1/kyc/sessions/{sessionId}. With the identity provider configured today every successful capture ends in review_pending, not completed: the webhook fires only after a Backoffice reviewer approves or overrides the case (usually within seconds, at most about 5 minutes after the decision), and a review_pending session never expires. Read verification and review on the status read to tell an approval from an override. See that route for the status table.

Email verification (optional step). hostedOptions.steps.emailVerification (off, optional or required) adds an email-verification step to the hosted flow: the customer types an address, receives one confirmation link, and only a click on that link marks the address verified. Omit the key to use your organisation’s default (Hosted configuration); an explicit value for this link wins in both directions. The effective mode is resolved once, here, and is fixed for the session. This is available only once the platform has enabled hosted email verification: until then an explicit optional or required is refused with 400 email_verification_unavailable (an explicit off is always accepted, and your organisation default is ignored), so a required session can never silently run unverified.

Domains (optional). One organisation can own several domains, and each domain can carry its own hosted configuration (business name, purpose, privacy and support pages, supported documents, approved return hosts, embed origins, email verification) that overrides your organisation default field by field; see GET /v1/kyc/config. The invitation is created for one domain, chosen in this order: domainId in the body, then siteKey in the body, then the request’s Origin header when it exactly equals the origin of one of your active domains (any other origin is ignored). With none of these the invitation uses your organisation default. A domainId and a siteKey that name different domains are refused with 400 domain_mismatch; an unknown or disabled domain, or one that belongs to another organisation, is refused with 404 domain_not_found (the same answer in all three cases, so the route cannot be used to discover other organisations’ domains). The domain is fixed at mint: the returnUrl check and the email-verification mode use that domain’s configuration, and the hosted page always shows the configuration the domain has at that moment (if you remove a domain’s override later, its sessions fall back to your organisation default). The mint response and the status read carry the session’s domainId, null for the organisation default. Until a domain has its own configuration, every invitation behaves as before.

Countries (optional). hostedOptions.allowedIssuingCountries narrows one invitation to some issuing countries, for example the countries you expect this customer to come from, without changing your hosted configuration. Send any ISO 3166-1 alpha-2 codes (CountryCodeList: 1 to 249 distinct codes; all 249 is the same as sending none). Each document type your configuration accepts keeps only the issuing countries that are also in the list, and a document type left with no country is dropped from the invitation. When no document type is left the mint is refused with 400 invalid_hosted_options (field allowedIssuingCountries, reason no_matching_document_country) and no session is created. Omit the key to keep every configured country. A list outside your configuration is not an error as long as one document type remains. hostedOptions must be the complete object when you send it, so send this key together with the other required keys. The list is fixed at mint: the hosted page shows your configuration at that moment narrowed by it, and the customer cannot choose a country outside it.

Idempotency. Idempotency-Key is optional (8 to 128 characters). Without it every call mints a fresh session. With it, a retry under the same key returns the SAME session with replayed: true and WITHOUT the url: the one-shot URL of a lost response cannot be recovered, so mint a new session under a new key instead. The replay matches on the key only; a different body under the same key still returns the original session.

Rate limits. A per-source-IP limit applies first, before authentication, then a per-key limit shared with the status read. See “Rate limits” above.

Authentication: Bearer token (ApiKeyAuth), scope write:identity.

Parameters

NameInTypeRequiredDescription
Idempotency-KeyheaderstringNoWhen 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.

FieldTypeRequiredDescription
subjectRefstringYesYour 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.
returnUrlstring (uri)NoWhere the “Return to <business>” link on the hosted page points. https only, no user info, at most 2048 characters, and the host must exactly equal (case insensitive) an entry in your organisation’s approved return hosts (allowedReturnUrlHosts in the Backoffice hosted configuration; at most 20 entries, host names only, no wildcards; port, path and query are not checked). An organisation with no approved hosts cannot send a returnUrl. Anything else is refused with 400 return_url_not_allowed. The page appends nothing to it. For an invitation created for a domain, the list is that domain’s own when it sets one, otherwise your organisation’s. At most 2048 characters. Pattern ^https://.
purposestringNoShown to the customer on the consent screen. Falls back to the default purpose of the invitation’s domain, then to your organisation’s default purpose. More than 500 characters is refused with 400 invalid_request. At most 500 characters.
idTypestringNoDocument 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.
expiresInSecondsintegerNoLifetime 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.
hostedOptionsobject (KycHostedSessionOptions)NoInvitation-scoped hosted-flow switches, frozen into the session at mint. When you send hostedOptions it must be the complete object (every required key below, no merging with defaults); omit it to use the defaults (all four document types, allowDeviceUpload false, documentBack auto, documentNearFace false, activeLiveness true, speechAnalysis false, ageGenderEstimation false, emailVerification off). Inside steps, ageGenderEstimation and emailVerification are optional. allowedIssuingCountries is optional too.
hostedOptions.allowedDocumentTypesarray of stringYesAt least 1 item, at most 4 items, unique items.
hostedOptions.allowedIssuingCountriesarray of stringNoOptional. Narrows the invitation to these issuing countries: each document type your hosted configuration accepts keeps only the issuing countries that are also listed, a document type left with none is dropped, and a mint where no document type remains is refused with 400 invalid_hosted_options (field allowedIssuingCountries, reason no_matching_document_country). Absent means no narrowing. Any ISO 3166-1 alpha-2 code is accepted, including codes your configuration does not offer. At least 1 item, at most 249 items, unique items.
hostedOptions.allowDeviceUploadbooleanYes
hostedOptions.stepsobjectYes
hostedOptions.steps.documentBackstringYesAllowed values: auto.
hostedOptions.steps.documentNearFacebooleanYes
hostedOptions.steps.activeLivenessbooleanYes
hostedOptions.steps.speechAnalysisbooleanYes
hostedOptions.steps.ageGenderEstimationbooleanNoOptional; 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.emailVerificationstringNoOptional (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.
domainIdstringNoCreate the invitation for this domain of your organisation (the d_... id shown on the Backoffice Domains page), so that it uses that domain’s hosted configuration. Optional. It is chosen before siteKey and the Origin header (see “Domains” above). An unknown, disabled or foreign domain is refused with 404 domain_not_found. At least 1 character, at most 64 characters.
siteKeystringNoThe site key (pk_...) of one of your active domains, as an alternative to domainId. When both are sent they must name the same domain, otherwise the call is refused with 400 domain_mismatch. Optional. An unknown or disabled site key is refused with 404 domain_not_found. At least 1 character.

Responses

StatusMeaningError codesContent
200Session minted, or (idempotent replay) the existing session returned without its URL.application/json
400invalid_request (unreadable JSON, Idempotency-Key outside 8 to 128 characters, subjectRef missing or longer than 256, expiresInSeconds outside 300 to 604800 or not an integer, purpose longer than 500, a bad payload), invalid_id_type, invalid_hosted_options (hostedOptions is not a complete, well-formed object, steps.emailVerification is not one of off, optional, required, or allowedIssuingCountries is not a list of 1 to 249 distinct ISO 3166-1 alpha-2 codes, with field allowedIssuingCountries; also when none of your configured document types is offered in any listed country, with reason no_matching_document_country), return_url_not_allowed, email_verification_unavailable (an explicit steps.emailVerification of optional or required was requested but hosted email verification is not enabled for the platform), or domain_mismatch (domainId and siteKey name different domains).domain_mismatch, email_verification_unavailable, invalid_hosted_options, invalid_id_type, invalid_request, return_url_not_allowedapplication/json KycHostedOptionsError
401The 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_keyapplication/json KycSessionError
403The key lacks the write:identity scope; or kyc_legal_basis_receipt_required (no active hosted-capture legal-basis receipt for your organisation); or evidence_not_provisioned (your organisation’s raw-evidence storage is not armed, reason raw_storage_not_armed, or is armed without an evidence residency of TR, EU or US, reason residency_unconfigured; no session was created); or age_gender_estimation_not_enabled (hostedOptions.steps.ageGenderEstimation was true but the platform feature or your organisation’s acknowledgement is missing; see reasonCode).age_gender_estimation_not_enabled, evidence_not_provisioned, kyc_legal_basis_receipt_requiredapplication/json KycSessionError
404Either the byte-identical “V1 endpoint not found” response returned when the route’s module is not enabled for your organisation (see GatedNotFound), or domain_not_found: the domainId or siteKey is unknown, names a disabled domain, or belongs to another organisation. The three causes of domain_not_found get the same answer by design.domain_not_foundapplication/json KycGatedOrDomainNotFoundError
413body_too_large — the request body exceeds 32768 bytes.body_too_largeapplication/json KycSessionError
415The request is not Content-Type: application/json (body invalid_request).invalid_requestapplication/json KycSessionError
429Rate limit exceeded. Wait Retry-After seconds (also retryAfter in the body) before retrying; the response carries no other rate-limit headers.Rate limit exceededapplication/json
500API-key authentication is not configured on this deployment (server_misconfigured). Retrying does not help; contact support with the requestId.server_misconfiguredapplication/json KycSessionError
503Retryable 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_requestapplication/json KycSessionError
503Noxtica could not read the key’s record. This is a temporary fault on Noxtica’s side and says nothing about your key. Retry after the Retry-After delay; do not treat it as a revoked key.api_key_auth_unavailableapplication/json

Response headers

StatusHeaderTypeDescription
429Retry-AfterintegerSeconds until the bucket allows another request.
503Retry-AfterintegerSeconds to wait before retrying.

Response fields (200, application/json)

FieldTypeAlways returnedDescription
okbooleanYesAllowed values: true.
sessionIdstring (uuid)YesThe session id. Store it with your customer record; it is the correlation handle for status reads and webhooks.
urlstring (uri)NoOne-shot hosted-flow URL; the ONLY place the bearer token is ever returned. Absent on an idempotent replay.
expiresAtinteger (int64)YesUnix time in milliseconds at which the unopened link expires.
replayedbooleanNoPresent (and true) only when an Idempotency-Key matched an existing session; url is then absent. Allowed values: true.
hostedOptionsobjectNoThe 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.allowedDocumentTypesarray of stringYesAt least 1 item, at most 4 items, unique items.
hostedOptions.allowedIssuingCountriesarray of stringNoOptional. Narrows the invitation to these issuing countries: each document type your hosted configuration accepts keeps only the issuing countries that are also listed, a document type left with none is dropped, and a mint where no document type remains is refused with 400 invalid_hosted_options (field allowedIssuingCountries, reason no_matching_document_country). Absent means no narrowing. Any ISO 3166-1 alpha-2 code is accepted, including codes your configuration does not offer. At least 1 item, at most 249 items, unique items.
hostedOptions.allowDeviceUploadbooleanYes
hostedOptions.stepsobjectYes
hostedOptions.steps.documentBackstringYesAllowed values: auto.
hostedOptions.steps.documentNearFacebooleanYes
hostedOptions.steps.activeLivenessbooleanYes
hostedOptions.steps.speechAnalysisbooleanYes
hostedOptions.steps.ageGenderEstimationbooleanNoOptional; 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.emailVerificationstringNoOptional (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.
domainIdstring, nullableYesThe domain the invitation was created for, or null when it uses your organisation default. Fixed at mint; the status read returns it too.
requestIdstringYes

Response example (200)

{
  "ok": true,
  "sessionId": "6d3c3a8e-6b4b-4a8f-8f4e-0a0e6b3d9d21",
  "url": "https://verify.noxtica.com/s/kycs.tenant_42.q3JkT0vN8mYb1xW6cE4aZ7uP2sH9dLfR5gKiA0oXyUw",
  "expiresAt": 1790765600000,
  "domainId": null,
  "requestId": "req-test"
}

Example request

curl

curl -X POST "https://collect.noxtica.com/v1/kyc/sessions" \
  -H "Authorization: Bearer $NOXTICA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "subjectRef": "cust_4821",
  "returnUrl": "https://merchant.example.com/kyc/return",
  "purpose": "Open your brokerage account",
  "idType": "passport",
  "expiresInSeconds": 3600,
  "hostedOptions": {
    "allowedDocumentTypes": [
      "passport",
      "national_id"
    ],
    "allowedIssuingCountries": [
      "TR",
      "GB",
      "DE"
    ],
    "allowDeviceUpload": false,
    "steps": {
      "documentBack": "auto",
      "documentNearFace": false,
      "activeLiveness": true,
      "speechAnalysis": false,
      "ageGenderEstimation": false
    }
  }
}'

Node.js (global fetch, Node 18 or later)

const response = await fetch('https://collect.noxtica.com/v1/kyc/sessions', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.NOXTICA_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    subjectRef: 'cust_4821',
    returnUrl: 'https://merchant.example.com/kyc/return',
    purpose: 'Open your brokerage account',
    idType: 'passport',
    expiresInSeconds: 3600,
    hostedOptions: {
      allowedDocumentTypes: [
        'passport',
        'national_id',
      ],
      allowedIssuingCountries: [
        'TR',
        'GB',
        'DE',
      ],
      allowDeviceUpload: false,
      steps: {
        documentBack: 'auto',
        documentNearFace: false,
        activeLiveness: true,
        speechAnalysis: false,
        ageGenderEstimation: false,
      },
    },
  }),
});
console.log(response.status, await response.text());

Python (standard library)

import json
import os
import urllib.request

request = urllib.request.Request(
    "https://collect.noxtica.com/v1/kyc/sessions",
    data=json.dumps({
        "subjectRef": "cust_4821",
        "returnUrl": "https://merchant.example.com/kyc/return",
        "purpose": "Open your brokerage account",
        "idType": "passport",
        "expiresInSeconds": 3600,
        "hostedOptions": {
            "allowedDocumentTypes": [
                "passport",
                "national_id",
            ],
            "allowedIssuingCountries": [
                "TR",
                "GB",
                "DE",
            ],
            "allowDeviceUpload": False,
            "steps": {
                "documentBack": "auto",
                "documentNearFace": False,
                "activeLiveness": True,
                "speechAnalysis": False,
                "ageGenderEstimation": False,
            },
        },
    }).encode(),
    headers={
        "Authorization": f"Bearer {os.environ['NOXTICA_API_KEY']}",
        "Content-Type": "application/json",
    },
    method="POST",
)
with urllib.request.urlopen(request) as response:
    print(response.status, response.read().decode())

Read a hosted session’s current status (tenant backend read)

GET /v1/kyc/sessions/{sessionId}

Server-to-server. The authoritative tenant-side read of a hosted session; use it to confirm any webhook before you act on it, and to poll when you have no webhook endpoint. Everything is nested under session, and every timestamp is Unix time in milliseconds. A session that is past its deadline reads 200 with status: "expired" (never 410; the 410 answers belong to the customer’s browser routes). A session id that does not exist, or belongs to another organisation, gets the same byte-identical 404 as an unknown path.

Status lifecycle.

statusMeaningkyc.session.completed / kyc.session.cancelled
pendingMinted; the customer has not opened the link.none
openedThe customer reached the capture page.none
verifyingThe customer submitted; the verification is running.none
review_pendingThe verification finished and awaits a Backoffice review decision. With the identity provider configured today every successful hosted capture lands here. It never expires.none yet; kyc.session.completed fires later, when a reviewer decides
completedA business decision exists: a reviewer approved or overrode the case (or, by design, a machine verified). completed does not mean verified: read verification and review below.kyc.session.completed with status: completed
unverifiedFinished with a negative outcome and no manual-review code (for example capture attempts exhausted).kyc.session.completed with status: unverified
errorA provider or infrastructure fault, or the submit was refused at admission.kyc.session.completed with status: error
expiredPast the deadline without a decision (see “Lifetime and expiry” on POST /v1/kyc/sessions).never
cancelled_by_userThe customer cancelled on the hosted page (only when cancellation is enabled for your organisation).kyc.session.cancelled

review_pending to completed. kyc.session.completed does not fire when a session lands in review_pending. It fires when a Backoffice reviewer approves or overrides the case, usually within seconds and at most about 5 minutes after the decision. Until then the only way to observe review_pending is this read. An override also moves the session to completed, so completed is not an approval. decision.outcome is derived from status alone (completed reads approved), so it reads approved after an override too: do not use it to tell the two apart. Use verification and review.

Verification outcome and review. Once the session has a verificationId (the customer submitted), session carries two more blocks that state what the verification finally is; before that neither key is present.

  • verification.status is the outcome: verified, unverified, failed, error or pending (the verification has no verdict yet). verification.source says who produced it: pipeline is the automated verdict exactly as recorded, review means a reviewer’s approve decision was applied over it. Only approve ever turns the reported status into verified; an override is reported in review but never upgrades the status.
  • review is null while no reviewer has acted, otherwise {state, decision, version, decidedAt}. state is in_review (escalated, no decision yet), approved or overridden; the vocabulary also holds open and closed, which no review action writes today. decision is approve, override, or null while the state is in_review. version is the number of review writes, starting at 1 and increasing by one with every write. decidedAt is Unix ms of the last decision, or null while there is none. No reviewer identity and no reviewer note is ever returned.

reasonCodes keeps the automated reasons after a review: provider_manual_review stays in the list once a reviewer has approved the case, so it does not mean the case is still waiting. Read status, verification and review instead.

Situationstatusverificationreview
Waiting for a reviewerreview_pendingunverified / pipelinenull
Escalated, undecidedreview_pendingunverified / pipelinestate: in_review, decision: null
Approvedcompletedverified / reviewstate: approved, decision: approve
Overriddencompletedthe automated status / pipelinestate: overridden, decision: override

There is no reject decision in the review vocabulary today, and a rejected end state cannot be expressed. kyc.verification.completed is sent once, when the automated verification ends (for a hosted session, unverified with provider_manual_review); it is never sent again after a review, so do not wait for it: read review.

Review decision and status events. A reviewer’s approve or override is announced by the kyc.review.decided event, for hosted sessions and for verifications you created through the API; for a hosted session that was review_pending, kyc.session.completed follows. An escalation (in_review) sends nothing. Each status a session enters is also announced by its own opt-in event: kyc.session.created (pending), kyc.session.opened, kyc.session.verifying, kyc.session.review_pending, kyc.session.unverified, kyc.session.error and kyc.session.expired; see the webhooks section.

Cancellation. status: cancelled_by_user is reported here, but cancelledAt and cancelReason are NOT part of this response; they are delivered in the kyc.session.cancelled webhook. An expiry never sends kyc.session.completed or kyc.session.cancelled. A cancelled or expired session is never billed as a verification (the kyc.verification meter is recorded only at submit-time admission); only the request-level endpoint.collect.request counts.

Email verification result. Only when hosted email verification is enabled for the platform AND the session’s effective mode is not off or an address is stored, the session object also carries email (the normalised address the customer entered, or null), emailVerified (true only after the customer clicked the confirmation link for that exact address), emailVerifiedAt (Unix ms, or null) and emailVerification (the effective mode: off, optional or required). Treat email with emailVerified: false as an unverified, customer-typed value. Otherwise these four keys are absent.

Noxtica Verification ID. Only when the Noxtica Verification ID (NVID) is enabled for the platform, the session object also carries nvid: the session’s NVID (NVID-XXXX-XXXX-XXXX-XXXX) or null (none was issued: an NVID is issued when the submit ends completed or review_pending). The key is absent while the feature is not enabled. The public status page of an NVID is https://verify.<environment host>/v/<NVID>; the public lookup route behind it can only be called from the hosted site’s browser origin, never from a tenant backend.

Check results. The per-check results below are evidence for a reviewer and for your own risk rules. Every number is a value the service uses today and may be retuned without a version bump unless a row says otherwise; model, engine and vendor names are deliberately not published.

  • Nothing here rejects a person by itself. Every hosted verification currently ends verification.status: unverified with reasonCodes: [provider_manual_review]; a reviewer’s approval completes it (verification.source: review). A check result never changes verification.status on its own.
  • fail means a confident failure; review means no confident answer, a person should look. An advisory or unqualified signal can only read review. A fail on a person who is clearly genuine is a false reject: report it with the sessionId.
  • While presentation-attack detection (PAD) is advisory, liveness and selfie never read pass. Their best outcome is review, so scorecard.overall is at best review for every session today. This is expected and is not a problem with the person’s capture.

What these readings mean for your integration, what to automate and what not to, and every PAD and liveness reason code are on the page Liveness & PAD policy.

Thresholds are platform-wide. Every threshold named below (the selfie quality limits, the face-match bands, the liveness and blink limits, the document-authenticity bands and the PAD operating points) is one value for all customers. No request carries a tenant, profile or threshold parameter, and neither your organisation default, a domain override nor an invitation changes a threshold: there is no tenant-level or domain-level threshold setting today. What you can configure, per organisation, per domain (a domain overrides your organisation default field by field) and per invitation, is which steps and documents run and which identity fields must be read: in the hosted verification configuration (GET /v1/kyc/config) documents[] (idType, issuingCountries, requiredIdentityFields), requireSelfie, requireVoice, allowDeviceUpload and emailVerification; on an invitation (POST /v1/kyc/sessions) hostedOptions (allowedDocumentTypes, allowedIssuingCountries and steps, for example steps.activeLiveness). None of these changes how a check is judged. The verdict of a hosted verification (unverified with provider_manual_review until a reviewer approves) is not configurable either.

Where the results appear: session.scorecard.<step> for document_front, document_back, selfie, liveness, voice, cross_validation and overall ({status, score 0..1, checks[{name, pass, score}], reasonCodes[]}), and session.stages.<stage> ({status, score, reasonCodes[]}). The identity read (GET /v1/kyc/verifications/{verificationId}/identity, scope read:identity_details) returns the verdicts only, as checks.documentAuthenticity, checks.faceMatch, checks.liveness and checks.selfie, without scores.

checks.* on the identity readBuilt fromScorecard step
documentAuthenticitythe document authenticity analysisdocument_front
faceMatchdocument portrait against selfie (and the liveness frames, the worse of the two)selfie
livenessthe active-liveness challenge (five gestures)liveness
selfieselfie quality and PAD, the worse of the twoselfie

Reading a scorecard reason code.

PatternMeaning
<check>_failedthat check (a name from the tables below) returned a confident failure.
<check>_inconclusivethe analysis could not judge that check (for example a blink that could not be measured). Never a failure.
<capability>_pass, _fail, _inconclusivethe analysis’s own overall verdict, in public capability names: document_authenticity, face_match, active_liveness, presentation_attack_detection, face_quality, document_recognition, document_text_reading, mrz_reading, barcode_reading, document_consistency. For an advisory analysis _fail is that analysis’s verdict only; the step still reads review.
awaiting_capture, awaiting_evaluation, not_required, not_evaluatedthe step has no result yet, is not required, or the analysis never produced one (gated or unavailable): review.
not_applicablethe document type has nothing to measure for this stage (a driving licence has no machine-readable zone): the stage reads skipped.
additional_check_faileda check outside the public vocabulary failed; no detail is exposed.

Document authenticity (checks.documentAuthenticity, step document_front). Advisory: the analysis is an unqualified classical image analysis whose own decision is always manual review. A failed check reads review at most; only an overall pass reads pass. It is never used to reject.

CheckWhat it measuresFailing meansWeight
card_geometryThe document’s own outline has the shape of a card or booklet page.The outline’s long to short side ratio differs by more than 0.08 (relative to the reference ratio) from a bank-card (ID-1) card (1.586) or a passport page (1.389). A card smaller than 300 px on its short side, or not found, is inconclusive, never fail.hard gate
moire_recaptureScreen-photographed or re-photographed document patterns.A recapture-like pattern was found.hard gate
layout_conformanceThe expected elements and photo region of a card of this kind are present.Too few elements were found (blank, cropped or wrong-side image).scored
mrz_structureStructure of the machine-readable-zone band (back sides only).The band is malformed.scored
print_consistencyThe print noise pattern is uniform across the card.Locally inconsistent print texture (possible overlay or edit).scored
copy_moveDuplicated regions inside the image.Repeated image regions. Soft: never fails alone.scored
elaCompression-history consistency (JPEG sources).Locally different compression history.scored
hologram_presenceA light-variable (hologram-like) region is visible.Not visible at this capture angle. Soft: never fails alone.scored
image_similarity_replayThe image is not a known replayed or duplicate capture.Matches a known replay (only when a reference catalogue is configured).scored

The analysis is fail when a hard-gate check fails or the weighted score is below the fail band; pass only when every check passes, the score is in the pass band and card_geometry was actually measured; everything else is inconclusive. The numeric bands and the forensic thresholds of copy_move, ela, moire_recapture, print_consistency and hologram_presence are not published. The advisory portrait-substitution observation (doc_auth_portrait_substitution) is computed after the verdict and is not part of the score: it appears in checks[] when it did not pass, adds no _failed code and never lowers a pass. card_geometry_failed on a real card almost always means the document was not found or was held at a strong angle: ask for a flat, well-lit capture with the whole card in view. A document other than the supported card layout (for example a driving licence) is measured against the card profile, so its layout and hologram results are weaker evidence. session.scorecard.document_front is wider than checks.documentAuthenticity: it also folds in the document recognition and text reading rows of the front. The text reading row is always inconclusive today (document_text_reading_inconclusive), so document_front reads review at best, even when checks.documentAuthenticity reads pass. Treat review as “a person should look”.

Liveness (checks.liveness, step liveness, stage active_liveness). Advisory evidence plus a capture gate in the page: the hosted page does not accept a gesture until the person has moved by the capture thresholds below. The server then re-checks the captured frames independently, and the browser’s own measurements can only make a result worse, never rescue a failed server check. Five gestures are captured in the order turn_up, turn_down, turn_left, turn_right, blink. Checks in the scorecard: active_liveness (the overall result), turn_left, turn_right, turn_up, turn_down, blink and pad.

Checkpassreview (<name>_inconclusive)fail (<name>_failed)
turn_left, turn_right, turn_up, turn_downThe head moved clearly in the requested direction, measured against the person’s own starting pose.No starting pose or head-pose measurement could be formed, or the browser’s measurements were unavailable.The head did not move enough, moved the wrong way, or the peak frame the browser claimed is not where the server saw it. A frame without exactly one face fails the gesture.
blinkA complete open-closed-open eyelid cycle lasting 50 to 1500 ms was observed.The blink could not be measured, or its duration was outside 50 to 1500 ms.No credible eyelid closure.
padnot applicable (advisory)always (pad_unvalidated_model)never while advisory
active_livenessAll five gestures pass and pad passes. Not reachable while PAD is advisory.Any gesture is review, or PAD is advisory.Any gesture failed, or there was no usable face.

Publishable capture thresholds (browser, per gesture, relative to the start pose): turn left or right at least 6 degrees; look up or down at least 4.5 degrees; at most 42 degrees sideways or 38 degrees up and down is accepted; blink between 50 and 1500 ms. The server-side gate, its noise margins and the PAD operating points are not published.

Liveness reason codeMeaningStatus it causes
challenge_direction_inconsistentHead movement did not match the requested direction. It is present when the gesture failed, or when the gesture reads review and no more specific cause from this table applies; a review that names its cause (for example client_metrics_unavailable) does not carry it.gesture fail (or review)
landmark_peak_mismatchThe browser’s reported peak frame does not match the server’s measurement. For up and down it is judged only when the head-pose measurement was available; without it the gesture reads review with pose_landmarks_unavailable, not fail.gesture fail (turn left and right, and up and down with a head-pose measurement)
no_face_detected, multiple_facesA frame had no face or more than one face.gesture fail
face_track_lostThe face was lost between frames.review or fail
client_metrics_unavailable, client_landmark_low_confidenceThe browser did not provide its own measurements, or with low confidence. A blink is judged by the server on its own frames and does not carry them.caps at review
neutral_reference_unavailableNo usable starting pose (fewer than three single-face frames in the blink burst).review, never pass
pose_landmarks_unavailableThe head-pose measurement was unavailable for a frame; up and down cannot be judged.pitch gesture review
blink_inconsistentThe blink could not be confirmed. When a blink-like cycle existed it comes with one of the three codes below.blink review or fail
blink_closed_plateauA real eye closing and reopening was found, but the closed frame sits next to another almost identical near-closed frame, so a frozen or painted eyelid cannot be told apart from a very long closure.blink review
blink_scene_changedThe blink was found, but the nose region, which serves as a control for lighting and head movement, changed between the open frame and the reopened frame.blink review
blink_cycle_partialOnly part of the evidence for a blink was present (the eyelid shape or the eye-region change, not both), or the cycle was incomplete.blink review
blink_duration_out_of_rangeThe blink cycle lasted outside 50 to 1500 ms.blink review
pad_unvalidated_modelThe PAD result is advisory by design.pad review
pad_unavailable, pad_incomplete, pad_face_not_detectedPAD could not run on every frame.pad review
pad_review, pad_rejectedReserved for when PAD becomes decision-affecting; not emitted today.not applicable
passive_temporal_evidence_unavailable, passive_temporal_inconsistent, motion_implausible, frame_duplicate_suspectedPassive-hold variant only: not enough or implausible motion across the three frames, or repeated frames. passive_temporal_inconsistent is present when the result is fail, or when a review has no more specific cause.review (inconsistent: fail)
evidence_missing, evidence_unavailable, evidence_wrong_content_type, invalid_evidence_package, evidence_lookup_failed, transport_error, unexpected_status, malformed_response, malformed_refusal_body, adapter_not_configured, adapter_config_invalid, not_configured, weights_ungatedThe liveness check could not run at all (capture package missing or invalid, or the verification service unavailable). Appears as the stage reason of stages.active_liveness.review, never fail

turn_*_failed on a genuine person usually means the head barely moved, a very small face or poor light: ask the person to turn clearly past the on-screen mark. blink_inconclusive is common and means “not measured”, not “no blink”; together with blink_closed_plateau, blink_scene_changed or blink_cycle_partial it means a blink-like movement was seen but not confirmed, so a person should look at it. A server result stored for the session counts as proof that the challenge was captured: the liveness step is not pending once that result exists, even when the browser never reported a completed gesture.

Selfie quality (checks.selfie, step selfie, stage selfie_quality). The hard checks are flow-affecting (the hosted page asks for a retake); the soft checks are advisory. Every check after face_count needs exactly one detected face. A failed check is reported as <check>_failed, with the check name from the first column, in scorecard.selfie.reasonCodes and session.stages.selfie_quality.reasonCodes; the overall result of the quality analysis adds face_quality_fail or face_quality_inconclusive. The quality analysis also has internal issue names (for example eyes_not_open, image_too_blurry, exposure_out_of_range); they are not reason codes of the scorecard and are not returned in these lists.

CheckPasses whenReason code when it failsKind
face_countexactly one faceface_count_failedhard (fail)
face_area_ratiothe face box covers 0.03 to 0.90 of the image areaface_area_ratio_failedhard (fail)
face_centeredthe face centre is within 0.18 of the width and 0.20 of the height from the image centreface_centered_failedhard (fail)
pose_yaw, pose_pitchthe landmark pose ratio of each axis is within 0.25 of frontal (a unitless ratio of facial landmark positions, not an angle)pose_yaw_failed, pose_pitch_failedsoft (review)
eyes_openeye-region texture of at least 8 (luma standard deviation)eyes_open_failedhard (fail)
not_occludednose and mouth-corner texture of at least 5not_occluded_failedhard (fail)
sharpnessLaplacian variance of the face box of at least 40sharpness_failedsoft (review)
exposuremean face luma between 35 and 220 (of 255) and at most 0.15 of the pixels clippedexposure_failedsoft (review)

The selfie analysis is fail (face_quality_fail) when any hard check fails, inconclusive (face_quality_inconclusive, reads review) when only soft checks fail or when every check passed but their mean score is below 0.6, otherwise pass. In the last case no <check>_failed code is present. The check scores (0..1) are in scorecard.selfie.checks. The thresholds are calibrated for typical webcam selfies around 640x480; sharpness is a pixel-scale measure and reads lower on higher-resolution captures of the same sharp face (a soft check: it can only produce review). exposure covers both too dark and too bright faces. A head that is clearly turned away can also make the hosted page ask the person to retake the selfie, within the retake budget of the session. checks.selfie also folds in PAD, so while PAD is advisory it is at best review even when every check above passes; it reads fail only when a hard selfie check fails.

Presentation-attack detection (PAD). Advisory, never decision-affecting. No operating point has been qualified on real replay and print cohorts, so every PAD result is reported as review with the reason pad_unvalidated_model and the scorecard code presentation_attack_detection_inconclusive. The live-score is recorded for reviewers but is not interpreted as live or spoof; its thresholds and model details are not published. Promoting PAD to a decision requires a new qualification and a documented contract change; the policy page linked above states what changes for your integration and what does not. When the PAD analysis does not find a face in the selfie although the face comparison did, the verification does not end in an error: document, machine-readable-zone and barcode reading continue, the selfie and liveness steps read review with the same two codes, and the session ends like every other hosted verification (unverified with provider_manual_review).

Face match (checks.faceMatch). Reviewer evidence: a low score can force manual review, it never rejects.

CheckMeaning
similarity_accept_bandPrimary comparison of the document portrait with the selfie: pass at a similarity of 0.42 or higher, review from 0.30 up to 0.42, fail below 0.30.
face_match_calibrated_scoreThe fused, calibrated score (0..1), advisory.
cross_check_1, cross_check_2, …Independent second-opinion comparisons (anonymous ordinal ids). Each one agrees only when its own calibrated score reaches its own accept bound; one that does not agree reads cross_check_N_failed and makes checks.faceMatch and the selfie step review even when similarity_accept_band passes. A second opinion never makes either of them fail: a fail comes only from the primary comparison.
face_match_borderlineThe comparison sat near a band edge.

A missing result reads null on the identity read and not_evaluated on the scorecard: no face was found in one of the images, or the analysis was unavailable. Different lighting, angle or age lowers the score: ask for a frontal, well-lit selfie.

Machine-readable zone and cross-check (stage mrz_crosscheck, steps document_back and cross_validation). A driving licence has no machine-readable zone. For a session whose document type is driving_license, session.stages.mrz_crosscheck reads {status: skipped, score: null, reasonCodes: [not_applicable]} and the machine-readable-zone rows are left out of document_back and cross_validation, so the missing zone is not a failure; a zone that was read from that document anyway is shown as usual. For every other document type, a zone that was not found, or that cannot be compared with the printed text, reads review (a measurement gap) and never fail. fail is kept for a decoded zone whose check digits are wrong or whose content contradicts the printed text. A barcode that was not found or could not be decoded on the back is a measurement gap as well: document_back reads review with barcode_reading_inconclusive, never fail; fail is kept for a barcode that was decoded and is invalid. On a zone that was read, the structure checks that only report which fields the band carries (for example mrz_personal_number_present, a field that a passport or a foreign identity card does not have) are informational: they stay in checks[] and never turn document_back into fail unless the zone itself failed. A driving_license whose cross_validation compared nothing reads skipped with not_applicable there instead of review. An auxiliary check of document_front that compares a secondary document classifier with the primary document type reads review at most and never turns the step into fail; the two label sets are compared by document type and side, so a document named as the same type and side by both agrees.

How steps roll up (scorecard.overall). fail dominates review, which dominates pass. skipped steps are ignored; a pending step turns an otherwise pass roll-up into pending but never hides a fail or a review.

The document recognition and reading codes that can appear next to these (document_type_uncertain, document_type_not_identified, document_front_no_portrait, document_back_has_portrait, mrz_not_detected, mrz_invalid, mrz_checksum_invalid, front_field_missing, front_back_field_mismatch) concern reading the document. Two informational codes concern the selection the person made: document_type_mismatch_selection (the classifier contradicts the selected document type) and issuing_country_mismatch_selection (the document shows another country than the selected one); see the extraction block of GET /v1/kyc/verifications/{verificationId}/identity.

Rate limit. Shares the per-key limit (30 requests per 60 seconds) with the mint call, after the per-source-IP limit that applies to every request.

Authentication: Bearer token (ApiKeyAuth), scope write:identity.

Parameters

NameInTypeRequiredDescription
sessionIdpathstringYesThe sessionId returned by the mint call. Pattern ^[A-Za-z0-9-]{1,64}$.

Responses

StatusMeaningError codesContent
200The session’s current status.application/json KycTenantSessionEnvelope
400Invalid sessionId — the path segment is not valid percent-encoding.Invalid sessionIdapplication/json KycSessionError
401The 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_keyapplication/json KycSessionError
403The presented key lacks the required scope.API key does not have write:identity scopeapplication/json
404The 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 foundapplication/json
429Rate limit exceeded. Wait Retry-After seconds (also retryAfter in the body) before retrying; the response carries no other rate-limit headers.Rate limit exceededapplication/json
500API-key authentication is not configured on this deployment (server_misconfigured). Retrying does not help; contact support with the requestId.server_misconfiguredapplication/json KycSessionError
503Retryable (Retry-After: 2). tenant_module_gate_unavailable, or invalid_request with a reason (pg_unavailable or backend_unavailable).invalid_requestapplication/json KycSessionError
503Noxtica could not read the key’s record. This is a temporary fault on Noxtica’s side and says nothing about your key. Retry after the Retry-After delay; do not treat it as a revoked key.api_key_auth_unavailableapplication/json

Response headers

StatusHeaderTypeDescription
429Retry-AfterintegerSeconds until the bucket allows another request.
503Retry-AfterintegerSeconds to wait before retrying.

Response fields (200, application/json)

FieldTypeAlways returnedDescription
okbooleanYesAllowed values: true.
sessionobject (KycTenantSessionStatus)YesThe 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.idstring (uuid)YesThe session id returned by the mint call.
session.domainIdstring, nullableYesThe domain the invitation was created for (see the domainId, siteKey and Origin rules of the mint call), or null when it uses your organisation default. Fixed at mint.
session.statusstringYesThe hosted session lifecycle status. pending — minted, link not opened yet. opened — the customer reached the capture page. verifying — the customer submitted and the verification is running. review_pending — the verification finished and awaits a Backoffice review decision (today every successful hosted capture lands here; it never expires, and kyc.session.completed has NOT fired yet). completed — a business decision exists (a reviewer’s approval or override: read verification and review on the status read for which). unverified — finished with a negative outcome and no manual-review code. error — a provider or infrastructure fault, or the submit was refused at admission. expired — the effective deadline passed without a decision (the status read reports it with 200; kyc.session.completed never fires for it). cancelled_by_user — the customer cancelled on the hosted page. Allowed values: pending, opened, verifying, review_pending, completed, unverified, error, expired, cancelled_by_user.
session.idTypestringYesThe document type hint given at mint (default national_id).
session.subjectIdstringYesYour own subjectRef from the mint call, returned unchanged. It is never sent in webhooks.
session.captureStepstringYesThe furthest capture step the customer has reached. Allowed values: consent, id_front, id_back, selfie, document_near_face, liveness, voice, review, submit.
session.createdAtinteger (int64)Yes
session.openedAtinteger (int64), nullableYesWhen the customer’s first recorded action on the hosted page happened (the session left pending); null until then.
session.expiresAtinteger (int64)YesThe effective deadline. At mint it is now + expiresInSeconds (24 hours by default, 5 minutes to 7 days); once the customer opens the session it is tightened to min(expiresAt, openedAt + 1800000) (the 30-minute capture window). It only moves earlier and is never extended. A session in review_pending does not expire; a session past this instant that was never submitted reads status: expired.
session.completedAtinteger (int64), nullableYesWhen the decision was made (pipeline outcome, or the reviewer’s decision for a session that was review_pending); null before.
session.verificationIdstring, nullableYesThe verification the submit ran (also data.verificationId of kyc.verification.completed); null before the customer submits. For a hosted session it is hosted: followed by 64 lowercase hexadecimal characters (71 characters in all); see KycVerificationId for the formats and the 128-character limit. At most 128 characters.
session.reasonCodesarray of stringYesClosed reason-code catalog. The reasons of the automated verification; a review does not rewrite them, so provider_manual_review stays after a reviewer approved the case. Read verification and review for the current outcome.
session.purposestring, nullableYesThe mint-time purpose.
session.returnUrlstring, nullableYesThe mint-time returnUrl.
session.selectedIdTypestring, nullableYesThe document type the customer chose on the page: passport, national_id, driving_license or residence_permit; null until the customer chooses.
session.selectedIssuingCountrystring, nullableYesThe issuing country the customer chose, as an ISO 3166-1 alpha-2 code (one of the CountryCode values); null until the customer chooses. Pattern ^[A-Z]{2}$.
session.consentVersionstring, nullableYesVersion of the privacy notice the customer acknowledged.
session.consentAcceptedAtinteger (int64), nullableYes
session.speechProfilingConsentAcceptedAtinteger (int64), nullableYes
session.ageGenderConsentAcceptedAtinteger (int64), nullableYes
session.evidenceobject (KycSessionEvidenceRefs)YesOpaque 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.frontstringNo
session.evidence.backstringNo
session.evidence.selfiestringNo
session.evidence.voicestringNo
session.evidence.documentNearFacestringNo
session.decisionobject (KycSessionDecision)NoThe decision block, present once the pipeline or a review decision has produced one. outcome maps from the session status alone: completed to approved, unverified to rejected, review_pending to review_pending, error to error. It is not the review decision: a session a reviewer overrode is completed too and reads approved here. Use verification and review to tell an approval from an override.
session.decision.outcomestringYesAllowed values: approved, rejected, review_pending, error.
session.decision.reasonCodesarray of stringYes
session.decision.decidedAtinteger (int64)YesUnix ms.
session.verificationobjectNoThe reported outcome of the verification. Present only once the session has a verificationId; absent before that.
session.verification.statusstringYesThe outcome of a verification. verified, unverified, failed and error are the verdict words of the automated verification; pending means the verification has no verdict yet. A reviewer’s approve decision turns the reported status into verified (see KycVerificationStatusSource); no other decision changes it. Allowed values: verified, unverified, failed, error, pending.
session.verification.sourcestringYesWho produced the reported status. pipeline — the automated verdict exactly as it was recorded. review — a reviewer’s approve decision was applied over it. An override decision never produces review. Allowed values: pipeline, review.
session.reviewone of KycTenantReview or any, nullableNoThe reviewer’s work on the verification: null while no reviewer has acted. Present (as null or an object) exactly when verification is; absent before the session has a verificationId.
session.review{KycTenantReview}object (KycTenantReview)NoWhat a reviewer did with a verification. No reviewer identity, no reviewer note and no reviewer reason codes are returned.
session.review{KycTenantReview}.statestringYesState of a reviewer’s work on a verification. in_review — a reviewer escalated the case without deciding (no decision yet). approved — a reviewer approved it. overridden — a reviewer replaced the automated verdict. open and closed belong to the vocabulary but no Backoffice review action writes them today; handle an unknown or unexpected value as “not decided”. Allowed values: open, in_review, approved, overridden, closed.
session.review{KycTenantReview}.decisionone of KycReviewDecision or any, nullableYesThe reviewer’s decision, or null while the state is in_review.
session.review{KycTenantReview}.versionintegerYesNumber of review writes for this verification, starting at 1 and increasing by one with every write (an escalation counts). Minimum 1.
session.review{KycTenantReview}.decidedAtinteger (int64), nullableYesUnix ms of the last review write that carried a decision, or null while there is none.
session.crossValidationobjectNoThe OCR / MRZ / barcode cross-validation advisory block, when the pipeline attached one. Absent for sessions without back-of-document evidence. Verdict-neutral.
session.stagesobject (KycTenantStages)YesPer-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_inventoryobject (KycTenantStageResult)NoOne 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.statusstringYesAllowed values: pass, review, fail, skipped, pending.
session.stages.scene_inventory.scorenumber, nullableYesRange 0 to 1, or null when the stage produced no score.
session.stages.scene_inventory.reasonCodesarray of stringYes
session.stages.document_type_idobject (KycTenantStageResult)NoOne 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.statusstringYesAllowed values: pass, review, fail, skipped, pending.
session.stages.document_type_id.scorenumber, nullableYesRange 0 to 1, or null when the stage produced no score.
session.stages.document_type_id.reasonCodesarray of stringYes
session.stages.ocr_backobject (KycTenantStageResult)NoOne 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.statusstringYesAllowed values: pass, review, fail, skipped, pending.
session.stages.ocr_back.scorenumber, nullableYesRange 0 to 1, or null when the stage produced no score.
session.stages.ocr_back.reasonCodesarray of stringYes
session.stages.mrz_crosscheckobject (KycTenantStageResult)NoOne 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.statusstringYesAllowed values: pass, review, fail, skipped, pending.
session.stages.mrz_crosscheck.scorenumber, nullableYesRange 0 to 1, or null when the stage produced no score.
session.stages.mrz_crosscheck.reasonCodesarray of stringYes
session.stages.selfie_qualityobject (KycTenantStageResult)NoOne 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.statusstringYesAllowed values: pass, review, fail, skipped, pending.
session.stages.selfie_quality.scorenumber, nullableYesRange 0 to 1, or null when the stage produced no score.
session.stages.selfie_quality.reasonCodesarray of stringYes
session.stages.document_near_faceobject (KycTenantStageResult)NoOne 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.statusstringYesAllowed values: pass, review, fail, skipped, pending.
session.stages.document_near_face.scorenumber, nullableYesRange 0 to 1, or null when the stage produced no score.
session.stages.document_near_face.reasonCodesarray of stringYes
session.stages.active_livenessobject (KycTenantStageResult)NoOne 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.statusstringYesAllowed values: pass, review, fail, skipped, pending.
session.stages.active_liveness.scorenumber, nullableYesRange 0 to 1, or null when the stage produced no score.
session.stages.active_liveness.reasonCodesarray of stringYes
session.stages.speech_profileobject (KycTenantStageResult)NoOne 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.statusstringYesAllowed values: pass, review, fail, skipped, pending.
session.stages.speech_profile.scorenumber, nullableYesRange 0 to 1, or null when the stage produced no score.
session.stages.speech_profile.reasonCodesarray of stringYes
session.stages.speech_analysisobject (KycTenantStageResult)NoOne 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.statusstringYesAllowed values: pass, review, fail, skipped, pending.
session.stages.speech_analysis.scorenumber, nullableYesRange 0 to 1, or null when the stage produced no score.
session.stages.speech_analysis.reasonCodesarray of stringYes
session.stages.age_genderobject (KycTenantStageResult)NoOne 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.statusstringYesAllowed values: pass, review, fail, skipped, pending.
session.stages.age_gender.scorenumber, nullableYesRange 0 to 1, or null when the stage produced no score.
session.stages.age_gender.reasonCodesarray of stringYes
session.scorecardobject (KycTenantScorecard)YesThe 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_frontobject (KycScorecardEntry)No
session.scorecard.document_front.statusstringYesAllowed values: pass, review, fail, skipped, pending.
session.scorecard.document_front.scorenumberYesRange 0 to 1; 0 for pending and skipped (no evaluation occurred).
session.scorecard.document_front.checksarray of objectYes
session.scorecard.document_front.checks[]objectNo
session.scorecard.document_front.checks[].namestringYes
session.scorecard.document_front.checks[].passbooleanYes
session.scorecard.document_front.checks[].scorenumber, nullableNo
session.scorecard.document_front.reasonCodesarray of stringYes
session.scorecard.document_backobject (KycScorecardEntry)No
session.scorecard.document_back.statusstringYesAllowed values: pass, review, fail, skipped, pending.
session.scorecard.document_back.scorenumberYesRange 0 to 1; 0 for pending and skipped (no evaluation occurred).
session.scorecard.document_back.checksarray of objectYes
session.scorecard.document_back.checks[]objectNo
session.scorecard.document_back.checks[].namestringYes
session.scorecard.document_back.checks[].passbooleanYes
session.scorecard.document_back.checks[].scorenumber, nullableNo
session.scorecard.document_back.reasonCodesarray of stringYes
session.scorecard.selfieobject (KycScorecardEntry)No
session.scorecard.selfie.statusstringYesAllowed values: pass, review, fail, skipped, pending.
session.scorecard.selfie.scorenumberYesRange 0 to 1; 0 for pending and skipped (no evaluation occurred).
session.scorecard.selfie.checksarray of objectYes
session.scorecard.selfie.checks[]objectNo
session.scorecard.selfie.checks[].namestringYes
session.scorecard.selfie.checks[].passbooleanYes
session.scorecard.selfie.checks[].scorenumber, nullableNo
session.scorecard.selfie.reasonCodesarray of stringYes
session.scorecard.livenessobject (KycScorecardEntry)No
session.scorecard.liveness.statusstringYesAllowed values: pass, review, fail, skipped, pending.
session.scorecard.liveness.scorenumberYesRange 0 to 1; 0 for pending and skipped (no evaluation occurred).
session.scorecard.liveness.checksarray of objectYes
session.scorecard.liveness.checks[]objectNo
session.scorecard.liveness.checks[].namestringYes
session.scorecard.liveness.checks[].passbooleanYes
session.scorecard.liveness.checks[].scorenumber, nullableNo
session.scorecard.liveness.reasonCodesarray of stringYes
session.scorecard.voiceobject (KycScorecardEntry)No
session.scorecard.voice.statusstringYesAllowed values: pass, review, fail, skipped, pending.
session.scorecard.voice.scorenumberYesRange 0 to 1; 0 for pending and skipped (no evaluation occurred).
session.scorecard.voice.checksarray of objectYes
session.scorecard.voice.checks[]objectNo
session.scorecard.voice.checks[].namestringYes
session.scorecard.voice.checks[].passbooleanYes
session.scorecard.voice.checks[].scorenumber, nullableNo
session.scorecard.voice.reasonCodesarray of stringYes
session.scorecard.cross_validationobject (KycScorecardEntry)No
session.scorecard.cross_validation.statusstringYesAllowed values: pass, review, fail, skipped, pending.
session.scorecard.cross_validation.scorenumberYesRange 0 to 1; 0 for pending and skipped (no evaluation occurred).
session.scorecard.cross_validation.checksarray of objectYes
session.scorecard.cross_validation.checks[]objectNo
session.scorecard.cross_validation.checks[].namestringYes
session.scorecard.cross_validation.checks[].passbooleanYes
session.scorecard.cross_validation.checks[].scorenumber, nullableNo
session.scorecard.cross_validation.reasonCodesarray of stringYes
session.scorecard.overallobject (KycScorecardEntry)No
session.scorecard.overall.statusstringYesAllowed values: pass, review, fail, skipped, pending.
session.scorecard.overall.scorenumberYesRange 0 to 1; 0 for pending and skipped (no evaluation occurred).
session.scorecard.overall.checksarray of objectYes
session.scorecard.overall.checks[]objectNo
session.scorecard.overall.checks[].namestringYes
session.scorecard.overall.checks[].passbooleanYes
session.scorecard.overall.checks[].scorenumber, nullableNo
session.scorecard.overall.reasonCodesarray of stringYes
session.emailstring, nullableNoPresent 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.emailVerifiedbooleanNoPresent under the same condition as email. true only after the customer clicked the confirmation link for that exact address.
session.emailVerifiedAtinteger (int64), nullableNoPresent under the same condition as email. When the confirmation link was clicked, or null.
session.emailVerificationstringNoHosted 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.nvidstring, nullableNoPresent 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.
requestIdstringYes

Response example (200)

{
  "ok": true,
  "session": {
    "id": "6d3c3a8e-6b4b-4a8f-8f4e-0a0e6b3d9d21",
    "domainId": null,
    "status": "review_pending",
    "idType": "passport",
    "subjectId": "cust_4821",
    "captureStep": "submit",
    "createdAt": 1790762000000,
    "openedAt": 1790762100000,
    "expiresAt": 1790763900000,
    "completedAt": 1790762445000,
    "verificationId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
    "reasonCodes": [
      "provider_manual_review"
    ],
    "purpose": "Open your brokerage account",
    "returnUrl": "https://merchant.example.com/kyc/return",
    "selectedIdType": "passport",
    "selectedIssuingCountry": "GB",
    "consentVersion": "2026-09-01",
    "consentAcceptedAt": 1790762110000,
    "speechProfilingConsentAcceptedAt": null,
    "ageGenderConsentAcceptedAt": null,
    "evidence": {
      "front": "obj_9c1e4a",
      "selfie": "obj_5b7d02"
    },
    "decision": {
      "outcome": "review_pending",
      "reasonCodes": [
        "provider_manual_review"
      ],
      "decidedAt": 1790762445000
    },
    "verification": {
      "status": "unverified",
      "source": "pipeline"
    },
    "review": null,
    "stages": {
      "scene_inventory": {
        "status": "pass",
        "score": 0.97,
        "reasonCodes": []
      },
      "document_type_id": {
        "status": "pass",
        "score": 0.94,
        "reasonCodes": []
      },
      "ocr_back": {
        "status": "skipped",
        "score": null,
        "reasonCodes": [
          "not_required"
        ]
      },
      "mrz_crosscheck": {
        "status": "pass",
        "score": 0.99,
        "reasonCodes": []
      },
      "selfie_quality": {
        "status": "pass",
        "score": 0.91,
        "reasonCodes": []
      },
      "document_near_face": {
        "status": "skipped",
        "score": null,
        "reasonCodes": [
          "not_required"
        ]
      },
      "active_liveness": {
        "status": "pass",
        "score": 0.93,
        "reasonCodes": []
      },
      "speech_profile": {
        "status": "skipped",
        "score": null,
        "reasonCodes": [
          "speech_profiling_not_requested"
        ]
      },
      "speech_analysis": {
        "status": "skipped",
        "score": null,
        "reasonCodes": [
          "speech_analysis_not_requested"
        ]
      },
      "age_gender": {
        "status": "skipped",
        "score": null,
        "reasonCodes": [
          "age_gender_not_requested"
        ]
      }
    },
    "scorecard": {
      "document_front": {
        "status": "pass",
        "score": 0.94,
        "checks": [],
        "reasonCodes": []
      },
      "document_back": {
        "status": "skipped",
        "score": 0,
        "checks": [],
        "reasonCodes": [
          "not_required"
        ]
      },
      "selfie": {
        "status": "review",
        "score": 0.91,
        "checks": [],
        "reasonCodes": [
          "presentation_attack_detection_inconclusive"
        ]
      },
      "liveness": {
        "status": "review",
        "score": 0.93,
        "checks": [],
        "reasonCodes": [
          "presentation_attack_detection_inconclusive"
        ]
      },
      "voice": {
        "status": "skipped",
        "score": 0,
        "checks": [],
        "reasonCodes": [
          "not_required"
        ]
      },
      "cross_validation": {
        "status": "pass",
        "score": 0.99,
        "checks": [],
        "reasonCodes": []
      },
      "overall": {
        "status": "review",
        "score": 0.94,
        "checks": [],
        "reasonCodes": [
          "presentation_attack_detection_inconclusive"
        ]
      }
    },
    "nvid": "NVID-7K3M-Q9XA-T2RW-4HZD"
  },
  "requestId": "req-test"
}

Example request

curl

curl "https://collect.noxtica.com/v1/kyc/sessions/example-sessionId" \
  -H "Authorization: Bearer $NOXTICA_API_KEY"

Node.js (global fetch, Node 18 or later)

const response = await fetch('https://collect.noxtica.com/v1/kyc/sessions/example-sessionId', {
  method: 'GET',
  headers: {
    Authorization: `Bearer ${process.env.NOXTICA_API_KEY}`,
  },
});
console.log(response.status, await response.text());

Python (standard library)

import os
import urllib.request

request = urllib.request.Request(
    "https://collect.noxtica.com/v1/kyc/sessions/example-sessionId",
    headers={
        "Authorization": f"Bearer {os.environ['NOXTICA_API_KEY']}",
    },
    method="GET",
)
with urllib.request.urlopen(request) as response:
    print(response.status, response.read().decode())

Read the hosted verification configuration, for your organisation or for one domain

GET /v1/kyc/config

Server-to-server, read-only. Returns the hosted verification configuration your invitations use: with no domainId, your organisation default; with a domainId, the configuration that applies to that domain.

Per-domain configuration. One organisation can own several domains, and each domain can override the hosted configuration (business name, purpose, privacy page, support page, supported documents, approved return hosts, embed origins, email verification, selfie, voice, device upload and liveness mode). Inheritance is field by field:

  • For every field the effective value is the domain’s own value when it sets one, otherwise your organisation default, otherwise the platform default. A field the domain leaves unset inherits; nothing is merged inside a field.
  • The lists (documents, allowedReturnUrlHosts, allowedEmbedOrigins) are replaced as a whole, never combined. An empty list is a real override (“no return hosts”, “cannot be embedded”) and is not the same as an unset list.
  • supportUrl: unset inherits. A domain can explicitly have no support link, and the effective value is then null.
  • The platform default applies only to a field neither the domain nor your organisation sets: requireSelfie true, requireVoice false, allowDeviceUpload false, livenessMode passive, allowedReturnUrlHosts [], allowedEmbedOrigins [], emailVerification off, supportUrl null.
  • businessName, purpose, privacyUrl, privacyVersion and documents have no platform default. When no layer sets one of them, effective is null and the hosted page shows “Verification isn’t ready yet”. A domain can therefore stand alone without an organisation default (for example when you operate several companies), or override only a few fields of the default.
  • A domain that sets its own privacyUrl should also set its own privacyVersion: the customer’s consent receipt records the version.

What a session uses. An invitation is created for one domain (see POST /v1/kyc/sessions; the mint response and the status read carry its domainId) and always shows that domain’s configuration as it is at that moment: if you remove a domain’s override later, its sessions fall back to your organisation default. The approved return hosts and the email-verification mode are checked once, at mint, against the chosen domain and are then fixed for the session. Until a domain has an override, every invitation behaves as it did with your organisation default alone.

Countries. documents[].issuingCountries accepts any ISO 3166-1 alpha-2 country (CountryCodeList: 1 to 249 distinct codes per document type), so a document type can be offered for every country. A single invitation can offer fewer countries without changing this configuration: see hostedOptions.allowedIssuingCountries on POST /v1/kyc/sessions. This read returns the configuration itself, not the narrowing of any invitation.

Required identity fields. An entry of documents can carry requiredIdentityFields: which of the five identity fields (surname, givenNames, dateOfBirth, personalNumber, documentNumber) must be read from that document type for the extraction to count as complete (see extraction on GET /v1/kyc/verifications/{verificationId}/identity). While the member is absent the default applies: surname, givenNames, dateOfBirth and documentNumber, plus personalNumber on a Turkish national ID card or driving licence, and on a Turkish document whose T.C. Kimlik No was read and passes its check digits documentNumber is not required. A non-empty list is exactly the set that is required and replaces that default, including the Turkish rule; an empty list requires nothing. A domain override replaces the whole documents list, like every other member of it. The member is edited in the Backoffice hosted verification settings (organisation default or one domain), not through this API, and this read returns it where it is set. It only decides which fields count towards extraction.status and the informational reason code identity_fields_unreadable: it never changes whether a session goes to review.

Response. With domainId: domainId and domain identify the domain, effective is the configuration its invitations use, sources names the layer each effective field comes from, and tenantDefault is your organisation default for comparison (null when you have none). Without domainId: domainId and domain are null, effective equals tenantDefault, and domains lists the active domains that carry an override, with the fields each overrides (a domain without an override is not listed: it uses your organisation default). An unknown domain, a disabled domain and a domain of another organisation all answer the same 404 domain_not_found.

Read only. The configuration is edited in the Backoffice hosted verification settings (a domain selector chooses the domain), not through the API: a change there needs a signed-in administrator’s fresh multi-factor confirmation, which an API key cannot give.

Before you can call. The identity verification module must be enabled for your organisation (otherwise the same byte-identical 404 as an unknown path) and your sk_ key needs the write:identity scope. See “Rate limits” above.

Authentication: Bearer token (ApiKeyAuth), scope write:identity.

Parameters

NameInTypeRequiredDescription
domainIdquerystringNoReturn the configuration that applies to this domain (the d_... id of a domain of your organisation, shown on the Backoffice Domains page; domains lists the ones that carry an override when this parameter is omitted). Omit it to read your organisation default and that list. An empty value is refused with 400 invalid_request. At least 1 character, at most 64 characters.

Responses

StatusMeaningError codesContent
200The configuration.application/json KycHostedConfigResponse
400invalid_request — domainId is present but empty.invalid_requestapplication/json KycSessionError
401The 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_keyapplication/json KycSessionError
403The presented key lacks the required scope.API key does not have write:identity scopeapplication/json
404Either the byte-identical “V1 endpoint not found” response returned when the route’s module is not enabled for your organisation (see GatedNotFound), or domain_not_found: the domainId or siteKey is unknown, names a disabled domain, or belongs to another organisation. The three causes of domain_not_found get the same answer by design.domain_not_foundapplication/json KycGatedOrDomainNotFoundError
429Rate limit exceeded. Wait Retry-After seconds (also retryAfter in the body) before retrying; the response carries no other rate-limit headers.Rate limit exceededapplication/json
500API-key authentication is not configured on this deployment (server_misconfigured). Retrying does not help; contact support with the requestId.server_misconfiguredapplication/json KycSessionError
503Retryable. tenant_module_gate_unavailable (the module gate could not be answered), or invalid_request with a reason (pg_unavailable or backend_unavailable).invalid_requestapplication/json KycSessionError
503Noxtica could not read the key’s record. This is a temporary fault on Noxtica’s side and says nothing about your key. Retry after the Retry-After delay; do not treat it as a revoked key.api_key_auth_unavailableapplication/json

Response headers

StatusHeaderTypeDescription
429Retry-AfterintegerSeconds until the bucket allows another request.
503Retry-AfterintegerSeconds to wait before retrying.

Response fields (200, application/json)

FieldTypeAlways returnedDescription
okbooleanYesAllowed values: true.
domainIdstring, nullableYesThe requested domain, or null when the call named none.
domainone of KycHostedConfigDomainRef or any, nullableYesThe requested domain, or null when the call named none.
domain{KycHostedConfigDomainRef}object (KycHostedConfigDomainRef)NoA domain of your organisation.
domain{KycHostedConfigDomainRef}.idstringYesThe domain id (d_...).
domain{KycHostedConfigDomainRef}.originstringYesThe domain’s origin, for example https://shop.example.com.
effectiveone of KycHostedConfig or any, nullableYesThe configuration invitations use. null when a required field (businessName, purpose, privacyUrl, privacyVersion, documents) is set by no layer; the hosted page then shows “Verification isn’t ready yet”.
effective{KycHostedConfig}object (KycHostedConfig)NoOne hosted verification configuration: your organisation default, or the effective configuration of one domain (see GET /v1/kyc/config for how a domain’s own values and your organisation default combine). Timestamps are Unix time in milliseconds.
effective{KycHostedConfig}.businessNamestringYesThe business name the hosted page shows to the customer.
effective{KycHostedConfig}.purposestringYesThe default purpose shown on the consent screen when the mint call sends none.
effective{KycHostedConfig}.privacyUrlstring (uri)YesThe privacy page the hosted page links to (https).
effective{KycHostedConfig}.supportUrlstring (uri), nullableYesThe support page the hosted page links to (https), or null when there is none.
effective{KycHostedConfig}.privacyVersionstringYesThe version label of the privacy notice at privacyUrl; the customer’s consent receipt records it.
effective{KycHostedConfig}.documentsarray of KycHostedConfigDocumentYesThe document types and issuing countries the customer can choose from.
effective{KycHostedConfig}.documents[]object (KycHostedConfigDocument)NoOne document type the hosted flow accepts, with the issuing countries it accepts it from.
effective{KycHostedConfig}.documents[].idTypestringYesAllowed values: national_id, passport, driving_license, residence_permit.
effective{KycHostedConfig}.documents[].issuingCountriesarray of stringYesIssuing countries the customer can pick for this document type, as ISO 3166-1 alpha-2 codes (CountryCode values; any of the 249 assigned codes). At most 249 items.
effective{KycHostedConfig}.documents[].requireBackbooleanYesWhether the back of the document is captured.
effective{KycHostedConfig}.documents[].requiredIdentityFieldsarray of stringNoThe identity fields that must be read from this document type for the extraction to count as complete (see extraction on GET /v1/kyc/verifications/{verificationId}/identity). Present only when set. Absent means the default: surname, given names, date of birth and document number, plus the personal number on a Turkish national ID card or driving licence, where a read T.C. Kimlik No that passes its check digits makes the document number optional. A non-empty list replaces that default exactly; an empty list requires none. Edited in the Backoffice hosted verification settings, not through this API. Unique items.
effective{KycHostedConfig}.requireSelfiebooleanYes
effective{KycHostedConfig}.requireVoicebooleanYes
effective{KycHostedConfig}.allowDeviceUploadbooleanYesWhether the customer may upload a file from their device instead of using the camera.
effective{KycHostedConfig}.livenessModestringYesAllowed values: passive, assisted_review, full_challenge.
effective{KycHostedConfig}.emailVerificationstringYesHosted email-verification mode. off — no email step. optional — the customer is offered the step and may skip it. required — the customer must verify an address before the session can be submitted (409 email_verification_required until then). The mode is resolved once at mint and fixed for the session. On the tenant-facing GET /v1/kyc/sessions/{sessionId} this key is the effective mode string; the hosted page’s own session routes carry the customer-facing state object KycEmailVerificationState under the same key. Allowed values: off, optional, required.
effective{KycHostedConfig}.allowedReturnUrlHostsarray of stringYesHost names a returnUrl may point to at mint.
effective{KycHostedConfig}.allowedEmbedOriginsarray of stringYesOrigins (scheme, host and optional port, for example https://shop.example.com) that may embed the hosted page in a frame. An empty list means it cannot be embedded.
effective{KycHostedConfig}.updatedAtinteger (int64)YesWhen the configuration last changed. For the effective configuration of a domain, the later of the domain’s change and your organisation’s change.
sourcesobject (KycHostedConfigSources)YesFor every overridable field, the layer its effective value comes from. All thirteen members are always present.
sources.businessNamestringYesThe layer an effective value comes from. domain — the domain’s own override. tenant — your organisation default. platform — the built-in default Noxtica applies to a field neither the domain nor your organisation sets. Allowed values: domain, tenant, platform.
sources.defaultPurposestringYesThe layer an effective value comes from. domain — the domain’s own override. tenant — your organisation default. platform — the built-in default Noxtica applies to a field neither the domain nor your organisation sets. Allowed values: domain, tenant, platform.
sources.privacyUrlstringYesThe layer an effective value comes from. domain — the domain’s own override. tenant — your organisation default. platform — the built-in default Noxtica applies to a field neither the domain nor your organisation sets. Allowed values: domain, tenant, platform.
sources.supportUrlstringYesThe layer an effective value comes from. domain — the domain’s own override. tenant — your organisation default. platform — the built-in default Noxtica applies to a field neither the domain nor your organisation sets. Allowed values: domain, tenant, platform.
sources.privacyVersionstringYesThe layer an effective value comes from. domain — the domain’s own override. tenant — your organisation default. platform — the built-in default Noxtica applies to a field neither the domain nor your organisation sets. Allowed values: domain, tenant, platform.
sources.documentsstringYesThe layer an effective value comes from. domain — the domain’s own override. tenant — your organisation default. platform — the built-in default Noxtica applies to a field neither the domain nor your organisation sets. Allowed values: domain, tenant, platform.
sources.requireSelfiestringYesThe layer an effective value comes from. domain — the domain’s own override. tenant — your organisation default. platform — the built-in default Noxtica applies to a field neither the domain nor your organisation sets. Allowed values: domain, tenant, platform.
sources.requireVoicestringYesThe layer an effective value comes from. domain — the domain’s own override. tenant — your organisation default. platform — the built-in default Noxtica applies to a field neither the domain nor your organisation sets. Allowed values: domain, tenant, platform.
sources.allowDeviceUploadstringYesThe layer an effective value comes from. domain — the domain’s own override. tenant — your organisation default. platform — the built-in default Noxtica applies to a field neither the domain nor your organisation sets. Allowed values: domain, tenant, platform.
sources.livenessModestringYesThe layer an effective value comes from. domain — the domain’s own override. tenant — your organisation default. platform — the built-in default Noxtica applies to a field neither the domain nor your organisation sets. Allowed values: domain, tenant, platform.
sources.allowedReturnUrlHostsstringYesThe layer an effective value comes from. domain — the domain’s own override. tenant — your organisation default. platform — the built-in default Noxtica applies to a field neither the domain nor your organisation sets. Allowed values: domain, tenant, platform.
sources.allowedEmbedOriginsstringYesThe layer an effective value comes from. domain — the domain’s own override. tenant — your organisation default. platform — the built-in default Noxtica applies to a field neither the domain nor your organisation sets. Allowed values: domain, tenant, platform.
sources.emailVerificationstringYesThe layer an effective value comes from. domain — the domain’s own override. tenant — your organisation default. platform — the built-in default Noxtica applies to a field neither the domain nor your organisation sets. Allowed values: domain, tenant, platform.
tenantDefaultone of KycHostedConfig or any, nullableYesYour organisation default, or null when you have not configured one.
tenantDefault{KycHostedConfig}object (KycHostedConfig)NoOne hosted verification configuration: your organisation default, or the effective configuration of one domain (see GET /v1/kyc/config for how a domain’s own values and your organisation default combine). Timestamps are Unix time in milliseconds.
tenantDefault{KycHostedConfig}.businessNamestringYesThe business name the hosted page shows to the customer.
tenantDefault{KycHostedConfig}.purposestringYesThe default purpose shown on the consent screen when the mint call sends none.
tenantDefault{KycHostedConfig}.privacyUrlstring (uri)YesThe privacy page the hosted page links to (https).
tenantDefault{KycHostedConfig}.supportUrlstring (uri), nullableYesThe support page the hosted page links to (https), or null when there is none.
tenantDefault{KycHostedConfig}.privacyVersionstringYesThe version label of the privacy notice at privacyUrl; the customer’s consent receipt records it.
tenantDefault{KycHostedConfig}.documentsarray of KycHostedConfigDocumentYesThe document types and issuing countries the customer can choose from.
tenantDefault{KycHostedConfig}.documents[]object (KycHostedConfigDocument)NoOne document type the hosted flow accepts, with the issuing countries it accepts it from.
tenantDefault{KycHostedConfig}.documents[].idTypestringYesAllowed values: national_id, passport, driving_license, residence_permit.
tenantDefault{KycHostedConfig}.documents[].issuingCountriesarray of stringYesIssuing countries the customer can pick for this document type, as ISO 3166-1 alpha-2 codes (CountryCode values; any of the 249 assigned codes). At most 249 items.
tenantDefault{KycHostedConfig}.documents[].requireBackbooleanYesWhether the back of the document is captured.
tenantDefault{KycHostedConfig}.documents[].requiredIdentityFieldsarray of stringNoThe identity fields that must be read from this document type for the extraction to count as complete (see extraction on GET /v1/kyc/verifications/{verificationId}/identity). Present only when set. Absent means the default: surname, given names, date of birth and document number, plus the personal number on a Turkish national ID card or driving licence, where a read T.C. Kimlik No that passes its check digits makes the document number optional. A non-empty list replaces that default exactly; an empty list requires none. Edited in the Backoffice hosted verification settings, not through this API. Unique items.
tenantDefault{KycHostedConfig}.requireSelfiebooleanYes
tenantDefault{KycHostedConfig}.requireVoicebooleanYes
tenantDefault{KycHostedConfig}.allowDeviceUploadbooleanYesWhether the customer may upload a file from their device instead of using the camera.
tenantDefault{KycHostedConfig}.livenessModestringYesAllowed values: passive, assisted_review, full_challenge.
tenantDefault{KycHostedConfig}.emailVerificationstringYesHosted email-verification mode. off — no email step. optional — the customer is offered the step and may skip it. required — the customer must verify an address before the session can be submitted (409 email_verification_required until then). The mode is resolved once at mint and fixed for the session. On the tenant-facing GET /v1/kyc/sessions/{sessionId} this key is the effective mode string; the hosted page’s own session routes carry the customer-facing state object KycEmailVerificationState under the same key. Allowed values: off, optional, required.
tenantDefault{KycHostedConfig}.allowedReturnUrlHostsarray of stringYesHost names a returnUrl may point to at mint.
tenantDefault{KycHostedConfig}.allowedEmbedOriginsarray of stringYesOrigins (scheme, host and optional port, for example https://shop.example.com) that may embed the hosted page in a frame. An empty list means it cannot be embedded.
tenantDefault{KycHostedConfig}.updatedAtinteger (int64)YesWhen the configuration last changed. For the effective configuration of a domain, the later of the domain’s change and your organisation’s change.
domainsarray of KycHostedConfigDomainSummaryNoPresent only when the call named no domainId. The active domains that carry an override.
domains[]object (KycHostedConfigDomainSummary)NoOne active domain that carries an override, with the hosted configuration fields it overrides.
domains[].domainIdstringYes
domains[].originstringYes
domains[].overriddenFieldsarray of stringYesThe fields this domain sets itself; every other field inherits from your organisation default. Never empty.
requestIdstringNo

Response example (200)

{
  "ok": true,
  "domainId": "d_7Kp3Zx9QmW2aLr5T",
  "domain": {
    "id": "d_7Kp3Zx9QmW2aLr5T",
    "origin": "https://markets.example.com"
  },
  "effective": {
    "businessName": "Acme Markets Inc.",
    "purpose": "Open your brokerage account",
    "privacyUrl": "https://markets.example.com/privacy",
    "supportUrl": "https://markets.example.com/help",
    "privacyVersion": "2026-10-15",
    "documents": [
      {
        "idType": "passport",
        "issuingCountries": [
          "GB",
          "DE"
        ],
        "requireBack": false
      },
      {
        "idType": "national_id",
        "issuingCountries": [
          "GB"
        ],
        "requireBack": true
      }
    ],
    "requireSelfie": true,
    "requireVoice": false,
    "allowDeviceUpload": false,
    "livenessMode": "passive",
    "emailVerification": "off",
    "allowedReturnUrlHosts": [
      "acme.example.com"
    ],
    "allowedEmbedOrigins": [],
    "updatedAt": 1790761000000
  },
  "sources": {
    "businessName": "domain",
    "defaultPurpose": "tenant",
    "privacyUrl": "domain",
    "supportUrl": "domain",
    "privacyVersion": "domain",
    "documents": "tenant",
    "requireSelfie": "tenant",
    "requireVoice": "tenant",
    "allowDeviceUpload": "tenant",
    "livenessMode": "tenant",
    "allowedReturnUrlHosts": "tenant",
    "allowedEmbedOrigins": "tenant",
    "emailVerification": "tenant"
  },
  "tenantDefault": {
    "businessName": "Acme Brokerage Ltd",
    "purpose": "Open your brokerage account",
    "privacyUrl": "https://acme.example.com/privacy",
    "supportUrl": "https://acme.example.com/support",
    "privacyVersion": "2026-09-01",
    "documents": [
      {
        "idType": "passport",
        "issuingCountries": [
          "GB",
          "DE"
        ],
        "requireBack": false
      },
      {
        "idType": "national_id",
        "issuingCountries": [
          "GB"
        ],
        "requireBack": true
      }
    ],
    "requireSelfie": true,
    "requireVoice": false,
    "allowDeviceUpload": false,
    "livenessMode": "passive",
    "emailVerification": "off",
    "allowedReturnUrlHosts": [
      "acme.example.com"
    ],
    "allowedEmbedOrigins": [],
    "updatedAt": 1790760000000
  },
  "requestId": "req-test"
}

Example request

curl

curl "https://collect.noxtica.com/v1/kyc/config" \
  -H "Authorization: Bearer $NOXTICA_API_KEY"

Node.js (global fetch, Node 18 or later)

const response = await fetch('https://collect.noxtica.com/v1/kyc/config', {
  method: 'GET',
  headers: {
    Authorization: `Bearer ${process.env.NOXTICA_API_KEY}`,
  },
});
console.log(response.status, await response.text());

Python (standard library)

import os
import urllib.request

request = urllib.request.Request(
    "https://collect.noxtica.com/v1/kyc/config",
    headers={
        "Authorization": f"Bearer {os.environ['NOXTICA_API_KEY']}",
    },
    method="GET",
)
with urllib.request.urlopen(request) as response:
    print(response.status, response.read().decode())

Read the current hosted session (browser-facing)

GET /v1/kyc/session-view

Called by the hosted verification page itself, never by a tenant backend. Noxtica Verification ID. Only when the Noxtica Verification ID (NVID) is enabled for the platform, the session object carries verificationIdentifier (KycVerificationIdentifier) — the NVID with its derived status and an emailOnFile flag — or null when the session has no NVID. It lets the page show the ID again when a finished link is reloaded. The key is absent while the feature is not enabled; the email address is never returned.

Authentication: Bearer token (SessionTokenAuth).

Responses

StatusMeaningError codesContent
200The session snapshot.application/json KycHostedSessionEnvelope
403Missing/disallowed Origin, or an invalid/expired session token.invalid_requestapplication/json ErrorEnvelope
404The 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 foundapplication/json
410The 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_cancelledapplication/json
429Rate limit exceeded. Wait Retry-After seconds (also retryAfter in the body) before retrying; the response carries no other rate-limit headers.Rate limit exceededapplication/json

Response headers

StatusHeaderTypeDescription
429Retry-AfterintegerSeconds until the bucket allows another request.

Response fields (200, application/json)

FieldTypeAlways returnedDescription
okbooleanNo
sessionobject (KycHostedSessionSnapshot)NoThe 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.statusstringYesThe hosted session lifecycle status. pending — minted, link not opened yet. opened — the customer reached the capture page. verifying — the customer submitted and the verification is running. review_pending — the verification finished and awaits a Backoffice review decision (today every successful hosted capture lands here; it never expires, and kyc.session.completed has NOT fired yet). completed — a business decision exists (a reviewer’s approval or override: read verification and review on the status read for which). unverified — finished with a negative outcome and no manual-review code. error — a provider or infrastructure fault, or the submit was refused at admission. expired — the effective deadline passed without a decision (the status read reports it with 200; kyc.session.completed never fires for it). cancelled_by_user — the customer cancelled on the hosted page. Allowed values: pending, opened, verifying, review_pending, completed, unverified, error, expired, cancelled_by_user.
session.idTypestringYes
session.captureStepstringYesThe furthest capture step the customer has reached. Allowed values: consent, id_front, id_back, selfie, document_near_face, liveness, voice, review, submit.
session.expiresAtinteger (int64)YesEffective 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.openedAtinteger (int64), nullableYesWhen the customer’s first hosted-flow action landed (the pending to opened transition); null until then.
session.stepsarray of stringYesThe full step vocabulary, in order.
session.configurationone of KycHostedSessionConfiguration or any, nullableYesThe hosted setup that applies to this session (the configuration of the domain it was created for, with every field the domain leaves unset taken from your organisation default; see GET /v1/kyc/config), filtered by this session’s options; null when no hosted configuration applies (the page shows “Verification isn’t ready yet”).
session.configuration{KycHostedSessionConfiguration}object (KycHostedSessionConfiguration)No
session.configuration{KycHostedSessionConfiguration}.businessNamestringYes
session.configuration{KycHostedSessionConfiguration}.purposestringYes
session.configuration{KycHostedSessionConfiguration}.privacyUrlstringYes
session.configuration{KycHostedSessionConfiguration}.supportUrlstring, nullableYes
session.configuration{KycHostedSessionConfiguration}.returnUrlstring, nullableYesThe 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}.privacyVersionstringYes
session.configuration{KycHostedSessionConfiguration}.biometricConsentVersionstringNoPresent only when biometric consent is enabled for the platform.
session.configuration{KycHostedSessionConfiguration}.documentsarray of objectYes
session.configuration{KycHostedSessionConfiguration}.documents[]objectNo
session.configuration{KycHostedSessionConfiguration}.documents[].idTypestringYesAllowed values: national_id, passport, driving_license, residence_permit.
session.configuration{KycHostedSessionConfiguration}.documents[].issuingCountriesarray of stringYesThe issuing countries the customer can pick for this document type, as ISO 3166-1 alpha-2 codes (CountryCode values), after the invitation’s hostedOptions.allowedIssuingCountries narrowing. At most 249 items.
session.configuration{KycHostedSessionConfiguration}.documents[].requireBackbooleanYes
session.configuration{KycHostedSessionConfiguration}.documents[].requiredIdentityFieldsarray of stringNoPresent only when your configuration sets it. The identity fields that must be read from this document type for the extraction to count as complete (extraction on the identity read); an empty list requires none. Absent means the default (see GET /v1/kyc/config). Unique items.
session.configuration{KycHostedSessionConfiguration}.requireSelfiebooleanYes
session.configuration{KycHostedSessionConfiguration}.requireVoicebooleanYes
session.configuration{KycHostedSessionConfiguration}.livenessModestringYesAllowed values: passive, assisted_review, full_challenge.
session.configuration{KycHostedSessionConfiguration}.optionsobject (KycHostedSessionOptions)YesInvitation-scoped hosted-flow switches, frozen into the session at mint. When you send hostedOptions it must be the complete object (every required key below, no merging with defaults); omit it to use the defaults (all four document types, allowDeviceUpload false, documentBack auto, documentNearFace false, activeLiveness true, speechAnalysis false, ageGenderEstimation false, emailVerification off). Inside steps, ageGenderEstimation and emailVerification are optional. allowedIssuingCountries is optional too.
session.configuration{KycHostedSessionConfiguration}.options.allowedDocumentTypesarray of stringYesAt least 1 item, at most 4 items, unique items.
session.configuration{KycHostedSessionConfiguration}.options.allowedIssuingCountriesarray of stringNoOptional. Narrows the invitation to these issuing countries: each document type your hosted configuration accepts keeps only the issuing countries that are also listed, a document type left with none is dropped, and a mint where no document type remains is refused with 400 invalid_hosted_options (field allowedIssuingCountries, reason no_matching_document_country). Absent means no narrowing. Any ISO 3166-1 alpha-2 code is accepted, including codes your configuration does not offer. At least 1 item, at most 249 items, unique items.
session.configuration{KycHostedSessionConfiguration}.options.allowDeviceUploadbooleanYes
session.configuration{KycHostedSessionConfiguration}.options.stepsobjectYes
session.configuration{KycHostedSessionConfiguration}.options.steps.documentBackstringYesAllowed values: auto.
session.configuration{KycHostedSessionConfiguration}.options.steps.documentNearFacebooleanYes
session.configuration{KycHostedSessionConfiguration}.options.steps.activeLivenessbooleanYes
session.configuration{KycHostedSessionConfiguration}.options.steps.speechAnalysisbooleanYes
session.configuration{KycHostedSessionConfiguration}.options.steps.ageGenderEstimationbooleanNoOptional; 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.emailVerificationstringNoOptional (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.selectionone of object or any, nullableYesThe customer’s document choice, once made.
session.selection{variant 1}objectNo
session.selection{variant 1}.idTypestringYes
session.selection{variant 1}.issuingCountrystringYesThe issuing country the customer chose, as an ISO 3166-1 alpha-2 code (one of the CountryCode values). Pattern ^[A-Z]{2}$.
session.consentone of KycHostedSessionConsent or any, nullableYesThe privacy-notice receipt; immutable once set.
session.consent{KycHostedSessionConsent}object (KycHostedSessionConsent)No
session.consent{KycHostedSessionConsent}.versionstringYes
session.consent{KycHostedSessionConsent}.acceptedAtinteger (int64)Yes
session.biometricConsentone of KycHostedSessionConsent or any, nullableNoThe biometric-processing consent receipt. Present only when biometric consent is enabled for the platform.
session.biometricConsent{KycHostedSessionConsent}object (KycHostedSessionConsent)No
session.biometricConsent{KycHostedSessionConsent}.versionstringYes
session.biometricConsent{KycHostedSessionConsent}.acceptedAtinteger (int64)Yes
session.speechProfilingConsentobjectNo
session.speechProfilingConsent.acceptedAtinteger (int64)Yes
session.ageGenderConsentobjectNo
session.ageGenderConsent.acceptedAtinteger (int64)Yes
session.evidenceobject (KycSessionEvidenceRefs)YesOpaque 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.frontstringNo
session.evidence.backstringNo
session.evidence.selfiestringNo
session.evidence.voicestringNo
session.evidence.documentNearFacestringNo
session.resumeobject (KycHostedSessionResume)YesWhether the customer link this snapshot answers for is still usable for further capture.
session.resume.eligiblebooleanYes
session.resume.reasonstringNoPresent 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.decisionobject (KycSessionDecision)NoThe decision block, present once the pipeline or a review decision has produced one. outcome maps from the session status alone: completed to approved, unverified to rejected, review_pending to review_pending, error to error. It is not the review decision: a session a reviewer overrode is completed too and reads approved here. Use verification and review to tell an approval from an override.
session.decision.outcomestringYesAllowed values: approved, rejected, review_pending, error.
session.decision.reasonCodesarray of stringYes
session.decision.decidedAtinteger (int64)YesUnix ms.
session.processingobjectNoCoarse “what is happening now” for the status page; absent while the customer is still capturing.
session.processing.stagestringYesAllowed values: uploading, queued, evaluating, review_pending, decided.
session.processing.updatedAtinteger (int64)Yes
session.evidenceQualityobjectNoPer-role structural quality receipt; absent until a role’s evidence has been inspected.
session.livenessStepsarray of objectNoThe configured liveness challenge with per-step completion.
session.livenessSteps[]objectNo
session.livenessSteps[].idstringYes
session.livenessSteps[].kindstringYes
session.livenessSteps[].completedbooleanYes
session.voiceChallengeobjectNoThe anti-replay digit phrase; present only when the tenant requires the voice step.
session.voiceChallenge.phrasestringYes
session.failureBudgetobjectYesAttempt counters per retryable step (front, back, selfie, liveness, voice, upload).
session.failureBudget.*objectNo
session.failureBudget.*.attemptsintegerYes
session.failureBudget.*.maxintegerYes
session.deviceChoicestringNoA previously recorded cross-device continuation choice; absent when none was recorded. Allowed values: desktop, phone.
session.serverNowinteger (int64)YesThe server clock when the snapshot was built; the page computes remaining time against it, not against its own clock.
session.sessionExpiresAtinteger (int64)YesThe same deadline as expiresAt, named for the two-timer UI.
session.currentStepstring, nullableNoThe step the customer most recently entered. Present only when per-step timers are enabled for the platform.
session.stepStartedAtinteger (int64), nullableNo
session.stepExpiresAtinteger (int64), nullableNomin(stepStartedAt + 10 minutes, sessionExpiresAt); null for an untimed step.
session.capabilitiesobject (KycHostedSessionCapabilities)NoServer 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.cancelbooleanNoThe collector accepts POST /v1/kyc/session-cancel for this session. Allowed values: true.
session.capabilities.emailVerificationbooleanNoThe hosted flow includes the email-verification step for this session. Allowed values: true.
session.emailVerificationobject (KycEmailVerificationState)NoThe 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.modestringYesAllowed values: optional, required.
session.emailVerification.emailstring, nullableYesThe masked address; null before one was entered.
session.emailVerification.verifiedbooleanYes
session.emailVerification.verifiedAtinteger (int64), nullableYes
session.emailVerification.expiresAtinteger (int64), nullableYesDeadline of the live confirmation link (30 minutes or the session end, whichever is sooner); null when none is live.
session.emailVerification.resendAvailableAtinteger (int64), nullableYesEarliest next send while the 60 second cooldown runs; null when a resend is allowed now or the cap is exhausted.
session.emailVerification.sendsRemainingintegerYesSends left of the 5 allowed per session.
session.verificationIdentifierone of KycVerificationIdentifier or any, nullableNoPresent only while the NVID is enabled for the platform; null when the session has none.
session.verificationIdentifier{KycVerificationIdentifier}object (KycVerificationIdentifier)NoThe 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}.nvidstringYesCanonical NVID, NVID-XXXX-XXXX-XXXX-XXXX.
session.verificationIdentifier{KycVerificationIdentifier}.statusstringYesPublic 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}.verifiedAtinteger (int64), nullableYesUnix ms of completion; null while pending.
session.verificationIdentifier{KycVerificationIdentifier}.expiresAtinteger (int64), nullableYesUnix ms, completion + 365 days; null while pending.
session.verificationIdentifier{KycVerificationIdentifier}.emailOnFilebooleanYesWhether the person gave an address to receive the NVID. The address itself is never returned.
session.evidenceUploadobjectNoCapability marker for step-wise evidence upload; present only when that capability is enabled for the platform.
session.evidenceReceiptsobjectNoServer-authoritative per-step evidence receipts; present with evidenceUpload.
session.scorecardobjectNoThe 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_frontobject (KycCustomerScorecardEntry)No
session.scorecard.document_front.statusstringYesAllowed values: pass, needs_review, failed, skipped, pending.
session.scorecard.document_backobject (KycCustomerScorecardEntry)No
session.scorecard.document_back.statusstringYesAllowed values: pass, needs_review, failed, skipped, pending.
session.scorecard.selfieobject (KycCustomerScorecardEntry)No
session.scorecard.selfie.statusstringYesAllowed values: pass, needs_review, failed, skipped, pending.
session.scorecard.livenessobject (KycCustomerScorecardEntry)No
session.scorecard.liveness.statusstringYesAllowed values: pass, needs_review, failed, skipped, pending.
session.scorecard.voiceobject (KycCustomerScorecardEntry)No
session.scorecard.voice.statusstringYesAllowed values: pass, needs_review, failed, skipped, pending.
session.scorecard.cross_validationobject (KycCustomerScorecardEntry)No
session.scorecard.cross_validation.statusstringYesAllowed values: pass, needs_review, failed, skipped, pending.
session.scorecard.overallobject (KycCustomerScorecardEntry)No
session.scorecard.overall.statusstringYesAllowed values: pass, needs_review, failed, skipped, pending.
requestIdstringNo

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

StatusMeaningError codesContent
200Checkpoint recorded; returns the updated snapshot.application/json KycHostedSessionEnvelope
400The 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_requestapplication/json ErrorEnvelope
403Missing/disallowed Origin, or an invalid/expired session token.invalid_requestapplication/json ErrorEnvelope
404The 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 foundapplication/json
409The session is already terminal.invalid_requestapplication/json ErrorEnvelope
410The 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_cancelledapplication/json
429Rate limit exceeded. Wait Retry-After seconds (also retryAfter in the body) before retrying; the response carries no other rate-limit headers.Rate limit exceededapplication/json

Response headers

StatusHeaderTypeDescription
429Retry-AfterintegerSeconds until the bucket allows another request.

Response fields (200, application/json)

FieldTypeAlways returnedDescription
okbooleanNo
sessionobject (KycHostedSessionSnapshot)NoThe 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.statusstringYesThe hosted session lifecycle status. pending — minted, link not opened yet. opened — the customer reached the capture page. verifying — the customer submitted and the verification is running. review_pending — the verification finished and awaits a Backoffice review decision (today every successful hosted capture lands here; it never expires, and kyc.session.completed has NOT fired yet). completed — a business decision exists (a reviewer’s approval or override: read verification and review on the status read for which). unverified — finished with a negative outcome and no manual-review code. error — a provider or infrastructure fault, or the submit was refused at admission. expired — the effective deadline passed without a decision (the status read reports it with 200; kyc.session.completed never fires for it). cancelled_by_user — the customer cancelled on the hosted page. Allowed values: pending, opened, verifying, review_pending, completed, unverified, error, expired, cancelled_by_user.
session.idTypestringYes
session.captureStepstringYesThe furthest capture step the customer has reached. Allowed values: consent, id_front, id_back, selfie, document_near_face, liveness, voice, review, submit.
session.expiresAtinteger (int64)YesEffective 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.openedAtinteger (int64), nullableYesWhen the customer’s first hosted-flow action landed (the pending to opened transition); null until then.
session.stepsarray of stringYesThe full step vocabulary, in order.
session.configurationone of KycHostedSessionConfiguration or any, nullableYesThe hosted setup that applies to this session (the configuration of the domain it was created for, with every field the domain leaves unset taken from your organisation default; see GET /v1/kyc/config), filtered by this session’s options; null when no hosted configuration applies (the page shows “Verification isn’t ready yet”).
session.configuration{KycHostedSessionConfiguration}object (KycHostedSessionConfiguration)No
session.configuration{KycHostedSessionConfiguration}.businessNamestringYes
session.configuration{KycHostedSessionConfiguration}.purposestringYes
session.configuration{KycHostedSessionConfiguration}.privacyUrlstringYes
session.configuration{KycHostedSessionConfiguration}.supportUrlstring, nullableYes
session.configuration{KycHostedSessionConfiguration}.returnUrlstring, nullableYesThe 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}.privacyVersionstringYes
session.configuration{KycHostedSessionConfiguration}.biometricConsentVersionstringNoPresent only when biometric consent is enabled for the platform.
session.configuration{KycHostedSessionConfiguration}.documentsarray of objectYes
session.configuration{KycHostedSessionConfiguration}.documents[]objectNo
session.configuration{KycHostedSessionConfiguration}.documents[].idTypestringYesAllowed values: national_id, passport, driving_license, residence_permit.
session.configuration{KycHostedSessionConfiguration}.documents[].issuingCountriesarray of stringYesThe issuing countries the customer can pick for this document type, as ISO 3166-1 alpha-2 codes (CountryCode values), after the invitation’s hostedOptions.allowedIssuingCountries narrowing. At most 249 items.
session.configuration{KycHostedSessionConfiguration}.documents[].requireBackbooleanYes
session.configuration{KycHostedSessionConfiguration}.documents[].requiredIdentityFieldsarray of stringNoPresent only when your configuration sets it. The identity fields that must be read from this document type for the extraction to count as complete (extraction on the identity read); an empty list requires none. Absent means the default (see GET /v1/kyc/config). Unique items.
session.configuration{KycHostedSessionConfiguration}.requireSelfiebooleanYes
session.configuration{KycHostedSessionConfiguration}.requireVoicebooleanYes
session.configuration{KycHostedSessionConfiguration}.livenessModestringYesAllowed values: passive, assisted_review, full_challenge.
session.configuration{KycHostedSessionConfiguration}.optionsobject (KycHostedSessionOptions)YesInvitation-scoped hosted-flow switches, frozen into the session at mint. When you send hostedOptions it must be the complete object (every required key below, no merging with defaults); omit it to use the defaults (all four document types, allowDeviceUpload false, documentBack auto, documentNearFace false, activeLiveness true, speechAnalysis false, ageGenderEstimation false, emailVerification off). Inside steps, ageGenderEstimation and emailVerification are optional. allowedIssuingCountries is optional too.
session.configuration{KycHostedSessionConfiguration}.options.allowedDocumentTypesarray of stringYesAt least 1 item, at most 4 items, unique items.
session.configuration{KycHostedSessionConfiguration}.options.allowedIssuingCountriesarray of stringNoOptional. Narrows the invitation to these issuing countries: each document type your hosted configuration accepts keeps only the issuing countries that are also listed, a document type left with none is dropped, and a mint where no document type remains is refused with 400 invalid_hosted_options (field allowedIssuingCountries, reason no_matching_document_country). Absent means no narrowing. Any ISO 3166-1 alpha-2 code is accepted, including codes your configuration does not offer. At least 1 item, at most 249 items, unique items.
session.configuration{KycHostedSessionConfiguration}.options.allowDeviceUploadbooleanYes
session.configuration{KycHostedSessionConfiguration}.options.stepsobjectYes
session.configuration{KycHostedSessionConfiguration}.options.steps.documentBackstringYesAllowed values: auto.
session.configuration{KycHostedSessionConfiguration}.options.steps.documentNearFacebooleanYes
session.configuration{KycHostedSessionConfiguration}.options.steps.activeLivenessbooleanYes
session.configuration{KycHostedSessionConfiguration}.options.steps.speechAnalysisbooleanYes
session.configuration{KycHostedSessionConfiguration}.options.steps.ageGenderEstimationbooleanNoOptional; 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.emailVerificationstringNoOptional (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.selectionone of object or any, nullableYesThe customer’s document choice, once made.
session.selection{variant 1}objectNo
session.selection{variant 1}.idTypestringYes
session.selection{variant 1}.issuingCountrystringYesThe issuing country the customer chose, as an ISO 3166-1 alpha-2 code (one of the CountryCode values). Pattern ^[A-Z]{2}$.
session.consentone of KycHostedSessionConsent or any, nullableYesThe privacy-notice receipt; immutable once set.
session.consent{KycHostedSessionConsent}object (KycHostedSessionConsent)No
session.consent{KycHostedSessionConsent}.versionstringYes
session.consent{KycHostedSessionConsent}.acceptedAtinteger (int64)Yes
session.biometricConsentone of KycHostedSessionConsent or any, nullableNoThe biometric-processing consent receipt. Present only when biometric consent is enabled for the platform.
session.biometricConsent{KycHostedSessionConsent}object (KycHostedSessionConsent)No
session.biometricConsent{KycHostedSessionConsent}.versionstringYes
session.biometricConsent{KycHostedSessionConsent}.acceptedAtinteger (int64)Yes
session.speechProfilingConsentobjectNo
session.speechProfilingConsent.acceptedAtinteger (int64)Yes
session.ageGenderConsentobjectNo
session.ageGenderConsent.acceptedAtinteger (int64)Yes
session.evidenceobject (KycSessionEvidenceRefs)YesOpaque 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.frontstringNo
session.evidence.backstringNo
session.evidence.selfiestringNo
session.evidence.voicestringNo
session.evidence.documentNearFacestringNo
session.resumeobject (KycHostedSessionResume)YesWhether the customer link this snapshot answers for is still usable for further capture.
session.resume.eligiblebooleanYes
session.resume.reasonstringNoPresent 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.decisionobject (KycSessionDecision)NoThe decision block, present once the pipeline or a review decision has produced one. outcome maps from the session status alone: completed to approved, unverified to rejected, review_pending to review_pending, error to error. It is not the review decision: a session a reviewer overrode is completed too and reads approved here. Use verification and review to tell an approval from an override.
session.decision.outcomestringYesAllowed values: approved, rejected, review_pending, error.
session.decision.reasonCodesarray of stringYes
session.decision.decidedAtinteger (int64)YesUnix ms.
session.processingobjectNoCoarse “what is happening now” for the status page; absent while the customer is still capturing.
session.processing.stagestringYesAllowed values: uploading, queued, evaluating, review_pending, decided.
session.processing.updatedAtinteger (int64)Yes
session.evidenceQualityobjectNoPer-role structural quality receipt; absent until a role’s evidence has been inspected.
session.livenessStepsarray of objectNoThe configured liveness challenge with per-step completion.
session.livenessSteps[]objectNo
session.livenessSteps[].idstringYes
session.livenessSteps[].kindstringYes
session.livenessSteps[].completedbooleanYes
session.voiceChallengeobjectNoThe anti-replay digit phrase; present only when the tenant requires the voice step.
session.voiceChallenge.phrasestringYes
session.failureBudgetobjectYesAttempt counters per retryable step (front, back, selfie, liveness, voice, upload).
session.failureBudget.*objectNo
session.failureBudget.*.attemptsintegerYes
session.failureBudget.*.maxintegerYes
session.deviceChoicestringNoA previously recorded cross-device continuation choice; absent when none was recorded. Allowed values: desktop, phone.
session.serverNowinteger (int64)YesThe server clock when the snapshot was built; the page computes remaining time against it, not against its own clock.
session.sessionExpiresAtinteger (int64)YesThe same deadline as expiresAt, named for the two-timer UI.
session.currentStepstring, nullableNoThe step the customer most recently entered. Present only when per-step timers are enabled for the platform.
session.stepStartedAtinteger (int64), nullableNo
session.stepExpiresAtinteger (int64), nullableNomin(stepStartedAt + 10 minutes, sessionExpiresAt); null for an untimed step.
session.capabilitiesobject (KycHostedSessionCapabilities)NoServer 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.cancelbooleanNoThe collector accepts POST /v1/kyc/session-cancel for this session. Allowed values: true.
session.capabilities.emailVerificationbooleanNoThe hosted flow includes the email-verification step for this session. Allowed values: true.
session.emailVerificationobject (KycEmailVerificationState)NoThe 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.modestringYesAllowed values: optional, required.
session.emailVerification.emailstring, nullableYesThe masked address; null before one was entered.
session.emailVerification.verifiedbooleanYes
session.emailVerification.verifiedAtinteger (int64), nullableYes
session.emailVerification.expiresAtinteger (int64), nullableYesDeadline of the live confirmation link (30 minutes or the session end, whichever is sooner); null when none is live.
session.emailVerification.resendAvailableAtinteger (int64), nullableYesEarliest next send while the 60 second cooldown runs; null when a resend is allowed now or the cap is exhausted.
session.emailVerification.sendsRemainingintegerYesSends left of the 5 allowed per session.
session.verificationIdentifierone of KycVerificationIdentifier or any, nullableNoPresent only while the NVID is enabled for the platform; null when the session has none.
session.verificationIdentifier{KycVerificationIdentifier}object (KycVerificationIdentifier)NoThe 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}.nvidstringYesCanonical NVID, NVID-XXXX-XXXX-XXXX-XXXX.
session.verificationIdentifier{KycVerificationIdentifier}.statusstringYesPublic 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}.verifiedAtinteger (int64), nullableYesUnix ms of completion; null while pending.
session.verificationIdentifier{KycVerificationIdentifier}.expiresAtinteger (int64), nullableYesUnix ms, completion + 365 days; null while pending.
session.verificationIdentifier{KycVerificationIdentifier}.emailOnFilebooleanYesWhether the person gave an address to receive the NVID. The address itself is never returned.
session.evidenceUploadobjectNoCapability marker for step-wise evidence upload; present only when that capability is enabled for the platform.
session.evidenceReceiptsobjectNoServer-authoritative per-step evidence receipts; present with evidenceUpload.
session.scorecardobjectNoThe 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_frontobject (KycCustomerScorecardEntry)No
session.scorecard.document_front.statusstringYesAllowed values: pass, needs_review, failed, skipped, pending.
session.scorecard.document_backobject (KycCustomerScorecardEntry)No
session.scorecard.document_back.statusstringYesAllowed values: pass, needs_review, failed, skipped, pending.
session.scorecard.selfieobject (KycCustomerScorecardEntry)No
session.scorecard.selfie.statusstringYesAllowed values: pass, needs_review, failed, skipped, pending.
session.scorecard.livenessobject (KycCustomerScorecardEntry)No
session.scorecard.liveness.statusstringYesAllowed values: pass, needs_review, failed, skipped, pending.
session.scorecard.voiceobject (KycCustomerScorecardEntry)No
session.scorecard.voice.statusstringYesAllowed values: pass, needs_review, failed, skipped, pending.
session.scorecard.cross_validationobject (KycCustomerScorecardEntry)No
session.scorecard.cross_validation.statusstringYesAllowed values: pass, needs_review, failed, skipped, pending.
session.scorecard.overallobject (KycCustomerScorecardEntry)No
session.scorecard.overall.statusstringYesAllowed values: pass, needs_review, failed, skipped, pending.
requestIdstringNo

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.

FieldTypeRequiredDescription
reasonstringNoThe 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

StatusMeaningError codesContent
200The 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
400invalid_request — invalid body, or reason outside the closed enum.invalid_requestapplication/json ErrorEnvelope
401invalid_session — the session token is invalid.invalid_sessionapplication/json ErrorEnvelope
403origin_not_allowed — missing or disallowed Origin (same origin enforcement as every other hosted route).origin_not_allowedapplication/json ErrorEnvelope
404The 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 foundapplication/json
409session_not_cancellable — the session is already verifying, completed, unverified, error, or review_pending; status names which.session_not_cancellableapplication/json KycSessionNotCancellableError
410The 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_expiredapplication/json KycSessionExpiredError
429Rate limit exceeded. Wait Retry-After seconds (also retryAfter in the body) before retrying; the response carries no other rate-limit headers.Rate limit exceededapplication/json
503invalid_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_requestapplication/json ErrorEnvelope

Response headers

StatusHeaderTypeDescription
429Retry-AfterintegerSeconds until the bucket allows another request.

Response fields (200, application/json)

FieldTypeAlways returnedDescription
okbooleanYesAllowed values: true.
statusstringYesAllowed values: cancelled_by_user.
cancelledAtinteger (int64)YesUnix ms of the cancel. An idempotent replay returns the ORIGINAL value, never a fresh timestamp.
cancelReasonone of KycSessionCancelReason or any, nullableYesThe reason recorded by the FIRST successful call (null if none was given). A replay returns the original reason.
requestIdstringYes

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

StatusMeaningError codesContent
200A resolved verify attempt (verified, unverified, or review_pending).application/json KycVerifyResponse
400idempotency_key_invalid or payload_invalid — both new distinct codes as of 2026-09-13.idempotency_key_invalid, invalid_request, payload_invalidapplication/json ErrorEnvelope
403Missing/disallowed Origin, an invalid/expired session token, or evidence_refused with refusal evidence_not_provisioned or residency_unconfigured (the organisation’s raw-evidence storage is no longer provisioned; nothing was consumed and the session stays retryable).evidence_refusedapplication/json KycSessionEvidenceError
404The 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 foundapplication/json
409session_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_consumedapplication/json ErrorEnvelope
410The 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_cancelledapplication/json
429session_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_limitedapplication/json ErrorEnvelope
503nonce_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_errorapplication/json ErrorEnvelope

Response fields (200, application/json)

FieldTypeAlways returnedDescription
okbooleanYesAllowed values: true.
verdictstringYeserror is an infra/provider fault, never a statement about the subject — always HTTP 200 (FR-6). Allowed values: verified, unverified, error.
verificationLevelstringYesAllowed values: verified, partial, unverified, inconclusive.
reasonCodesarray of stringYes
verificationIdstring (uuid)Yes
requestIdstringYes
legalBasisRefstringNoPresent only when this request’s tenant is running under classified legal-basis receipt gating.
nfcAssuranceobjectNoPresent only for eMRTD/passport-chip verification paths.
deviceTrustobjectNoPresent 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.

FieldTypeRequiredDescription
emailstringYesThe address to send the NVID to (normalised server-side). At most 254 characters.
localestringYesOne of the 36 supported UI locales; the email is rendered in it (unsupported falls back to en).
consentobjectYes
consent.versionstringYesAllowed values: nvid-email-v1.
consent.acceptedbooleanYesAllowed values: true.

Responses

StatusMeaningError codesContent
200The email was accepted for delivery.application/json
400invalid_email or consent_required.consent_required, invalid_emailapplication/json ErrorEnvelope
403Missing/disallowed Origin, or an invalid/expired session token.origin_not_allowedapplication/json ErrorEnvelope
404The 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 foundapplication/json
409verification_id_unavailable — the session has no NVID, or it was revoked.verification_id_unavailableapplication/json ErrorEnvelope
410The 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_cancelledapplication/json
429Rate 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_limitedapplication/json
502email_send_failed — the provider refused or failed; the send slot is released.email_send_failedapplication/json ErrorEnvelope
503email_unavailable — the address cannot be sealed right now; nothing was stored.email_unavailableapplication/json ErrorEnvelope

Response headers

StatusHeaderTypeDescription
429Retry-AfterintegerSeconds until the bucket allows another request.

Response fields (200, application/json)

FieldTypeAlways returnedDescription
okbooleanYesAllowed values: true.

Response example (200)

{
  "ok": true
}

Example request

curl

curl -X POST "https://collect.noxtica.com/v1/kyc/session-verification-id/email" \
  -H "Authorization: Bearer $NOXTICA_SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "email": "[email protected]",
  "locale": "en",
  "consent": {
    "version": "nvid-email-v1",
    "accepted": true
  }
}'

Node.js (global fetch, Node 18 or later)

const response = await fetch('https://collect.noxtica.com/v1/kyc/session-verification-id/email', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.NOXTICA_SESSION_TOKEN}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    email: '[email protected]',
    locale: 'en',
    consent: {
      version: 'nvid-email-v1',
      accepted: true,
    },
  }),
});
console.log(response.status, await response.text());

Python (standard library)

import json
import os
import urllib.request

request = urllib.request.Request(
    "https://collect.noxtica.com/v1/kyc/session-verification-id/email",
    data=json.dumps({
        "email": "[email protected]",
        "locale": "en",
        "consent": {
            "version": "nvid-email-v1",
            "accepted": True,
        },
    }).encode(),
    headers={
        "Authorization": f"Bearer {os.environ['NOXTICA_SESSION_TOKEN']}",
        "Content-Type": "application/json",
    },
    method="POST",
)
with urllib.request.urlopen(request) as response:
    print(response.status, response.read().decode())

Send the email-verification link for the address the customer typed (browser-facing)

POST /v1/kyc/session-email/send

Called by the hosted verification page, never by a tenant backend. Available only while hosted email verification is enabled for the platform AND the session’s effective emailVerification mode (fixed at mint) is optional or required; otherwise the byte-identical gated 404 (platform off) or 409 email_verification_not_enabled (mode off). Sends one confirmation link to the address; the link is valid for 30 minutes or until the session ends, whichever is sooner. Opening the link changes nothing: the customer must press the button on the page it opens, because mail gateways prefetch links. Limits per session: at most 5 accepted sends, at least 60 seconds between sends (429 cooldown with retryAfterSec, 429 send_limit_reached), no send with less than 60 seconds of session left (409 session_expiring). Per client IP 20 sends an hour and per address 5 sends an hour (429 Rate limit exceeded). A provider failure rolls the send back (502 email_send_failed) so an outage does not spend the customer’s quota. The response carries the masked address only.

Authentication: Bearer token (SessionTokenAuth).

Request body

Content type application/json, required.

FieldTypeRequiredDescription
emailstringYesThe address to verify (normalised server-side). At most 254 characters.
localestringNoOne of the supported UI locales; the email is rendered in it (an unsupported value falls back to en).

Responses

StatusMeaningError codesContent
200The link was sent.application/json KycEmailVerificationStateEnvelope
400invalid_email, or invalid_request (unreadable body).invalid_email, invalid_requestapplication/json ErrorEnvelope
401invalid_session — no session bearer, or one that does not resolve to a session.invalid_sessionapplication/json ErrorEnvelope
403Missing or disallowed Origin.origin_not_allowedapplication/json ErrorEnvelope
404The 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 foundapplication/json
409email_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_editableapplication/json ErrorEnvelope
410The 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_cancelledapplication/json
413body_too_large — the request exceeded this route’s byte cap.body_too_largeapplication/json ErrorEnvelope
429cooldown (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_reachedapplication/json ErrorEnvelope
502email_send_failed — the mail provider refused or failed; the send was rolled back.email_send_failedapplication/json ErrorEnvelope
503hosted_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_unavailableapplication/json ErrorEnvelope

Response fields (200, application/json)

FieldTypeAlways returnedDescription
emailVerificationobject (KycEmailVerificationState)YesThe 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.modestringYesAllowed values: optional, required.
emailVerification.emailstring, nullableYesThe masked address; null before one was entered.
emailVerification.verifiedbooleanYes
emailVerification.verifiedAtinteger (int64), nullableYes
emailVerification.expiresAtinteger (int64), nullableYesDeadline of the live confirmation link (30 minutes or the session end, whichever is sooner); null when none is live.
emailVerification.resendAvailableAtinteger (int64), nullableYesEarliest next send while the 60 second cooldown runs; null when a resend is allowed now or the cap is exhausted.
emailVerification.sendsRemainingintegerYesSends left of the 5 allowed per session.
requestIdstringYes

Response example (200)

{
  "emailVerification": {
    "mode": "required",
    "email": "j***@example.com",
    "verified": false,
    "verifiedAt": null,
    "expiresAt": 1790764000000,
    "resendAvailableAt": 1790762260000,
    "sendsRemaining": 4
  },
  "requestId": "req-test"
}

Example request

curl

curl -X POST "https://collect.noxtica.com/v1/kyc/session-email/send" \
  -H "Authorization: Bearer $NOXTICA_SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "email": "[email protected]",
  "locale": "en"
}'

Node.js (global fetch, Node 18 or later)

const response = await fetch('https://collect.noxtica.com/v1/kyc/session-email/send', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.NOXTICA_SESSION_TOKEN}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    email: '[email protected]',
    locale: 'en',
  }),
});
console.log(response.status, await response.text());

Python (standard library)

import json
import os
import urllib.request

request = urllib.request.Request(
    "https://collect.noxtica.com/v1/kyc/session-email/send",
    data=json.dumps({
        "email": "[email protected]",
        "locale": "en",
    }).encode(),
    headers={
        "Authorization": f"Bearer {os.environ['NOXTICA_SESSION_TOKEN']}",
        "Content-Type": "application/json",
    },
    method="POST",
)
with urllib.request.urlopen(request) as response:
    print(response.status, response.read().decode())

Poll the email-verification state (browser-facing)

GET /v1/kyc/session-email/status

Called by the hosted verification page while the customer is in their mail app; it builds no snapshot, just the emailVerification state (masked address). Same availability as POST /v1/kyc/session-email/send: the byte-identical gated 404 while the platform has the feature off, 409 email_verification_not_enabled for a session whose mode is off.

Authentication: Bearer token (SessionTokenAuth).

Responses

StatusMeaningError codesContent
200The current state.application/json KycEmailVerificationStateEnvelope
401invalid_session.invalid_sessionapplication/json ErrorEnvelope
403Missing or disallowed Origin.origin_not_allowedapplication/json ErrorEnvelope
404The 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 foundapplication/json
409email_verification_not_enabled — the session’s effective mode is off.email_verification_not_enabledapplication/json ErrorEnvelope
410The 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_cancelledapplication/json
429Rate limit exceeded. Wait Retry-After seconds (also retryAfter in the body) before retrying; the response carries no other rate-limit headers.Rate limit exceededapplication/json
503session_backend_unavailable or invalid_request — transient; retry.invalid_request, session_backend_unavailableapplication/json ErrorEnvelope

Response headers

StatusHeaderTypeDescription
429Retry-AfterintegerSeconds until the bucket allows another request.

Response fields (200, application/json)

FieldTypeAlways returnedDescription
emailVerificationobject (KycEmailVerificationState)YesThe 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.modestringYesAllowed values: optional, required.
emailVerification.emailstring, nullableYesThe masked address; null before one was entered.
emailVerification.verifiedbooleanYes
emailVerification.verifiedAtinteger (int64), nullableYes
emailVerification.expiresAtinteger (int64), nullableYesDeadline of the live confirmation link (30 minutes or the session end, whichever is sooner); null when none is live.
emailVerification.resendAvailableAtinteger (int64), nullableYesEarliest next send while the 60 second cooldown runs; null when a resend is allowed now or the cap is exhausted.
emailVerification.sendsRemainingintegerYesSends left of the 5 allowed per session.
requestIdstringYes

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.

FieldTypeRequiredDescription
tokenstringYesThe link token, kyev.<tenant>.<secret>. At most 512 characters.

Responses

StatusMeaningError codesContent
200The address is now verified for that session.application/json
400invalid_or_expired (every token problem), or invalid_request (unreadable body).invalid_or_expired, invalid_requestapplication/json ErrorEnvelope
403Missing or disallowed Origin.origin_not_allowedapplication/json ErrorEnvelope
404The 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 foundapplication/json
413body_too_large — the request exceeded this route’s byte cap.body_too_largeapplication/json ErrorEnvelope
429Rate limit exceeded. Wait Retry-After seconds (also retryAfter in the body) before retrying; the response carries no other rate-limit headers.Rate limit exceededapplication/json
503temporarily_unavailable — transient fault; retry after Retry-After seconds.temporarily_unavailableapplication/json ErrorEnvelope

Response headers

StatusHeaderTypeDescription
429Retry-AfterintegerSeconds until the bucket allows another request.

Response fields (200, application/json)

FieldTypeAlways returnedDescription
okbooleanYesAllowed values: true.
requestIdstringYes

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

NameInTypeRequiredDescription
idquerystringYesThe NVID, canonical form NVID-XXXX-XXXX-XXXX-XXXX (the collector also accepts the lower-case, unhyphenated and O/I/L-confusable spellings).

Responses

StatusMeaningError codesContent
200The ID’s public status.application/json KycVerificationIdStatusResponse
403Missing or disallowed Origin.origin_not_allowedapplication/json ErrorEnvelope
404not_found — unknown, erased or malformed ID (identical bodies), or the feature is not enabled for the platform (the gated 404 shape).not_foundapplication/json
429Per-IP limit exceeded (60 requests per 60 seconds). Retry-After carries the seconds.rate_limitedapplication/json
503Temporarily unavailable; retry after Retry-After seconds (2).temporarily_unavailableapplication/json

Response headers

StatusHeaderTypeDescription
200Cache-Controlstring
429Retry-Afterinteger
503Retry-Afterinteger

Response fields (200, application/json)

FieldTypeAlways returnedDescription
okbooleanYesAllowed values: true.
verificationobjectYes
verification.idstringYesCanonical NVID.
verification.statusstringYesPublic 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.verifiedAtinteger (int64), nullableYesUnix ms of completion; null while pending.
verification.expiresAtinteger (int64), nullableYesUnix ms, completion + 365 days; null while pending.
verification.revokedAtinteger (int64), nullableYesUnix ms of the revocation; null unless revoked.
verification.requestedBystring, nullableYesThe 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

StatusMeaningError codesContent
200Handle minted.application/json
400The 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_requestapplication/json ErrorEnvelope
403Missing/disallowed Origin, an invalid/expired session token, device_upload_disabled, or evidence_refused with a closed refusal: evidence_not_provisioned (the organisation’s raw-evidence storage was never armed), residency_unconfigured (armed, but no evidence residency of TR, EU or US), residency_refused (the residency route refused the data class and jurisdiction pair, the only true region refusal), binding_unprovisioned, evidence_storage_disabled or verification_not_admitted. Treat an unknown refusal as a generic unavailable answer; never as a regional restriction.evidence_refusedapplication/json KycSessionEvidenceError
404The 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 foundapplication/json
410The 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_cancelledapplication/json
429Rate limit exceeded. Wait Retry-After seconds (also retryAfter in the body) before retrying; the response carries no other rate-limit headers.Rate limit exceededapplication/json

Response headers

StatusHeaderTypeDescription
429Retry-AfterintegerSeconds 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

StatusMeaningError codesContent
200Terminal inspection outcome.application/json
400The 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_requestapplication/json ErrorEnvelope
403Missing/disallowed Origin, an invalid/expired session token, or a failed handle verification.invalid_requestapplication/json ErrorEnvelope
404The 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 foundapplication/json
410The 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_cancelledapplication/json
429Rate limit exceeded. Wait Retry-After seconds (also retryAfter in the body) before retrying; the response carries no other rate-limit headers.Rate limit exceededapplication/json

Response headers

StatusHeaderTypeDescription
429Retry-AfterintegerSeconds 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.

FieldTypeRequiredDescription
attestationIdstringYes

Responses

StatusMeaningError codesContent
200Always 200 (fail-closed discipline).application/json
401The 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_keyapplication/json KycSessionError
403The presented key lacks the required scope.API key does not have write:identity scopeapplication/json
404The 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 foundapplication/json
429Rate limit exceeded. Wait Retry-After seconds (also retryAfter in the body) before retrying; the response carries no other rate-limit headers.Rate limit exceededapplication/json
503Noxtica could not read the key’s record. This is a temporary fault on Noxtica’s side and says nothing about your key. Retry after the Retry-After delay; do not treat it as a revoked key.api_key_auth_unavailableapplication/json

Response headers

StatusHeaderTypeDescription
429Retry-AfterintegerSeconds until the bucket allows another request.
503Retry-AfterintegerSeconds to wait before retrying.

Response fields (200, application/json)

FieldTypeAlways returnedDescription
okbooleanNoAllowed values: true.
requestedbooleanNo
statusstringNoAllowed values: subject_not_attested, requested, erasure_request_failed.
requestIdstringNo

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.

FieldTypeRequiredDescription
attestationIdstringYes
receiptIdstringYes

Responses

StatusMeaningError codesContent
200Always 200 (fail-closed discipline) — a false activated:true is never returned.application/json
401The 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_keyapplication/json KycSessionError
403The presented key lacks the required scope.API key does not have write:identity scopeapplication/json
404The 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 foundapplication/json
429Rate limit exceeded. Wait Retry-After seconds (also retryAfter in the body) before retrying; the response carries no other rate-limit headers.Rate limit exceededapplication/json
503Noxtica could not read the key’s record. This is a temporary fault on Noxtica’s side and says nothing about your key. Retry after the Retry-After delay; do not treat it as a revoked key.api_key_auth_unavailableapplication/json

Response headers

StatusHeaderTypeDescription
429Retry-AfterintegerSeconds until the bucket allows another request.
503Retry-AfterintegerSeconds to wait before retrying.

Response fields (200, application/json)

FieldTypeAlways returnedDescription
okbooleanNoAllowed values: true.
activatedbooleanNo
reasonstringNoPresent when activated is false (e.g. an unresolvable/foreign/stale attestation or receipt).
capabilityobjectNoPresent only when activated is true.
capability.proofStatusstringNoAllowed values: proofed_unbound.
capability.verificationLevelstringNoAllowed values: verified, partial, unverified, inconclusive.
capability.receiptIdstringNo
capability.activatedAtintegerNo
requestIdstringNo

Response example (200)

{
  "ok": true,
  "activated": true,
  "reason": "user_requested",
  "capability": {
    "proofStatus": "proofed_unbound",
    "verificationLevel": "verified",
    "receiptId": "receipt_2f6a8b3c9d4e5f60718293a4b5c6d7e8",
    "activatedAt": 1
  },
  "requestId": "req-test"
}

Example request

curl

curl -X POST "https://collect.noxtica.com/v1/kyc/activate" \
  -H "Authorization: Bearer $NOXTICA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "attestationId": "att_9c6b2f7e4a3d4c8e9f1a2b3c4d5e6f70",
  "receiptId": "receipt_2f6a8b3c9d4e5f60718293a4b5c6d7e8"
}'

Node.js (global fetch, Node 18 or later)

const response = await fetch('https://collect.noxtica.com/v1/kyc/activate', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.NOXTICA_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    attestationId: 'att_9c6b2f7e4a3d4c8e9f1a2b3c4d5e6f70',
    receiptId: 'receipt_2f6a8b3c9d4e5f60718293a4b5c6d7e8',
  }),
});
console.log(response.status, await response.text());

Python (standard library)

import json
import os
import urllib.request

request = urllib.request.Request(
    "https://collect.noxtica.com/v1/kyc/activate",
    data=json.dumps({
        "attestationId": "att_9c6b2f7e4a3d4c8e9f1a2b3c4d5e6f70",
        "receiptId": "receipt_2f6a8b3c9d4e5f60718293a4b5c6d7e8",
    }).encode(),
    headers={
        "Authorization": f"Bearer {os.environ['NOXTICA_API_KEY']}",
        "Content-Type": "application/json",
    },
    method="POST",
)
with urllib.request.urlopen(request) as response:
    print(response.status, response.read().decode())

Read the identity details of a verification

GET /v1/kyc/verifications/{verificationId}/identity

Server-to-server. Returns the identity details Noxtica extracted from the document captured for one of your own verifications: the document type, number and expiry, the holder’s names, date of birth, sex and nationality, the machine-readable-zone check results, and a summary of the verification checks. It never returns images or biometric data.

Opt-in. The key needs the read:identity_details scope, and the route answers 403 identity_read_not_enabled until a tenant administrator enables identity details access for your organisation in the Backoffice (KYC settings). A key keeps its scopes until a tenant administrator changes them (Edit scopes on the key in the Backoffice, API Configuration): add read:identity_details to the key, or create a new key that carries it.

Which id. verificationId is the id returned by POST /v1/kyc/verify (a UUID) or the verificationId of a hosted session (hosted: followed by 64 lower-case hexadecimal characters); see KycVerificationId for the formats and the 128-character maximum. The id of a hosted session and the evidenceGroupId of a verification made from your own capture screens are accepted too and resolve to the same verification. An id that is malformed, unknown, or belongs to another organisation gets the same byte-identical 404 as an unknown path.

Outcome and review. verification.status is verified, unverified, failed, error or pending, and verification.source says who produced it: pipeline is the automated verdict, review means a reviewer’s approve decision was applied over it (the same outcome the hosted session status read reports). verification.review is null while no reviewer has acted, otherwise {state, decision, version, decidedAt} (see KycTenantReview). verification.level is the automated verification level and a review does not change it. A verification you created through the API has no session, so this read is where you see what a reviewer decided about it.

Review decision event. A reviewer’s approve or override on a verification is announced by the kyc.review.decided event; read this route again after it arrives to see the resulting verification and review. A decision recorded by your own back office through POST /v1/kyc/verifications/{verificationId}/review-decision is announced in the same way.

Selected and detected document type. document.selectedType is the document type the person chose in the hosted flow (passport, national_id, driving_license or residence_permit), or null for a verification created through the API or a session that recorded no choice. document.type is the document type the stored extraction is for. Compare the two: they differ when the extraction ran against another document type than the person presented.

Extraction: what was read, and why not. extraction answers, field by field, whether the identity fields a relying party needs were read from the document, so a null in person or document.number always comes with a reason instead of an empty value. It is present on every 200. Which of the five fields are required is yours to set (see “Required identity fields” below); a field that is not required is still reported, with its reason.

KeyValues
extraction.statusComputed over the required fields only. complete: every required field was read (also when none is required). partial: some were. unreadable: none was.
extraction.fields[]Always five entries, in this order: person.surname, person.givenNames, person.dateOfBirth, person.personalNumber, document.number. Each has status, reason, source and required; person.surname and person.givenNames also carry foldedValue and, when they apply, uncertainty, uncertainPositions, alternatives, script and latinForm.
fields[].statusread: the field has a value. It is released, or withheld by the masking rules below, so read does not mean the value is in this response. unreadable: no value, see reason. not_expected: this document does not carry the field and your configuration does not require it. person.personalNumber is expected on a Turkish (TR) national ID card or driving licence, where it is the T.C. Kimlik No.
fields[].requiredtrue when the field counts towards extraction.status and identity_fields_unreadable; false when it is reported but does not. An unreadable field with required: false keeps its reason and does not lower extraction.status.
fields[].reasonOnly with unreadable. label_not_found: the printed label or numbered marker was not found in the recognised text. value_not_found: the label was found, with no value beside or below it. value_invalid: a value was found that is not a valid name, date or number. checksum_invalid: the personal number was read but its check digits fail; the value is withheld and never returned. layout_unrecognised: the front of the document matched no known layout, or nothing was recognised. low_confidence: a date was read below the recognition confidence floor and is withheld. not_recorded: the record was written before reasons were kept.
fields[].sourceWhere the value came from: viz (the printed text of the document), mrz or barcode. null unless status is read.
fields[].uncertaintyOnly on person.surname and person.givenNames, and only when it applies: the key is absent otherwise. A list of advisory caveats on a read value. diacritic_uncertain: the value is the best reading, but a letter in it may carry a mark that the reading did not show, or the reverse: a Turkish Ş shown as S, a Polish Ł as L. It is set when the reader saw the marked and the plain letter as close candidates, when two readers of the printed text differed only in a mark, or when the value comes from the machine-readable zone or a barcode, which cannot print marks. mrz_transliteration_conflict: the printed name and the name in the machine-readable zone fit together only if a plain letter of the printed name carries a mark (printed MULLER, zone MUELLER); it can come alone or together with diacritic_uncertain. Both are advisory: status stays read, and they change neither extraction.status nor the verdict. What to do with each is in “Names, diacritics and alphabets” below.
fields[].uncertainPositionsOnly with diacritic_uncertain. The 0-based Unicode code point positions in the name (person.surname or person.givenNames, spaces counted), ascending, of the letters that may carry a different mark. For a name read from the printed text these are the letters the reader had doubt about; for a name taken from the machine-readable zone or a barcode they are every letter that has a marked form in the alphabet of the issuing country (for TR: C, G, I, O, S, U).
fields[].alternativesUp to four complete spellings of the name, most likely first, made only by swapping the flagged letters for their marked or plain forms. None equals the value and none repeats. With mrz_transliteration_conflict the list includes the spelling the machine-readable zone points to. Absent when there is no reading-based doubt, in particular for a name taken from the machine-readable zone or a barcode: that source cannot show a mark, so there is nothing to rank. The value is never replaced by an alternative.
fields[].foldedValueOn every read person.surname and person.givenNames, with or without an uncertainty: the value with its marks removed as ICAO Doc 9303 Part 3 section 6.A lists them (one base letter such as Ş to S, İ to I, Ä to A, Ñ to N; the expansions ß to SS, Æ to AE, Œ to OE, Ø to OE, Þ to TH, IJ to IJ). The case of the value is kept, and it equals the value when no letter folds. It is a key for tolerant comparison, not a spelling of the name to store.
fields[].scriptThe writing system the name was read in, as an ISO 15924 code: Latn, Cyrl, Grek, Arab, Hebr, Geor, Armn, Deva, Thai, Hani, Jpan or Kore. Absent when it was not determined.
fields[].latinForm{value, source}: a Latin-letter form of the name when one is available. source is viz (printed on the document) or mrz (the name in the machine-readable zone, which uses the ICAO transliteration). Absent when there is none.
extraction.reasonCodesidentity_fields_unreadable when a required field is unreadable. document_type_mismatch_selection when documentType.contradictsSelection is true. issuing_country_mismatch_selection when issuingCountry.contradictsSelection is true. diacritic_uncertain or mrz_transliteration_conflict when person.surname or person.givenNames carries that uncertainty. An empty array when none applies. The first three codes are also added to the reasonCodes of the verification and of the session; the two name codes appear only here. All are informational and change neither the verdict nor the retake prompts.
extraction.documentTypeselected: the type the person chose (the same value as document.selectedType). classified: the type the classifier named, only when it is confident (0.72 or higher), otherwise null. contradictsSelection: true when the classified type is a different identity document type than the selection, false when they agree, null when they cannot be compared.
extraction.issuingCountryselected: the country the person chose in the hosted flow (ISO 3166-1 alpha-2), or null. detected: the country the document shows, or null when nothing readable named one. source: mrz (the issuing state of the machine-readable zone, used only when none of its check digits failed) or viz (the printed text), null exactly when detected is null. contradictsSelection: true when both are known and differ, false when they are equal, null when they cannot be compared; an unknown is never reported as a contradiction.

Required identity fields. By default person.surname, person.givenNames, person.dateOfBirth and document.number are required, and so is person.personalNumber on a Turkish national ID card or driving licence. A document counts as Turkish when it shows Türkiye (document.issuingCountry, see extraction.issuingCountry), or, when no country could be read from it, when the person selected Türkiye. For those two Turkish document types one rule relaxes this: when the T.C. Kimlik No was read and passes its check digits, document.number is not required, so an unreadable document number is still reported (with its reason and required: false) but no longer raises identity_fields_unreadable. You can set the list per document type with requiredIdentityFields on documents in your hosted configuration (organisation default or one domain; see GET /v1/kyc/config). A non-empty list is exactly the set that is required, replaces the Turkish rule, and may name personalNumber for any country; an empty list requires nothing. The list that applied is recorded with the verification. A verification made before this was recorded is read with the default rules at read time, while its stored session reasonCodes stay as they were recorded.

identity_fields_unreadable is informational. It does not decide whether a session is reviewed: every hosted verification currently ends unverified with provider_manual_review until a reviewer decides, whatever the extraction says.

Selected and detected issuing country. extraction.issuingCountry compares the country the person selected with the country the document shows. Only Türkiye can be recognised from printed text (the Turkish country heading, or a Turkish layout with a T.C. Kimlik No that passes its check digits); for any other country detected is set only when the machine-readable zone names it. The words of a document alone, such as “SÜRÜCÜ BELGESİ”, are not evidence of a country. A dependent territory selected against its sovereign state (for example GF and FR) is not a contradiction. document.issuingCountry is the country the document shows when one was read, otherwise the country the person selected, otherwise null. The hosted page starts with the first country of the list selected, so narrow documents[].issuingCountries in your hosted configuration (or hostedOptions.allowedIssuingCountries on the invitation) to the countries you accept; an example of a contradiction is {selected: AD, detected: TR, source: viz, contradictsSelection: true}.

A personal number is returned (when fields names personalNumber) only if it passes the official T.C. Kimlik No check-digit rule; a number that fails it is null and its field reads unreadable with checksum_invalid. Dates are validated against the calendar before they are returned. A value that could not be validated is reported unreadable, never guessed.

Names, diacritics and alphabets. person.surname and person.givenNames hold the name as the document prints it; on a document where the printed text and the machine-readable zone name the same person, the printed spelling is used. A text reader can show a letter with a mark as its plain form (a Turkish Ş as S, a German Ü as U) or the other way round, a letter can occasionally be missing, and the machine-readable zone cannot print marks at all. The name fields therefore carry advisory data next to the value and never instead of it: the value is not changed to a letter the printed read did not produce, and every other spelling is offered as an alternative for you to check.

You seeIt meansWhat to do
No uncertaintyThe reader found no doubt on this name. This is not a confirmation of each letter.Compare tolerantly (below) and keep a review path for a mismatch.
diacritic_uncertain with alternativesThe name was read from the printed text and the letters in uncertainPositions may carry a different mark.Check the value against the registry first, then each entry of alternatives in order, then foldedValue against registries that compare folded names only. If none matches, ask for a new capture or send the case to manual review.
diacritic_uncertain without alternativesThe name comes from the machine-readable zone or a barcode, which cannot show marks. uncertainPositions lists the letters that could have one; nothing says which.Do not make up spellings. Compare foldedValue with registries that compare folded names, otherwise ask the person to confirm the spelling, ask for a capture of the printed side, or send the case to manual review.
foldedValueThe value with marks removed. Present on every read name.Compare the foldedValue of both sides when you want a tolerant match (below). Do not store it as the name.
mrz_transliteration_conflictThe printed name and the machine-readable zone do not fit together unless a plain letter of the printed name carries a mark. alternatives holds that spelling.Check the spelling from alternatives against the registry before foldedValue; if it matches, have the person or a reviewer confirm it. If nothing matches, ask for a new capture or send the case to manual review.

What the machine-readable zone can and cannot show. The zone holds the letters A to Z, digits and < only. ICAO Doc 9303 Part 3 section 6.A lists how an issuing state writes other letters there: Ä as AE or A, Ö as OE or O, Ü as UE, UXX or U, Å as AA or A, Ø as OE, Æ as AE, ß as SS, Ñ as N or NXX, Þ as TH. Turkish letters have no such expansion: Ş, Ç, Ğ, İ, Ö and Ü always become S, C, G, I, O and U, so a Turkish zone cannot confirm which letter was printed (SAHIN fits ŞAHİN, SAHİN and SAHIN). A zone that uses an expansion can: printed MÜLLER with zone MUELLER agrees only when the printed letter is Ü, so that letter is dropped from uncertainPositions. Printed MULLER with zone MUELLER is a conflict: the zone spelling needs a mark that the printed reading lacks, and mrz_transliteration_conflict is set. An ordinary difference between the two names is not a conflict and is not flagged here.

What a missing flag means, and how often a flag is right. A name without uncertainty means that the reader found no doubt on it. It is not a confirmation of each letter, and the name can still contain a wrong one: measured on synthetic test documents, many wrong letters are read with full confidence and carry no flag. For the printed name on Turkish national ID card and driving licence fronts, the flag appears on roughly one in ten names. About one in three flagged names is actually wrong (the others are already right), and for most wrong flagged names the true spelling is among alternatives. So treat a flag as a reason to try the alternatives, not as a verdict, and keep the tolerant comparison and the review path for a mismatch whether the flag is present or not.

Check a name against a registry (Turkish national registry example).

  1. Always send the value first, upper-cased with Turkish rules (i to İ, ı to I); a plain upper-case turns i into I and breaks the match.
  2. On a mismatch while diacritic_uncertain is present, send each entry of alternatives in order, upper-cased the same way.
  3. If nothing matches, ask the person for a new capture or send the case to manual review. A registry that compares folded names can be sent foldedValue instead of steps 1 and 2.
  4. Do not write an alternative or the folded value into your records as the legal name without a confirmation by the person, a reviewer or the registry answer.

Tolerant matching. To compare a name with a name you hold without a registry, fold both sides the same way and compare the folded strings: use foldedValue for the name read from the document and apply the same ICAO folding (and one case convention, for example Turkish upper case) to your own value. A match of the folded strings says the two names agree up to marks; it does not say which marks are right, so keep the review path for names that matter.

Scope. The fields are optional and additive: a verification recorded before this release can lack alternatives, uncertainPositions, script, latinForm and mrz_transliteration_conflict, and a finished verification is not read again, so treat each of them as optional. Doubts about the printed read, and so alternatives, exist for the Turkish national ID card and driving licence layouts. Flags for a name taken from the machine-readable zone or a barcode exist for the countries whose alphabet Noxtica lists, for example TR, DE, AT, LI and PL; foldedValue is on every read name. For a name in a non-Latin script, script says which one and latinForm gives a Latin form when there is one; the value stays as read.

Turkish driving licence number. The document number of a Turkish driving licence is the value of field 5. on the front: six digits, with leading zeros kept; it has no check digit that could be verified. Spaces, dots and stray marks inside the number (123 456, 12|3456) are dropped. A letter read in place of a look-alike digit (for example O for 0 or I for 1) is corrected only when the value has six characters and at least five of them are digits; a value of any other shape (six to twelve letters and digits with at least one digit, such as the older A123456 form) is accepted exactly as read and is never repaired. A number that is not printed, or that cannot be recognised, stays unreadable (value_not_found or value_invalid) and is never guessed. Unless you list documentNumber in requiredIdentityFields, an unreadable licence number does not raise identity_fields_unreadable while the T.C. Kimlik No of the licence was read.

Check results. checks.documentAuthenticity, checks.faceMatch, checks.liveness and checks.selfie are verdicts (pass, review, fail, skipped, pending, or null when no automated result was recorded for that check). They are evidence for review and for your own risk rules, not a decision. What each check measures, the reason codes behind it and the publishable thresholds are described under “Check results” on GET /v1/kyc/sessions/{sessionId}. Why liveness and selfie read at best review is explained on Liveness & PAD policy.

Masked by default. Names, date of birth, expiry date, nationality, sex, issuing country, ID type and document type are returned in full. The document number is returned masked (its last characters only), and the personal number and the raw machine-readable-zone lines are left out. Name them in fields (comma separated) to receive them: documentNumber, personalNumber, mrzLines. Every masked or missing item is listed in redactions with the fields value that returns it; data Noxtica does not extract (the address today) is listed with the reason not_extracted. A person.surname, person.givenNames or person.dateOfBirth that is null is listed with the reason not_available. An unknown fields value is a 400 invalid_fields that lists the accepted values.

Availability. Details exist once a verification has finished and its fields were extracted. A verification without stored details answers 409 identity_not_available with reason not_completed (not finished yet) or not_extracted (nothing was extracted or stored: evidence storage is not enabled for your organisation, or the verification predates extraction). A verification you deleted answers 410 verification_deleted.

Audit and handling. Each successful read is recorded for your organisation with the key that made it and the names of the fields returned, never their values. Every response of this route carries Cache-Control: no-store, private: do not cache or log the body. Identity details are never sent in webhooks.

Rate limit. A per-key limit applies to the identity details routes together; a limited request answers 429 with Retry-After.

Authentication: Bearer token (ApiKeyAuth), scope read:identity_details.

Parameters

NameInTypeRequiredDescription
verificationIdpathstringYesThe verificationId of the verification. Pattern ^[A-Za-z0-9:_-]{1,128}$.
fieldsqueryarray of stringNoComma-separated list of withheld values to include: documentNumber (the full document number), personalNumber, mrzLines (the raw machine-readable-zone lines). Omit it for the masked default.

Responses

StatusMeaningError codesContent
200The identity details of the verification.application/json KycVerificationIdentity
400invalid_fields or invalid_request — fields names a value this route does not accept (allowed lists the accepted values), or the request could not be parsed. Nothing was read.invalid_fields, invalid_requestapplication/json IdentityInvalidFieldsError
401The 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_keyapplication/json KycSessionError
403identity_read_not_enabled — a tenant administrator has not enabled identity details access for your organisation, so nothing was read; or the key lacks the read:identity_details scope (error is API key does not have read:identity_details scope).identity_read_not_enabledapplication/json IdentityForbiddenError
404The byte-identical “V1 endpoint not found” response, returned for a malformed id, an id that does not exist, an id that belongs to another organisation, or a route that is not available for your organisation — indistinguishable from an unknown path by design.V1 endpoint not foundapplication/json
409identity_not_available — the verification exists but has no stored identity details. reason is not_completed (it has not finished) or not_extracted (nothing was extracted or stored for it).identity_not_availableapplication/json IdentityNotAvailableError
410verification_deleted — you deleted this verification; its details no longer exist.verification_deletedapplication/json ErrorEnvelope
429Rate limit exceeded. Wait Retry-After seconds (also retryAfter in the body) before retrying; the response carries no other rate-limit headers.Rate limit exceededapplication/json
500API-key authentication is not configured on this deployment (server_misconfigured). Retrying does not help; contact support with the requestId.server_misconfiguredapplication/json KycSessionError
503identity_audit_unavailable or identity_unavailable — retryable. The access could not be recorded, so nothing was released; or the stored details could not be read right now.identity_audit_unavailable, identity_unavailableapplication/json ErrorEnvelope
503Noxtica could not read the key’s record. This is a temporary fault on Noxtica’s side and says nothing about your key. Retry after the Retry-After delay; do not treat it as a revoked key.api_key_auth_unavailableapplication/json

Response headers

StatusHeaderTypeDescription
200Cache-ControlstringAlways no-store, private. Never cache or store a response of an identity details route or of the review decision route.
400Cache-ControlstringAlways no-store, private. Never cache or store a response of an identity details route or of the review decision route.
403Cache-ControlstringAlways no-store, private. Never cache or store a response of an identity details route or of the review decision route.
404Cache-ControlstringAlways no-store, private. Never cache or store a response of an identity details route or of the review decision route.
409Cache-ControlstringAlways no-store, private. Never cache or store a response of an identity details route or of the review decision route.
410Cache-ControlstringAlways no-store, private. Never cache or store a response of an identity details route or of the review decision route.
429Retry-AfterintegerSeconds until the bucket allows another request.
503Cache-ControlstringAlways no-store, private. Never cache or store a response of an identity details route or of the review decision route.
503Retry-AfterintegerSeconds to wait before retrying.

Response fields (200, application/json)

FieldTypeAlways returnedDescription
okbooleanYesAllowed values: true.
verificationIdstringYesThe identifier of one verification. Treat it as an opaque string and store it in a column that holds at least 128 characters. The API contract fixes two limits that hold for every id: it is never longer than 128 characters, and it only contains the characters A-Z, a-z, 0-9, :, _ and -. Today an id has one of two shapes. A verification created through POST /v1/kyc/verify (including one made from an evidenceGroupId, and each item of a batch) is a lower-case UUID: 36 characters, for example 3fa85f64-5717-4562-b3fc-2c963f66afa6. The verification a hosted session runs when the customer submits is hosted: followed by 64 lower-case hexadecimal characters: 71 characters in all. Do not parse an id or rely on its prefix; a new shape may appear, but always within the two limits above. At least 1 character, at most 128 characters. Pattern ^[A-Za-z0-9:_-]{1,128}$.
sourceEvidenceGroupIdstring, nullableYesThe evidenceGroupId the verification was made from, or null when it did not come from your own capture screens.
extractedAtstring (date-time)YesWhen the details were extracted and stored.
verificationobjectYes
verification.statusstringYesThe outcome of a verification. verified, unverified, failed and error are the verdict words of the automated verification; pending means the verification has no verdict yet. A reviewer’s approve decision turns the reported status into verified (see KycVerificationStatusSource); no other decision changes it. Allowed values: verified, unverified, failed, error, pending.
verification.sourcestringYesWho produced the reported status. pipeline — the automated verdict exactly as it was recorded. review — a reviewer’s approve decision was applied over it. An override decision never produces review. Allowed values: pipeline, review.
verification.levelstring, nullableYesThe automated verification level, or null when none was assigned. A review does not change it. Allowed values: verified, partial, unverified, inconclusive, null.
verification.reviewone of KycTenantReview or any, nullableYesThe reviewer’s work on the verification; null while no reviewer has acted.
verification.review{KycTenantReview}object (KycTenantReview)NoWhat a reviewer did with a verification. No reviewer identity, no reviewer note and no reviewer reason codes are returned.
verification.review{KycTenantReview}.statestringYesState of a reviewer’s work on a verification. in_review — a reviewer escalated the case without deciding (no decision yet). approved — a reviewer approved it. overridden — a reviewer replaced the automated verdict. open and closed belong to the vocabulary but no Backoffice review action writes them today; handle an unknown or unexpected value as “not decided”. Allowed values: open, in_review, approved, overridden, closed.
verification.review{KycTenantReview}.decisionone of KycReviewDecision or any, nullableYesThe reviewer’s decision, or null while the state is in_review.
verification.review{KycTenantReview}.versionintegerYesNumber of review writes for this verification, starting at 1 and increasing by one with every write (an escalation counts). Minimum 1.
verification.review{KycTenantReview}.decidedAtinteger (int64), nullableYesUnix ms of the last review write that carried a decision, or null while there is none.
documentobjectYes
document.typestringYesThe document type the stored extraction is for. Allowed values: national_id, driving_license, passport, residence_permit, unknown.
document.selectedTypestring, nullableYesThe document type the person chose in the hosted flow, or null for a verification created through the API or a session that recorded no choice. Differs from type when the extraction ran against another document type than the person presented. Allowed values: national_id, passport, driving_license, residence_permit, null.
document.sidestringYesAllowed values: front, back, data_page, unknown.
document.issuingCountrystring, nullableYesThe country the document shows when one was read (extraction.issuingCountry), otherwise the country the person selected in the hosted flow, otherwise null.
document.numberstring, nullableYesThe document number, masked (its last characters only) unless fields names documentNumber.
document.expiryDatestring (date), nullableYes
document.idTypestring, nullableYes
personobjectYes
person.fullNamestring, nullableYes
person.surnamestring, nullableYes
person.givenNamesstring, nullableYes
person.dateOfBirthstring (date), nullableYes
person.sexstring, nullableYesAs read from the document, usually M, F or X.
person.nationalitystring, nullableYes
person.personalNumberstring, nullableYesLeft out (null) unless fields names personalNumber.
addressobject, nullableYesAlways null today: Noxtica does not extract an address from identity documents. The key is present so a later release can fill it without changing the shape.
mrzobject, nullableYesThe machine-readable zone of the document, or null when none was found.
mrz.formatstring, nullableYesAllowed values: TD1, TD2, TD3, null.
mrz.fieldChecksarray of objectYesThe check-digit result of each MRZ field. Structural results only.
mrz.fieldChecks[]objectNo
mrz.fieldChecks[].fieldstringYes
mrz.fieldChecks[].checkDigitValidbooleanYes
mrz.linesarray of stringNoThe raw MRZ lines. Present only when fields names mrzLines.
checksobject (KycIdentityChecks)YesA summary of the verification checks as verdicts. No scores or provider details are included.
checks.documentAuthenticitystring, nullableYesOne check family’s verdict, or null when the verification has no such check. Allowed values: pass, review, fail, skipped, pending, null.
checks.faceMatchstring, nullableYesOne check family’s verdict, or null when the verification has no such check. Allowed values: pass, review, fail, skipped, pending, null.
checks.livenessstring, nullableYesOne check family’s verdict, or null when the verification has no such check. Allowed values: pass, review, fail, skipped, pending, null.
checks.selfiestring, nullableYesOne check family’s verdict, or null when the verification has no such check. Allowed values: pass, review, fail, skipped, pending, null.
checks.crossValidationobject, nullableYesWhether the values read from the different parts of the document agree. The agreement is returned, not the values compared; null when no cross-check ran.
checks.crossValidation.overallstringYesAllowed values: match, mismatch, unavailable.
checks.crossValidation.fieldsarray of objectYes
checks.crossValidation.fields[]objectNo
checks.crossValidation.fields[].fieldstringYesThe name of the compared value.
checks.crossValidation.fields[].agreementstringYesAllowed values: match, mismatch, unavailable.
extractionobject (KycIdentityExtraction)YesWhether the identity fields a relying party needs were read from the document, field by field, with the reason for every field that was not.
extraction.statusstringYescomplete — every required field was read (also when none is required). partial — some were. unreadable — none was. Allowed values: complete, partial, unreadable.
extraction.reasonCodesarray of stringYesidentity_fields_unreadable when a required field is unreadable; document_type_mismatch_selection when documentType.contradictsSelection is true; issuing_country_mismatch_selection when issuingCountry.contradictsSelection is true; diacritic_uncertain or mrz_transliteration_conflict when person.surname or person.givenNames carries that uncertainty (informational; unlike the other three, not added to the reasonCodes of the verification and of the session).
extraction.fieldsarray of KycIdentityFieldReadYesAlways five entries, in this order: person.surname, person.givenNames, person.dateOfBirth, person.personalNumber, document.number. At least 5 items, at most 5 items.
extraction.fields[]object (KycIdentityFieldRead)NoThe outcome of reading one identity field from the document: whether it was read and, when it was not, why.
extraction.fields[].fieldstringYesThe path of the field in this response. Allowed values: person.surname, person.givenNames, person.dateOfBirth, person.personalNumber, document.number.
extraction.fields[].statusstringYesread — the field has a value (released, or withheld by the masking rules; read does not mean the value is in the response). unreadable — no value, see reason. not_expected — this document does not carry the field and your configuration does not require it. Allowed values: read, unreadable, not_expected.
extraction.fields[].reasonstring, nullableYesOnly with unreadable, otherwise null. label_not_found, value_not_found, value_invalid, checksum_invalid (the personal number’s check digits fail; the value is withheld), layout_unrecognised, low_confidence (a date read below the recognition confidence floor is withheld) or not_recorded (a record written before reasons were kept). Allowed values: label_not_found, value_not_found, value_invalid, checksum_invalid, layout_unrecognised, low_confidence, not_recorded, null.
extraction.fields[].sourcestring, nullableYesWhere the value came from, or null unless status is read. Allowed values: viz, mrz, barcode, null.
extraction.fields[].uncertaintyarray of stringNoOnly on person.surname and person.givenNames, and only when it applies (the key is absent otherwise). Advisory caveats on a read value. diacritic_uncertain — the value is the best reading, but a letter in it may carry a mark that the reading did not show, or the reverse (a Turkish Ş shown as S): the reader saw the marked and the plain letter as close candidates, two readers of the printed text differed only in a mark, or the value comes from the machine-readable zone or a barcode, which cannot print marks. mrz_transliteration_conflict — the printed name and the machine-readable zone fit together only if a plain letter of the printed name carries a mark (printed MULLER, zone MUELLER); it can come with or without diacritic_uncertain. status stays read; check alternatives and foldedValue as described under “Names, diacritics and alphabets” on GET /v1/kyc/verifications/{verificationId}/identity. A field without this key can still contain a wrong letter.
extraction.fields[].uncertainPositionsarray of integerNoOnly with diacritic_uncertain. 0-based Unicode code point positions in the name (person.surname or person.givenNames, spaces counted), ascending, of the letters that may carry a different mark. For a name read from the printed text: the letters the reader had doubt about. For a name taken from the machine-readable zone or a barcode: every letter that has a marked form in the alphabet of the issuing country.
extraction.fields[].alternativesarray of stringNoUp to four complete spellings of the name, most likely first, made only by swapping the flagged letters for their marked or plain forms. None equals the value and none repeats. With mrz_transliteration_conflict the list includes the spelling the machine-readable zone points to. Absent when there is no reading-based doubt, in particular for a name taken from the machine-readable zone or a barcode. The value is never replaced by an alternative; do not store one as the legal name without a confirmation. At most 4 items.
extraction.fields[].foldedValuestringNoOn every read person.surname and person.givenNames, with or without an uncertainty: the value with its marks removed as ICAO Doc 9303 Part 3 section 6.A lists them (one base letter such as Ş to S; the expansions ß to SS, Æ to AE, Œ to OE, Ø to OE, Þ to TH, IJ to IJ). The case of the value is kept. Use it as a key for tolerant comparison, not as the spelling of the name.
extraction.fields[].scriptstringNoThe writing system the name was read in, as an ISO 15924 code. Absent when it was not determined. Allowed values: Latn, Cyrl, Grek, Arab, Hebr, Geor, Armn, Deva, Thai, Hani, Jpan, Kore.
extraction.fields[].latinFormobjectNoA Latin-letter form of the name, when one is available.
extraction.fields[].latinForm.valuestringYes
extraction.fields[].latinForm.sourcestringYesviz — printed on the document. mrz — the name in the machine-readable zone, which uses the ICAO transliteration. Allowed values: viz, mrz.
extraction.fields[].requiredbooleanYestrue when the field counts towards extraction.status and the reason code identity_fields_unreadable; false when it is reported but does not. An unreadable field with required: false keeps its reason.
extraction.documentTypeobjectYes
extraction.documentType.selectedstring, nullableYesThe document type the person chose in the hosted flow (the same value as document.selectedType), or null. Allowed values: national_id, passport, driving_license, residence_permit, null.
extraction.documentType.classifiedstring, nullableYesThe document type the classifier named, only when it is confident (0.72 or higher); otherwise null. Allowed values: national_id, driving_license, passport, residence_permit, null.
extraction.documentType.contradictsSelectionboolean, nullableYestrue when the classified type is a different identity document type than the selection, false when they agree, null when they cannot be compared.
extraction.issuingCountryobjectYesWhether the country the person selected agrees with the country the document shows.
extraction.issuingCountry.selectedstring, nullableYesThe issuing country the person chose in the hosted flow, as an ISO 3166-1 alpha-2 code (a CountryCode value), or null for a verification created through the API, a session that recorded no choice, or a record written before the selection was kept. Pattern ^[A-Z]{2}$.
extraction.issuingCountry.detectedstring, nullableYesThe country the document shows, as an ISO 3166-1 alpha-2 code, or null when nothing readable named one. Pattern ^[A-Z]{2}$.
extraction.issuingCountry.sourcestring, nullableYesWhere detected came from: mrz (the issuing state of the machine-readable zone, used only when none of its check digits failed) or viz (the printed text of the front). null exactly when detected is null. Allowed values: mrz, viz, null.
extraction.issuingCountry.contradictsSelectionboolean, nullableYestrue when both are known and differ, false when they are equal, null when they cannot be compared (no selection, nothing read, or an older record). An unknown is never reported as a contradiction; a dependent territory against its sovereign state is not one.
redactionsarray of IdentityRedactionYes
redactions[]object (IdentityRedaction)NoOne item the response masked, left out or could not provide. It tells “not there” apart from “left out on purpose”.
redactions[].fieldstringYesPath of the item in the response, for example document.number, person.surname, person.givenNames, person.dateOfBirth, person.personalNumber, mrz, mrz.lines, address or documents[].extractions.
redactions[].reasonstringYesmasked_by_default — the value is returned masked (its last characters only). omitted_by_default — the value is left out. not_extracted — Noxtica does not extract this item, or did not for this record. not_available — the item is not available for this record. Allowed values: masked_by_default, omitted_by_default, not_extracted, not_available.
redactions[].includestringNoThe fields value that returns the full item. Absent when no fields value does.
requestIdstringYes

Response example (200)

{
  "ok": true,
  "verificationId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "sourceEvidenceGroupId": null,
  "extractedAt": "2026-10-28T14:22:11.000Z",
  "verification": {
    "status": "verified",
    "source": "pipeline",
    "level": "verified",
    "review": null
  },
  "document": {
    "type": "national_id",
    "selectedType": null,
    "side": "front",
    "issuingCountry": "GB",
    "number": "••••4821",
    "expiryDate": "2031-05-17",
    "idType": "national_id"
  },
  "person": {
    "fullName": "ADA EXAMPLE",
    "surname": "EXAMPLE",
    "givenNames": "ADA",
    "dateOfBirth": "1990-01-31",
    "sex": "F",
    "nationality": "GB",
    "personalNumber": null
  },
  "address": null,
  "mrz": {
    "format": "TD1",
    "fieldChecks": [
      {
        "field": "document_number",
        "checkDigitValid": true
      },
      {
        "field": "birth_date",
        "checkDigitValid": true
      },
      {
        "field": "expiry_date",
        "checkDigitValid": true
      }
    ]
  },
  "checks": {
    "documentAuthenticity": "pass",
    "faceMatch": "pass",
    "liveness": "review",
    "selfie": "review",
    "crossValidation": {
      "overall": "match",
      "fields": [
        {
          "field": "dateOfBirth",
          "agreement": "match"
        },
        {
          "field": "documentNumber",
          "agreement": "match"
        }
      ]
    }
  },
  "extraction": {
    "status": "complete",
    "reasonCodes": [],
    "fields": [
      {
        "field": "person.surname",
        "status": "read",
        "reason": null,
        "source": "mrz",
        "required": true,
        "foldedValue": "EXAMPLE",
        "script": "Latn"
      },
      {
        "field": "person.givenNames",
        "status": "read",
        "reason": null,
        "source": "mrz",
        "required": true,
        "foldedValue": "ADA",
        "script": "Latn"
      },
      {
        "field": "person.dateOfBirth",
        "status": "read",
        "reason": null,
        "source": "mrz",
        "required": true
      },
      {
        "field": "person.personalNumber",
        "status": "not_expected",
        "reason": null,
        "source": null,
        "required": false
      },
      {
        "field": "document.number",
        "status": "read",
        "reason": null,
        "source": "mrz",
        "required": true
      }
    ],
    "documentType": {
      "selected": null,
      "classified": "national_id",
      "contradictsSelection": null
    },
    "issuingCountry": {
      "selected": null,
      "detected": "GB",
      "source": "mrz",
      "contradictsSelection": null
    }
  },
  "redactions": [
    {
      "field": "document.number",
      "reason": "masked_by_default",
      "include": "documentNumber"
    },
    {
      "field": "person.personalNumber",
      "reason": "omitted_by_default",
      "include": "personalNumber"
    },
    {
      "field": "mrz.lines",
      "reason": "omitted_by_default",
      "include": "mrzLines"
    },
    {
      "field": "address",
      "reason": "not_extracted"
    }
  ],
  "requestId": "req-test"
}

Example request

curl

curl "https://collect.noxtica.com/v1/kyc/verifications/example-verificationId/identity" \
  -H "Authorization: Bearer $NOXTICA_API_KEY"

Node.js (global fetch, Node 18 or later)

const response = await fetch('https://collect.noxtica.com/v1/kyc/verifications/example-verificationId/identity', {
  method: 'GET',
  headers: {
    Authorization: `Bearer ${process.env.NOXTICA_API_KEY}`,
  },
});
console.log(response.status, await response.text());

Python (standard library)

import os
import urllib.request

request = urllib.request.Request(
    "https://collect.noxtica.com/v1/kyc/verifications/example-verificationId/identity",
    headers={
        "Authorization": f"Bearer {os.environ['NOXTICA_API_KEY']}",
    },
    method="GET",
)
with urllib.request.urlopen(request) as response:
    print(response.status, response.read().decode())

Record a review decision for a verification

POST /v1/kyc/verifications/{verificationId}/review-decision

Server-to-server. Lets your own back office record the decision on a verification that is waiting for review: the same decision a Backoffice reviewer records in the review panel, written by the same decision service, with the same effect on the status Noxtica reports, the same kyc.review.decided event and the same audit trail. Nothing else changes: the automated verdict of the verification is never rewritten, and a decision is never made for you.

Opt-in at two levels. The route is off for every organisation until Noxtica turns the review decision API on for the platform and a tenant administrator of your organisation enables it in the Backoffice (KYC → Invitations → Settings tab, card “Review decision API”, confirmed with step-up multi-factor authentication; it is not on the Settings page of your own account). While the platform has it off, a request answers the byte-identical 404 of an unknown path; while your organisation has not enabled it, 403 review_decision_not_enabled. The key needs the write:kyc_review_decision scope: no other scope reaches this route, and write:identity does not imply it. A key keeps its scopes until a tenant administrator changes them (Edit scopes on the key in the Backoffice, API Configuration): add the scope to the key, or create a new key that carries it. The tenant administrator can switch the route off again at any time; the decisions already written stay.

Which id. verificationId is the id returned by POST /v1/kyc/verify (a UUID) or the verificationId of a hosted session (hosted: followed by 64 lower-case hexadecimal characters); see KycVerificationId for the formats and the 128-character maximum. An id that is malformed, unknown, or belongs to another organisation gets the same byte-identical 404 as an unknown path, so the route cannot be used to find out whether a verification exists.

Decisions. There are two, the two affirmative decisions of the review panel, and no other:

decisionMeaningReported status afterwardsAllowed reasonCode
approveYour reviewer affirmed the verification.verified, with verification.source review (see GET /v1/kyc/verifications/{verificationId}/identity and GET /v1/kyc/sessions/{sessionId}). approve is the only decision that reports a verification as verified.identity_confirmed, registry_crosscheck_passed, name_variant_accepted, manual_document_check_passed, other
overrideYour reviewer replaced the automated verdict. It is not an approval.The automated verdict is reported as it is; override never makes the status verified.identity_mismatch, registry_crosscheck_failed, document_invalid, suspected_fraud, liveness_not_accepted, other

There is no reject decision: the review panel has none, and override is the action that replaces the automated verdict. Escalating a case is a state of the review, not a decision, and is not offered here. A reasonCode that does not belong to the decision of the same request is a 400 invalid_reason_code. other means none of the listed reasons; keep your own reason text in your own system, keyed by reviewer and the delivery id of the kyc.review.decided event.

Who decided. reviewer is your own identifier of the person who took the decision (for example the id of the reviewer in your back office): 1 to 128 visible characters, no surrounding spaces and no control characters. Noxtica stores it with the decision, next to the API key that sent the request, and returns it in this response. Use an internal id, not a name or an email address. The optional note (at most 2000 characters) is stored with the decision. Neither the reviewer, the key nor the note is ever sent in a webhook or returned by a read.

Pending reviews only. A decision is accepted while the review of the verification is still pending: no reviewer has acted on it, or a reviewer escalated it without deciding. A verification that already holds a decision, from the panel or from this route, answers 409 review_not_pending and nothing is written; use the Backoffice to change a decision that was already recorded. As for the panel, the verification must have finished and have an active legal basis recorded for it (409 legal_basis_missing otherwise). A review that was closed answers 409 review_closed.

What a decision does. The decision is written together with its audit row (action kyc_review_decision_recorded, source tenant_api) in one transaction. A hosted session that was review_pending is completed by it, exactly as a panel decision completes it, and the kyc.review.decided event is sent to the endpoints that subscribe to it with data.actor.type api and the closed data.reviewReasonCode. Hosted verifications keep ending unverified with the reason provider_manual_review until a decision is recorded: the automated outcome never approves, and a decision recorded here is the only thing that changes what is reported. Read verification and review on GET /v1/kyc/verifications/{verificationId}/identity (scope read:identity_details) or GET /v1/kyc/sessions/{sessionId} afterwards. The response of this route already carries the resulting state, version and decidedAt.

Idempotency. Idempotency-Key is required: 8 to 128 characters (a UUID is fine), a new value for every new decision. Send the same key again with the same body (a retry after a timeout) and you get the first answer back: 200 with the header Idempotent-Replayed: true and replayed: true. Nothing is written a second time, so no second event is sent. The same key with a different verification or a different body answers 422 idempotency_key_reuse. A key is scoped to your organisation. A 409 conflict means another decision for the same verification was written at the same moment and yours was not: read the verification, and send a new request only if it is still pending.

Audit and handling. Every decision is recorded for your organisation with the key that made it and the reviewer you sent. Every response of this route carries Cache-Control: no-store, private.

Rate limit. 30 requests per 60 seconds for each key; a limited request answers 429 with Retry-After. The route is available only while the KYC module is enabled for your organisation; otherwise it answers the GatedNotFound 404.

Authentication: Bearer token (ApiKeyAuth), scope write:kyc_review_decision.

Parameters

NameInTypeRequiredDescription
verificationIdpathstringYesThe verificationId of the verification to decide. Pattern ^[A-Za-z0-9:_-]{1,128}$.
Idempotency-KeyheaderstringYesA 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.

FieldTypeRequiredDescription
decisionstringYesA reviewer’s decision. approve — the reviewer affirmed the verification; the reported status becomes verified. override — the reviewer replaced the automated verdict; it is reported as it is but is not an approval and never makes the status verified. There is no reject decision today. An escalation is not a decision: it leaves the decision null. Allowed values: approve, override.
reasonCodestringYesThe closed reason code of a decision made through the API, never free text. Each decision accepts its own subset. approve: identity_confirmed, registry_crosscheck_passed, name_variant_accepted, manual_document_check_passed, other. override: identity_mismatch, registry_crosscheck_failed, document_invalid, suspected_fraud, liveness_not_accepted, other. other means none of the listed reasons. A code outside the subset of the decision is refused with invalid_reason_code. Allowed values: identity_confirmed, registry_crosscheck_passed, name_variant_accepted, manual_document_check_passed, identity_mismatch, registry_crosscheck_failed, document_invalid, suspected_fraud, liveness_not_accepted, other.
reviewerstringYesYour own identifier of the person who took the decision: 1 to 128 visible characters, no surrounding spaces, no control characters. Stored with the decision and returned in the response; never sent in a webhook. Use an internal id rather than a name or an email address. At least 1 character, at most 128 characters.
notestring, nullableNoOptional free text, at most 2000 characters. Stored with the decision; never sent in a webhook and never returned. At most 2000 characters.

Responses

StatusMeaningError codesContent
200The decision is recorded and now stands (replayed: false), or the same request was already recorded and its first answer is returned (replayed: true, header Idempotent-Replayed: true).application/json KycReviewDecisionResult
400The request is not valid; nothing was written. error is idempotency_key_invalid (the Idempotency-Key header is missing or not 8 to 128 characters), invalid_body (not a JSON object, or it has a member this route does not accept), invalid_decision, invalid_reason_code (not in the list of the decision), invalid_reviewer or invalid_note. message says what to fix and never repeats your values.idempotency_key_invalid, invalid_reason_codeapplication/json KycReviewDecisionError
401The 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_keyapplication/json KycSessionError
403review_decision_not_enabled — a tenant administrator has not enabled the review decision API for your organisation, so nothing was written; or the key lacks the write:kyc_review_decision scope (error is API key does not have write:kyc_review_decision scope). A tenant administrator enables the API in the Backoffice, KYC → Invitations → Settings tab, card “Review decision API”.review_decision_not_enabledapplication/json KycReviewDecisionError
404The byte-identical “V1 endpoint not found” response, returned for a malformed id, an id that does not exist, an id that belongs to another organisation, or a route that is not available (the review decision API is off for the platform, or the KYC module is not enabled for your organisation) — indistinguishable from an unknown path by design.V1 endpoint not foundapplication/json
409Nothing was written. review_not_pending — the verification already holds a decision. review_closed — the review was closed. legal_basis_missing — the verification has not finished, or no active legal basis is recorded for it. conflict — another decision for this verification was written at the same moment; read the verification and send a new request only if it is still pending.conflict, legal_basis_missing, review_closed, review_not_pendingapplication/json KycReviewDecisionError
413invalid_body — the body is larger than 16 KiB. Nothing was written.invalid_bodyapplication/json KycReviewDecisionError
415invalid_body — the Content-Type is not application/json. Nothing was written.invalid_bodyapplication/json KycReviewDecisionError
422idempotency_key_reuse — the Idempotency-Key was already used for a different verification or a different body. Nothing was written. Use a new key for a new decision.idempotency_key_reuseapplication/json KycReviewDecisionError
429Rate limit exceeded. Wait Retry-After seconds (also retryAfter in the body) before retrying; the response carries no other rate-limit headers.Rate limit exceededapplication/json
500API-key authentication is not configured on this deployment (server_misconfigured). Retrying does not help; contact support with the requestId.server_misconfiguredapplication/json KycSessionError
503Retryable. Retry with the same Idempotency-Key: you get the decision written by an earlier try as a replay, or the decision is written now. review_decision_unavailable — the decision, or the setting that enables the route, could not be read or written right now. tenant_module_gate_unavailable — the module check could not be confirmed. api_key_auth_unavailable — the key could not be checked. Every one of them carries Retry-After.tenant_module_gate_unavailableapplication/json KycReviewDecisionError
503Noxtica could not read the key’s record. This is a temporary fault on Noxtica’s side and says nothing about your key. Retry after the Retry-After delay; do not treat it as a revoked key.api_key_auth_unavailableapplication/json

Response headers

StatusHeaderTypeDescription
200Cache-ControlstringAlways no-store, private. Never cache or store a response of an identity details route or of the review decision route.
200Idempotent-Replayedstringtrue only when the answer is the stored answer of an earlier request with the same Idempotency-Key. Absent otherwise.
400Cache-ControlstringAlways no-store, private. Never cache or store a response of an identity details route or of the review decision route.
403Cache-ControlstringAlways no-store, private. Never cache or store a response of an identity details route or of the review decision route.
404Cache-ControlstringAlways no-store, private. Never cache or store a response of an identity details route or of the review decision route.
409Cache-ControlstringAlways no-store, private. Never cache or store a response of an identity details route or of the review decision route.
413Cache-ControlstringAlways no-store, private. Never cache or store a response of an identity details route or of the review decision route.
415Cache-ControlstringAlways no-store, private. Never cache or store a response of an identity details route or of the review decision route.
422Cache-ControlstringAlways no-store, private. Never cache or store a response of an identity details route or of the review decision route.
429Retry-AfterintegerSeconds until the bucket allows another request.
503Cache-ControlstringAlways no-store, private. Never cache or store a response of an identity details route or of the review decision route.
503Retry-AfterintegerSeconds to wait before retrying.

Response fields (200, application/json)

FieldTypeAlways returnedDescription
requestIdstringYes
verificationIdstringYesThe decided verification. Format and limits of a verification id are in KycVerificationId.
decisionstringYesA reviewer’s decision. approve — the reviewer affirmed the verification; the reported status becomes verified. override — the reviewer replaced the automated verdict; it is reported as it is but is not an approval and never makes the status verified. There is no reject decision today. An escalation is not a decision: it leaves the decision null. Allowed values: approve, override.
reasonCodestringYesThe closed reason code of a decision made through the API, never free text. Each decision accepts its own subset. approve: identity_confirmed, registry_crosscheck_passed, name_variant_accepted, manual_document_check_passed, other. override: identity_mismatch, registry_crosscheck_failed, document_invalid, suspected_fraud, liveness_not_accepted, other. other means none of the listed reasons. A code outside the subset of the decision is refused with invalid_reason_code. Allowed values: identity_confirmed, registry_crosscheck_passed, name_variant_accepted, manual_document_check_passed, identity_mismatch, registry_crosscheck_failed, document_invalid, suspected_fraud, liveness_not_accepted, other.
reviewerstringYesThe reviewer of the request.
statestringYesThe review state after the decision, approved for approve and overridden for override. Allowed values: approved, overridden.
versionintegerYesThe review version after the decision (review.version on the reads, data.decisionVersion of the kyc.review.decided event). Minimum 1.
decidedAtinteger (int64)YesUnix ms the decision was written.
replayedbooleanYestrue when this is the stored answer of an earlier request with the same Idempotency-Key (the response also carries Idempotent-Replayed: true); false when this request wrote the decision.

Response example (200)

{
  "requestId": "req-test",
  "verificationId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "decision": "approve",
  "reasonCode": "registry_crosscheck_passed",
  "reviewer": "reviewer-17",
  "state": "approved",
  "version": 1,
  "decidedAt": 1790762445000,
  "replayed": false
}

Example request

curl

curl -X POST "https://collect.noxtica.com/v1/kyc/verifications/example-verificationId/review-decision" \
  -H "Authorization: Bearer $NOXTICA_API_KEY" \
  -H "Idempotency-Key: example-Idempotency-Key" \
  -H "Content-Type: application/json" \
  -d '{
  "decision": "approve",
  "reasonCode": "registry_crosscheck_passed",
  "reviewer": "reviewer-17",
  "note": "Name matches the registry answer."
}'

Node.js (global fetch, Node 18 or later)

const response = await fetch('https://collect.noxtica.com/v1/kyc/verifications/example-verificationId/review-decision', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.NOXTICA_API_KEY}`,
    'Idempotency-Key': 'example-Idempotency-Key',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    decision: 'approve',
    reasonCode: 'registry_crosscheck_passed',
    reviewer: 'reviewer-17',
    note: 'Name matches the registry answer.',
  }),
});
console.log(response.status, await response.text());

Python (standard library)

import json
import os
import urllib.request

request = urllib.request.Request(
    "https://collect.noxtica.com/v1/kyc/verifications/example-verificationId/review-decision",
    data=json.dumps({
        "decision": "approve",
        "reasonCode": "registry_crosscheck_passed",
        "reviewer": "reviewer-17",
        "note": "Name matches the registry answer.",
    }).encode(),
    headers={
        "Authorization": f"Bearer {os.environ['NOXTICA_API_KEY']}",
        "Idempotency-Key": "example-Idempotency-Key",
        "Content-Type": "application/json",
    },
    method="POST",
)
with urllib.request.urlopen(request) as response:
    print(response.status, response.read().decode())

Read the current verification capacity and a start / wait recommendation

GET /v1/kyc/load

Server-to-server. Tells your backend how busy Noxtica’s verification capacity is right now and whether this is a good moment to start a new verification. Call it before you send a customer into a capture flow or submit a verification, when you would rather queue or delay the customer than let them wait on a busy platform. The answer is advisory: it never reserves, admits or blocks anything, a verification started while the answer is defer is still processed, and every verification route keeps its own limits.

What the numbers mean. capacity is counted in concurrent inference slots, not in sessions. One slot runs one inference job at a time, and one verification uses a slot for each of its steps (a step typically takes 25 ms to 3 s; a verification has about three to five steps). capacity: 8 therefore means eight inference jobs can run at the same moment, not that eight customers can be verified at the same moment. inUse is the number of busy slots, queueDepth the number of jobs waiting for a slot, estimatedWaitMs how long a job that arrives now would wait before it starts, and processingMs the p50 and p95 duration of recently completed jobs. Everything except yours is a platform-wide aggregate shared by all customers; yours.inFlight is the number of verifications your own organisation has running right now.

Recommendation.

recommendationMeaningSuggested action
acceptCapacity is available and nothing is queued.Start the verification.
degradedVerifications are still accepted and will finish, but expect them to be slower than usual, or the margin is thin.Start it if your flow tolerates extra latency; otherwise show a “taking longer than usual” message.
deferThe platform is saturated or has no ready capacity; a new job is likely to wait several seconds or be refused with an overload answer.Queue the customer, show a wait message, and ask again later with backoff, using estimatedWaitMs as a hint.

recommendation is derived from reasons: any reason from the first group below gives defer, otherwise any reason from the second group gives degraded, otherwise accept. reasons is ordered (the defer reasons first) and never repeats a value.

reasons valueGroupPresent when
no_ready_instancedeferNo inference capacity is ready to take work (capacity is 0).
queue_wait_highdeferestimatedWaitMs is at least 6000 ms, or at least the platform’s own queue-wait limit when that is lower.
queue_fulldeferqueueDepth is greater than or equal to capacity.
recent_overloaddeferA request was refused for overload, queue timeout or memory pressure within the last 30 seconds.
high_utilizationdegradedinUse / capacity is at least 0.75.
queue_buildingdegradedqueueDepth is greater than 0.
wait_elevateddegradedestimatedWaitMs is at least 1500 ms.
reduced_capacitydegradedPart of the platform’s inference capacity is out of service, so capacity is lower than normal.
escalation_unavailabledegradedThe escalation stage that re-examines difficult cases is unavailable; verifications continue without it.
escalation_saturateddegradedThe escalation stage is running at full capacity, so difficult cases may wait.
vram_pressuredegradedAccelerator memory is low on part of the capacity, which slows or refuses new jobs.
slow_processingdegradedprocessingMs.p95 is at least 10000 ms over at least 20 samples.
load_unavailabledegradedstatus is unknown: the load could not be read.

status: unknown. If Noxtica cannot read its own load (a timeout, an unreachable service, an invalid answer), the route still answers 200, never a 5xx, with status: unknown, recommendation: degraded, reasons: ["load_unavailable"] and null in capacity, inUse, queueDepth, estimatedWaitMs and processingMs. It is degraded, not defer, on purpose: a monitoring glitch must not make you stop every verification. Keep starting verifications; if the verification service is really down, the verification call itself reports it. An unknown answer is cached for 5 seconds, then the load is read again.

Caching and polling. The platform part of the answer is computed once and shared by all customers; ttlSeconds is how long that snapshot is kept (8 seconds, or 5 for unknown), and the response carries Cache-Control: private, max-age=5. Polling faster than every 10 seconds only returns the same snapshot. Poll from your backend on a timer (or when a customer is about to start), cache the answer on your server and share it between your users; do not call this route once per end-user page view. yours.inFlight is computed on each request. The route is rate limited per API key (30 requests per 60 seconds); a limited call answers 429 with Retry-After.

Authentication. Authorization: Bearer sk_... with the write:identity scope, the same scope as GET /v1/kyc/sessions/{sessionId}; there is no separate read scope. A key without it gets 403. The route is available only when the KYC module is enabled for your organisation; otherwise it answers the GatedNotFound 404, indistinguishable from an unknown path.

Privacy. The answer carries aggregated platform numbers and your own in-flight count only. It contains no personal data, no other customer’s data, and nothing about how or where the capacity is hosted.

Authentication: Bearer token (ApiKeyAuth), scope write:identity.

Responses

StatusMeaningError codesContent
200The capacity snapshot. Also the answer, with status: unknown, when the platform could not read its own load; this route does not answer 5xx for that.application/json KycLoadStatus
401The 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_keyapplication/json KycSessionError
403The presented key lacks the required scope.API key does not have write:identity scopeapplication/json
404The 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 foundapplication/json
429Rate limit exceeded. Wait Retry-After seconds (also retryAfter in the body) before retrying; the response carries no other rate-limit headers.Rate limit exceededapplication/json
500API-key authentication is not configured on this deployment (server_misconfigured). Retrying does not help; contact support with the requestId.server_misconfiguredapplication/json KycSessionError
503Retryable (Retry-After: 2). tenant_module_gate_unavailable: whether the KYC module is enabled for your organisation could not be confirmed. Retry shortly.tenant_module_gate_unavailableapplication/json KycSessionError
503Noxtica could not read the key’s record. This is a temporary fault on Noxtica’s side and says nothing about your key. Retry after the Retry-After delay; do not treat it as a revoked key.api_key_auth_unavailableapplication/json

Response headers

StatusHeaderTypeDescription
200Cache-ControlstringThe answer may be reused by the calling backend for up to 5 seconds.
429Retry-AfterintegerSeconds until the bucket allows another request.
503Retry-AfterintegerSeconds to wait before retrying.

Response fields (200, application/json)

FieldTypeAlways returnedDescription
okbooleanYesAllowed values: true.
statusstringYesok: the load was read and the numbers below are real. unknown: the load could not be read; the numbers are null and recommendation is degraded. Both are HTTP 200. Allowed values: ok, unknown.
recommendationstringYesaccept: start the verification. degraded: it will be accepted but may be slower than usual. defer: the platform is saturated; queue the customer and ask again later. Allowed values: accept, degraded, defer.
reasonsarray of stringYesWhy recommendation is not accept; empty when it is accept. Ordered with the defer reasons first; no value repeats. Treat an unrecognised value as informational.
capacityinteger, nullableYesConcurrent inference slots the platform has ready right now. One slot runs one inference job at a time and a verification uses a slot for each of its steps, so this is not a number of sessions. 0 when no capacity is ready; null when status is unknown. Minimum 0.
inUseinteger, nullableYesSlots that are running a job right now, platform-wide (your jobs and everyone else’s). null when status is unknown. Minimum 0.
queueDepthinteger, nullableYesJobs, platform-wide, that are waiting for a slot. null when status is unknown. Minimum 0.
estimatedWaitMsinteger, nullableYesEstimated milliseconds a job that arrives now would wait before it starts running (not including its own processing time). 0 when a slot is free. null when status is unknown, or when no capacity is ready. Minimum 0.
processingMsone of KycLoadProcessingMs or any, nullableYesHow long recently completed inference jobs took, in milliseconds (the job itself, not the queue wait). null when status is unknown or no job has completed recently.
processingMs{KycLoadProcessingMs}object (KycLoadProcessingMs)NoPercentiles of the duration of recently completed inference jobs. Each value is the upper bound of the duration bucket that holds the percentile (25, 50, 100, 200, 400, 800, 1500, 3000, 6000, 12000 or 20000 ms), so it is a conservative figure; durations above 20000 ms read as 20000.
processingMs{KycLoadProcessingMs}.p50integerYesMedian job duration in milliseconds. Minimum 0.
processingMs{KycLoadProcessingMs}.p95integerYes95th-percentile job duration in milliseconds. Minimum 0.
processingMs{KycLoadProcessingMs}.samplesintegerYesHow many recently completed jobs the percentiles are computed from. Minimum 1.
yoursobjectYesThe only values that are specific to your organisation.
yours.inFlightinteger, nullableYesVerifications of your own organisation that are running right now (submitted to the verification service within the last 90 seconds and not yet finished). Computed on each request, never cached. null when it could not be counted; this never fails the request. Minimum 0.
asOfstring (date-time)YesThe instant, in ISO 8601 UTC, the platform snapshot was taken (for unknown, when the failed read was recorded).
ttlSecondsintegerYesHow long Noxtica keeps the platform snapshot before reading the load again: 8 seconds, or 5 for an unknown answer. Polling more often than this only returns the same snapshot.
requestIdstringYes

Response example (200)

{
  "ok": true,
  "status": "ok",
  "recommendation": "accept",
  "reasons": [],
  "capacity": 8,
  "inUse": 5,
  "queueDepth": 0,
  "estimatedWaitMs": 0,
  "processingMs": {
    "p50": 400,
    "p95": 1500,
    "samples": 256
  },
  "yours": {
    "inFlight": 1
  },
  "asOf": "2026-10-28T17:31:08.000Z",
  "ttlSeconds": 8,
  "requestId": "req-test"
}

Example request

curl

curl "https://collect.noxtica.com/v1/kyc/load" \
  -H "Authorization: Bearer $NOXTICA_API_KEY"

Node.js (global fetch, Node 18 or later)

const response = await fetch('https://collect.noxtica.com/v1/kyc/load', {
  method: 'GET',
  headers: {
    Authorization: `Bearer ${process.env.NOXTICA_API_KEY}`,
  },
});
console.log(response.status, await response.text());

Python (standard library)

import os
import urllib.request

request = urllib.request.Request(
    "https://collect.noxtica.com/v1/kyc/load",
    headers={
        "Authorization": f"Bearer {os.environ['NOXTICA_API_KEY']}",
    },
    method="GET",
)
with urllib.request.urlopen(request) as response:
    print(response.status, response.read().decode())

Report one observed merchant transaction signal

POST /v1/kym/transactions

Notify-only. Records exactly one transaction signal that feeds scheduled alert-rule evaluation; this endpoint never calls a payment or execution API. The target caseId must already exist under KYM monitoring (created in the Backoffice KYB/KYM case surface).

Authentication: Bearer token (ApiKeyAuth), scope write:kym.

Request body

Content type application/json, required.

FieldTypeRequiredDescription
caseIdstringYesAt most 512 characters.
idempotencyKeystringYesAt most 255 characters.
amountMinorintegerYesAmount in the currency’s minor unit (e.g. cents). Minimum 0.
currencystringYesPattern ^[A-Z]{3}$.
counterpartyRefstring, nullableNo
occurredAtstring (date-time)Yes

Responses

StatusMeaningError codesContent
200A replayed idempotencyKey — the original row (deduplicated).application/json KymTransactionResponse
201A new transaction-event row was recorded.application/json KymTransactionResponse
400invalid_case_id / invalid_idempotency_key / invalid_amount / invalid_currency / invalid_occurred_at.invalid_case_id, invalid_requestapplication/json ErrorEnvelope
401The 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_keyapplication/json KycSessionError
403The presented key lacks the required scope.API key does not have write:identity scopeapplication/json
404The byte-identical gated-route 404, OR case_not_found (the caseId does not exist for this tenant).invalid_requestapplication/json ErrorEnvelope
429Rate limit exceeded. Wait Retry-After seconds (also retryAfter in the body) before retrying; the response carries no other rate-limit headers.Rate limit exceededapplication/json
503write_failed — a persistence fault.invalid_request, write_failedapplication/json ErrorEnvelope
503Noxtica could not read the key’s record. This is a temporary fault on Noxtica’s side and says nothing about your key. Retry after the Retry-After delay; do not treat it as a revoked key.api_key_auth_unavailableapplication/json

Response headers

StatusHeaderTypeDescription
429Retry-AfterintegerSeconds until the bucket allows another request.
503Retry-AfterintegerSeconds to wait before retrying.

Response fields (200, application/json)

FieldTypeAlways returnedDescription
okbooleanNoAllowed values: true.
eventIdstringNo
deduplicatedbooleanNo
requestIdstringNo

Response example (200)

{
  "ok": true,
  "eventId": "evt_3c9b2f7e4a3d4c8e9f1a2b3c4d5e6f70",
  "deduplicated": false,
  "requestId": "req-test"
}

Response fields (201, application/json)

FieldTypeAlways returnedDescription
okbooleanNoAllowed values: true.
eventIdstringNo
deduplicatedbooleanNo
requestIdstringNo

Response example (201)

{
  "ok": true,
  "eventId": "evt_3c9b2f7e4a3d4c8e9f1a2b3c4d5e6f70",
  "deduplicated": false,
  "requestId": "req-test"
}

Example request

curl

curl -X POST "https://collect.noxtica.com/v1/kym/transactions" \
  -H "Authorization: Bearer $NOXTICA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "caseId": "example-caseId",
  "idempotencyKey": "example-idempotencyKey",
  "amountMinor": 0,
  "currency": "USD",
  "counterpartyRef": "example-counterpartyRef",
  "occurredAt": "2026-09-13T12:00:00.000Z"
}'

Node.js (global fetch, Node 18 or later)

const response = await fetch('https://collect.noxtica.com/v1/kym/transactions', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.NOXTICA_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    caseId: 'example-caseId',
    idempotencyKey: 'example-idempotencyKey',
    amountMinor: 0,
    currency: 'USD',
    counterpartyRef: 'example-counterpartyRef',
    occurredAt: '2026-09-13T12:00:00.000Z',
  }),
});
console.log(response.status, await response.text());

Python (standard library)

import json
import os
import urllib.request

request = urllib.request.Request(
    "https://collect.noxtica.com/v1/kym/transactions",
    data=json.dumps({
        "caseId": "example-caseId",
        "idempotencyKey": "example-idempotencyKey",
        "amountMinor": 0,
        "currency": "USD",
        "counterpartyRef": "example-counterpartyRef",
        "occurredAt": "2026-09-13T12:00:00.000Z",
    }).encode(),
    headers={
        "Authorization": f"Bearer {os.environ['NOXTICA_API_KEY']}",
        "Content-Type": "application/json",
    },
    method="POST",
)
with urllib.request.urlopen(request) as response:
    print(response.status, response.read().decode())

This page is generated from the Noxtica OpenAPI specification release v0.16.1: kyc.yaml version 1.14.1-2026-11-07 (SHA-256 dc9e4b53d94c). Do not edit it by hand.