On this page

Identity verification API

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

EnvironmentBase URL
Productionhttps://collect.noxtica.com

Credential: ApiKeyAuth, SessionTokenAuth. See Authentication.

How this page is organisedBar length shows each section’s share of the text. Select a section to jump to it.

KYC Verification

1 operation

KYC Cross-Validation

1 operation

KYC Batch

4 operations

KYC Reverify

1 operation

KYC Evidence

2 operations

KYC Hosted Sessions

13 operations

KYC Compliance

2 operations

KYM

1 operation

Operations in Identity verification API

Verify an identity

POST /v1/kyc/verify

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

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

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

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

Parameters

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.

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).

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 foundapplication/json
409A conflicting in-flight or idempotency-key-bound request (action:'verify' only).example-code, idempotency_conflictapplication/json ConflictError
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

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].

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

Response headers

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

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

Response headers

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

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

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

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

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

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)Yes
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

Response headers

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

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.
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

Response headers

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

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())

Mint a Noxtica-hosted KYC verification-session link

POST /v1/kyc/sessions

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

Before you can mint. (1) The identity verification module must be enabled for your organisation; until it is, this route answers the same byte-identical 404 as an unknown path. (2) Your sk_ key needs the write:identity scope (a key created with the default scope is refused with 403). (3) A tenant administrator must have recorded an active legal-basis receipt for hosted document capture in the Backoffice; without it the mint is refused with 403 kyc_legal_basis_receipt_required. Noxtica never selects a legal basis for you. (4) Your organisation needs a hosted configuration (business name, privacy URL, supported documents) in the Backoffice, or the customer sees “Verification isn’t ready yet”; the mint itself does not check this.

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

Lifetime. An unopened link keeps expiresInSeconds (default 24 hours). Once the customer first opens it the deadline tightens to min(expiresAt, openedAt + 30 minutes); the customer must finish capture inside that window. expiresAt in every response is Unix time in milliseconds.

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

Outcome. Learn the outcome with the kyc.session.completed webhook or by polling GET /v1/kyc/sessions/{sessionId}. With the identity provider configured today every successful capture ends in review_pending, not completed: the webhook fires only after a Backoffice reviewer approves the case (allow up to about 5 minutes after the decision), and a review_pending session never expires. See that route for the status table.

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

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

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

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

Parameters

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. At most 2048 characters. Pattern ^https://.
purposestringNoShown to the customer on the consent screen. Falls back to your organisation’s default purpose. More than 500 characters is refused with 400 invalid_request. At most 500 characters.
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.
hostedOptions.allowedDocumentTypesarray of stringYesAt least 1 item, at most 4 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.

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, or steps.emailVerification is not one of off, optional, required), return_url_not_allowed, or email_verification_unavailable (an explicit steps.emailVerification of optional or required was requested but hosted email verification is not enabled for the platform).email_verification_unavailable, invalid_hosted_options, invalid_id_type, invalid_request, return_url_not_allowedapplication/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 key lacks the write:identity scope; or kyc_legal_basis_receipt_required (no active hosted-capture legal-basis receipt for your organisation); or age_gender_estimation_not_enabled (hostedOptions.steps.ageGenderEstimation was true but the platform feature or your organisation’s acknowledgement is missing; see reasonCode).age_gender_estimation_not_enabled, kyc_legal_basis_receipt_requiredapplication/json KycSessionError
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 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

Response headers

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

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.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.
requestIdstringYes

Response example (200)

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

Example request

curl

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

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

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

Python (standard library)

import json
import os
import urllib.request

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

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

GET /v1/kyc/sessions/{sessionId}

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

Status lifecycle.

statusMeaningkyc.session.* webhook
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; it fires later as completed
completedA business decision exists: a reviewer approved or overrode the case (or, by design, a machine verified).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.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; allow up to about 5 minutes between the decision and the delivery. Until then the only way to observe review_pending is this read. A rejected end state is not currently expressible by the review vocabulary.

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

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

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

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

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

Parameters

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

Response headers

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

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.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. unverified — finished with a negative outcome and no manual-review code. error — a provider or infrastructure fault, or the submit was refused at admission. expired — the effective deadline passed without a decision (reported by the status read; no webhook). cancelled_by_user — the customer cancelled on the hosted page. Allowed values: pending, opened, verifying, review_pending, completed, unverified, error, expired, cancelled_by_user.
session.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 first opened the link; null until then.
session.expiresAtinteger (int64)YesThe effective deadline. Equal to the mint-time deadline while the link is unopened; once the customer opens it, tightened to min(expiresAt, openedAt + 1800000) (the 30-minute capture window). A session in review_pending does not expire.
session.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.
session.reasonCodesarray of stringYesClosed reason-code catalog. provider_manual_review means the case is waiting for a manual review.
session.purposestring, nullableYesThe mint-time purpose.
session.returnUrlstring, nullableYesThe mint-time returnUrl.
session.selectedIdTypestring, nullableYesThe document type the customer chose on the page.
session.selectedIssuingCountrystring, nullableYesThe issuing country the customer chose.
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: completed to approved, unverified to rejected, review_pending to review_pending, error to error.
session.decision.outcomestringYesAllowed values: approved, rejected, review_pending, error.
session.decision.reasonCodesarray of stringYes
session.decision.decidedAtinteger (int64)YesUnix ms.
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",
    "status": "review_pending",
    "idType": "passport",
    "subjectId": "cust_4821",
    "captureStep": "submit",
    "createdAt": 1790762000000,
    "openedAt": 1790762100000,
    "expiresAt": 1790763900000,
    "completedAt": 1790762445000,
    "verificationId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
    "reasonCodes": [
      "provider_manual_review"
    ],
    "purpose": "Open your brokerage account",
    "returnUrl": "https://merchant.example.com/kyc/return",
    "selectedIdType": "passport",
    "selectedIssuingCountry": "GB",
    "consentVersion": "2026-09-01",
    "consentAcceptedAt": 1790762110000,
    "speechProfilingConsentAcceptedAt": null,
    "ageGenderConsentAcceptedAt": null,
    "evidence": {
      "front": "obj_9c1e4a",
      "selfie": "obj_5b7d02"
    },
    "decision": {
      "outcome": "review_pending",
      "reasonCodes": [
        "provider_manual_review"
      ],
      "decidedAt": 1790762445000
    },
    "stages": {
      "scene_inventory": {
        "status": "pass",
        "score": 0.97,
        "reasonCodes": []
      },
      "document_type_id": {
        "status": "pass",
        "score": 0.94,
        "reasonCodes": []
      },
      "ocr_back": {
        "status": "skipped",
        "score": null,
        "reasonCodes": [
          "not_required"
        ]
      },
      "mrz_crosscheck": {
        "status": "pass",
        "score": 0.99,
        "reasonCodes": []
      },
      "selfie_quality": {
        "status": "pass",
        "score": 0.91,
        "reasonCodes": []
      },
      "document_near_face": {
        "status": "skipped",
        "score": null,
        "reasonCodes": [
          "not_required"
        ]
      },
      "active_liveness": {
        "status": "pass",
        "score": 0.93,
        "reasonCodes": []
      },
      "speech_profile": {
        "status": "skipped",
        "score": null,
        "reasonCodes": [
          "speech_profiling_not_requested"
        ]
      },
      "speech_analysis": {
        "status": "skipped",
        "score": null,
        "reasonCodes": [
          "speech_analysis_not_requested"
        ]
      },
      "age_gender": {
        "status": "skipped",
        "score": null,
        "reasonCodes": [
          "age_gender_not_requested"
        ]
      }
    },
    "scorecard": {
      "document_front": {
        "status": "pass",
        "score": 0.94,
        "checks": [],
        "reasonCodes": []
      },
      "document_back": {
        "status": "skipped",
        "score": 0,
        "checks": [],
        "reasonCodes": [
          "not_required"
        ]
      },
      "selfie": {
        "status": "pass",
        "score": 0.91,
        "checks": [],
        "reasonCodes": []
      },
      "liveness": {
        "status": "pass",
        "score": 0.93,
        "checks": [],
        "reasonCodes": []
      },
      "voice": {
        "status": "skipped",
        "score": 0,
        "checks": [],
        "reasonCodes": [
          "not_required"
        ]
      },
      "cross_validation": {
        "status": "pass",
        "score": 0.99,
        "checks": [],
        "reasonCodes": []
      },
      "overall": {
        "status": "pass",
        "score": 0.94,
        "checks": [],
        "reasonCodes": []
      }
    },
    "nvid": "NVID-7K3M-Q9XA-T2RW-4HZD"
  },
  "requestId": "req-test"
}

Example request

curl

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

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

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

Python (standard library)

import os
import urllib.request

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

Read the current hosted session (browser-facing)

GET /v1/kyc/session-view

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

Authentication: Bearer token (SessionTokenAuth).

Responses

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. unverified — finished with a negative outcome and no manual-review code. error — a provider or infrastructure fault, or the submit was refused at admission. expired — the effective deadline passed without a decision (reported by the status read; no webhook). cancelled_by_user — the customer cancelled on the hosted page. Allowed values: pending, opened, verifying, review_pending, completed, unverified, error, expired, cancelled_by_user.
session.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 tenant’s hosted setup filtered by this session’s options; null when the tenant has no hosted configuration (the page shows “Verification isn’t ready yet”).
session.configuration{KycHostedSessionConfiguration}object (KycHostedSessionConfiguration)No
session.configuration{KycHostedSessionConfiguration}.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 stringYes
session.configuration{KycHostedSessionConfiguration}.documents[].requireBackbooleanYes
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.
session.configuration{KycHostedSessionConfiguration}.options.allowedDocumentTypesarray of stringYesAt least 1 item, at most 4 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}.issuingCountrystringYes
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: completed to approved, unverified to rejected, review_pending to review_pending, error to error.
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. unverified — finished with a negative outcome and no manual-review code. error — a provider or infrastructure fault, or the submit was refused at admission. expired — the effective deadline passed without a decision (reported by the status read; no webhook). cancelled_by_user — the customer cancelled on the hosted page. Allowed values: pending, opened, verifying, review_pending, completed, unverified, error, expired, cancelled_by_user.
session.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 tenant’s hosted setup filtered by this session’s options; null when the tenant has no hosted configuration (the page shows “Verification isn’t ready yet”).
session.configuration{KycHostedSessionConfiguration}object (KycHostedSessionConfiguration)No
session.configuration{KycHostedSessionConfiguration}.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 stringYes
session.configuration{KycHostedSessionConfiguration}.documents[].requireBackbooleanYes
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.
session.configuration{KycHostedSessionConfiguration}.options.allowedDocumentTypesarray of stringYesAt least 1 item, at most 4 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}.issuingCountrystringYes
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: completed to approved, unverified to rejected, review_pending to review_pending, error to error.
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, 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
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": "jane.doe@example.com",
  "locale": "en",
  "consent": {
    "version": "nvid-email-v1",
    "accepted": true
  }
}'

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

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

Python (standard library)

import json
import os
import urllib.request

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

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

POST /v1/kyc/session-email/send

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

Authentication: Bearer token (SessionTokenAuth).

Request body

Content type application/json, required.

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": "jane.doe@example.com",
  "locale": "en"
}'

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

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

Python (standard library)

import json
import os
import urllib.request

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

Poll the email-verification state (browser-facing)

GET /v1/kyc/session-email/status

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

Authentication: Bearer token (SessionTokenAuth).

Responses

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, 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)

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

Response headers

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

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

Response headers

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

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())

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

Response headers

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

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.4.0: kyc.yaml version 1.2.0-2026-10-27 (SHA-256 c91a695cfde0). Do not edit it by hand.