---
title: "Self-hosted identity verification (direct API)"
description: "Call the identity verification API directly from your server: mint a nonce, run a verification, read the result, and learn what the direct route does and does not cover today."
canonical_url: "https://noxtica.com/docs/identity-verification-self-hosted/"
markdown_url: "https://noxtica.com/docs/identity-verification-self-hosted/index.md"
last_updated: 2026-10-27
published: 2026-10-27
section: "identity-verification"
section_title: "Identity Verification"
site: "Noxtica"
language: "en"
source: markdown
---

# Self-hosted identity verification (direct API)

This guide is for a backend engineer who wants to start identity verifications from a server without a link and without the page Noxtica hosts. Your server calls the direct API, receives a verdict, reason codes and a verification id, and keeps them next to your own customer record.

Read the limits first. The direct route is a smaller surface than hosted sessions, and one thing does not work today: it gives your own screens no way to capture a document and have it checked. The section "Capture in your own application" says exactly why and what to use instead.

## What it is

Availability: Controlled availability. Identity verification is provisioned per tenant by your account team, and the account team confirms which of the routes below apply to your tenant. See the [product page](https://noxtica.com/products/identity-verification/) for the overview and the limits of the result.

The direct API is a set of server-to-server routes under `https://collect.noxtica.com/v1/kyc/`. Every call uses your server key as a bearer token. What it provides today:

- **Admission and result.** `POST /v1/kyc/verify` mints a single-use nonce (`action: "mint"`), then runs one verification with that nonce (`action: "verify"`). The response carries a verdict, a verification level, reason codes and a `verificationId`.
- **Safe retries.** A repeated call with the same `Idempotency-Key` returns the stored response again instead of running a second verification.
- **Re-check a known subject.** `POST /v1/kyc/reverify` asks whether a subject is already verified, to at least a level you name, using your tenant's device registry.
- **Bulk admission.** `POST /v1/kyc/verify/batch` admits up to 500 rows from a JSON or CSV manifest, with status, results and cancel routes.
- **Evidence for a verification that already exists.** `POST /v1/kyc/evidence/handle` and `POST /v1/kyc/evidence/finalize` upload sealed evidence for an admitted verification. A proof-of-address cross-match builds on that upload.
- **An event when a verification finishes.** `kyc.verification.completed`, described in [Identity verification events and webhooks](https://noxtica.com/docs/identity-verification-webhooks/).

Results are automated signals. You decide what each outcome means for your customer, and a person in your organisation should review any adverse decision before it affects someone.

## Capture in your own application

Documents are captured on the page Noxtica hosts. The direct API does not replace that page, for two reasons you can check against the routes above:

- A verify request carries data only. It does not accept images, audio or other media, and the request body is limited to 32 KiB.
- An evidence handle is issued only for a verification that already exists in your tenant. The verify call creates that verification and returns its id in the same request that runs the check, so your application has no moment in which to upload a capture and then run the document check on it.

Until a route for this exists, use [hosted identity verification](https://noxtica.com/docs/identity-verification-integration/) when the customer must capture a document, or send an invitation from Backoffice ([Invitations and hosted verification settings](https://noxtica.com/docs/identity-verification-invitations/)). This section is where a future route for capture in your own application will be documented; nothing on this page promises one.

Plan your code for what the current document check returns. It hands its result to a person for review and reports the reason code `provider_manual_review`, so expect `unverified` together with that reason rather than an automatic `verified`. Handle every verdict.

## Prerequisites

- **The module is provisioned for your tenant.** Until it is, every route below returns the plain `404` with the body `V1 endpoint not found`. Two switches have to be on (a platform switch and your tenant's module), and the response is identical whichever one is off, so a `404` means "ask your account team", not "retry".
- **A server key with the identity permission.** In Backoffice open API & SDK → Server API Keys and create a key with "Verify identity documents (KYC)" ticked. A new key does not carry it by default. The secret is shown once; keep it in your server secret store and never in a browser or an app.
- **For evidence routes only:** raw evidence storage armed for your tenant and a data residency (`TR`, `EU` or `US`) set for it. Ask your account team. A request whose `jurisdiction` does not match your tenant's residency gets the same plain `404`.
- **A recorded legal basis where your organisation is required to have one.** Without it the verify route answers `403 kyc_legal_basis_receipt_required`. An authorised person records it in Backoffice → Identity Verification → KYC Invitations → Settings.

## Steps

### 1. Mint an admission nonce

The nonce is single-use, tied to your tenant and valid for 300 seconds. Mint it as close to the verify call as you can.

```bash
curl -sS -X POST https://collect.noxtica.com/v1/kyc/verify \
  -H "Authorization: Bearer $NOXTICA_SERVER_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "action": "mint" }'
```

```json
{ "ok": true, "nonce": "<nonce>", "ttlSeconds": 300 }
```

### 2. Run the verification

Send the nonce within its lifetime, with an `Idempotency-Key` header of 8 to 128 characters. A missing or malformed key is rejected with `400 invalid_request` before anything runs.

```bash
curl -sS -X POST https://collect.noxtica.com/v1/kyc/verify \
  -H "Authorization: Bearer $NOXTICA_SERVER_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "action": "verify",
    "nonce": "<nonce>",
    "id_type": "passport",
    "purpose": "identity_verification"
  }'
```

```js
import { randomUUID } from 'node:crypto';

const base = 'https://collect.noxtica.com';
const headers = {
  Authorization: `Bearer ${process.env.NOXTICA_SERVER_KEY}`,
  'Content-Type': 'application/json',
};

const mint = await fetch(`${base}/v1/kyc/verify`, { method: 'POST', headers, body: JSON.stringify({ action: 'mint' }) });
if (!mint.ok) throw new Error(`mint failed: ${mint.status}`);
const { nonce } = await mint.json();

const idempotencyKey = randomUUID(); // store it with the customer; reuse it only to retry this same request
const verify = await fetch(`${base}/v1/kyc/verify`, {
  method: 'POST',
  headers: { ...headers, 'Idempotency-Key': idempotencyKey },
  body: JSON.stringify({ action: 'verify', nonce, id_type: 'passport', purpose: 'identity_verification' }),
});
const result = await verify.json();
if (!verify.ok) throw new Error(`verify failed: ${verify.status} ${result.error}`);
// Store result.verificationId with your customer record. Read the verdict below.
```

| Field | Required | Notes |
| --- | --- | --- |
| `action` | No | `mint` or `verify`. When you leave it out the request is treated as `verify`. |
| `nonce` | Yes | From the mint call. Used once. A missing nonce is rejected with `400 missing_nonce`. |
| `id_type` (or `idType`) | Yes | The document type your customer presents. A missing value is rejected with `400 invalid_request`. |
| `purpose` | No | Defaults to `identity_verification`. Other values come from a closed list; an unknown value can be rejected with `400 invalid_request`. |
| `payload` | No | Data only, never media. What a payload may contain depends on the configured check; leave it out unless your account team specifies one. |
| `deviceContext` | No | `true` adds an advisory `deviceTrust` block to the response. |

A request that includes an `attachDeviceTrust` field, in any shape, is rejected with `400`: device trust cannot be asserted by the caller.

### 3. Read and store the result

A verdict is always returned with status `200`. A fault on Noxtica's side comes back as `"verdict": "error"`, never as a subject statement and never as a `5xx`.

```json
{
  "ok": true,
  "verdict": "unverified",
  "verificationLevel": "partial",
  "reasonCodes": ["provider_manual_review"],
  "verificationId": "<verification-id>",
  "requestId": "<request-id>"
}
```

| Field | Meaning |
| --- | --- |
| `verdict` | `verified`, `unverified` or `error`. Act on `verified` as an approval only after your own checks; treat everything else as not approved. |
| `verificationLevel` | `verified`, `partial`, `unverified` or `inconclusive`. |
| `reasonCodes` | Closed-list codes that explain the outcome. Branch on codes, never on text, and ignore codes you do not know. |
| `verificationId` | Identifies the verification. Find it in Backoffice under Identity Verification → KYC Verifications. |
| `requestId` | Quote it when you contact support. |

A tenant policy that blocks or challenges can turn a provider `verified` into `unverified` and add `policy_blocked` or `policy_challenge_required` to the reason codes.

A repeated request with the same `Idempotency-Key` returns the stored response byte for byte, with no second verification. A different body under a key you already used is refused with `409 idempotency_conflict`. A request still running under that key answers `409` with the reason code `request_in_progress`; wait and retry with the same key.

### 4. Choose how you learn about later changes

`POST /v1/kyc/verify` answers when the check ends, so you do not need to poll for the result of that call. A verification that reaches a final state also sends a signed event, `kyc.verification.completed`. Register an endpoint under Integrations → Webhooks and verify the signature as described in [Identity verification events and webhooks](https://noxtica.com/docs/identity-verification-webhooks/). Treat the event as a notification: compare it with the verification you stored before you act. A person's review decision is visible in Backoffice under Identity Verification → KYC Review Queue and Identity Verification → KYC Verifications.

### 5. Re-check a subject you verified before (optional)

`POST /v1/kyc/reverify` answers one question: is this subject already verified, in your tenant's device registry, to at least the level you ask for. It needs the same key permission and a device registry module that your account team enables, it takes the opaque subject identifier your records use for the customer, and it always returns `200`.

```bash
curl -sS -X POST https://collect.noxtica.com/v1/kyc/reverify \
  -H "Authorization: Bearer $NOXTICA_SERVER_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "subjectId": "cust_example", "requiredLevel": "verified" }'
```

```json
{ "ok": true, "verified": true, "verificationLevel": "verified", "requestId": "<request-id>" }
```

A missing `subjectId` or an unknown `requiredLevel` is rejected with `400 invalid_request`.

### 6. Admit rows in bulk (optional)

`POST /v1/kyc/verify/batch` accepts a JSON manifest (`{ "items": [{ "idType": "passport" }] }`) or a CSV with an `id_type` column, up to 500 rows and 256 KiB, with a required `Idempotency-Key` header. It answers `202` with a `batchId` and one status per row. Read progress with `GET /v1/kyc/verify/batch/{batchId}/status` and per-row outcomes with `GET /v1/kyc/verify/batch/{batchId}/results`, and stop work that has not started with `POST /v1/kyc/verify/batch/{batchId}/cancel`. The field-level reference is in the [identity API reference](https://noxtica.com/docs/api/reference/kyc/).

### 7. Attach evidence to an admitted verification (optional)

Evidence is uploaded as sealed ciphertext: `POST /v1/kyc/evidence/handle` returns a short-lived handle and the keys to seal with, and `POST /v1/kyc/evidence/finalize` redeems it. Your code does the sealing, so plan time for it and read the [identity API reference](https://noxtica.com/docs/api/reference/kyc/) before you start. The handle is valid for 15 minutes. Evidence kinds are a closed list, and kinds that the platform derives itself are refused with `400 invalid_request`.

## Verify it works

- The mint call returns `"ok": true` and a nonce, and the verify call returns status `200` with a `verificationId`.
- Repeating the verify call with the same `Idempotency-Key` and body returns the same response.
- The verification appears in Backoffice under Identity Verification → KYC Verifications with the `verificationId` you stored.
- If you registered an endpoint, your handler receives `kyc.verification.completed` with a valid signature.

## Troubleshooting

| Symptom | Cause | Fix |
| --- | --- | --- |
| `404` with `V1 endpoint not found` | The module is not provisioned for your tenant, the path is wrong, or an evidence request named a residency other than your tenant's. | Check the path, then ask your account team. Do not retry in a loop. |
| `401 invalid_api_key` | The key is missing, malformed, unknown or revoked. | Check the header and that the key is a current `sk_prod_` key. |
| `403` with `API key does not have write:identity scope` | The key lacks the identity permission. | Create a key with "Verify identity documents (KYC)" ticked. |
| `403 kyc_legal_basis_receipt_required` | Your organisation has no recorded legal basis. | Record it in Backoffice, then retry. |
| `403 tenant_inactive` | The tenant is suspended. | Contact your account team. |
| `400 missing_nonce` | The verify body has no nonce. | Mint a nonce and send it. |
| `400 invalid_request` | A missing `Idempotency-Key` or `id_type`, a malformed `purpose`, an `attachDeviceTrust` field, or evidence of a kind you may not upload. | Fix the request. |
| Verdict `unverified` with `provider_manual_review` | The check handed the result to a person. | A reviewer decides in Backoffice under Identity Verification → KYC Review Queue. |
| Verdict `error` | A fault on the verification side, not a statement about the subject. | Retry with a new nonce and a new `Idempotency-Key`. |
| `402 spend_cap_reached` | Your organisation's spending limit is reached. | Contact your account team. |
| `409 idempotency_conflict` | The key was used with a different request. | Use a new key for a new request. |
| `413 body_too_large` or `415 invalid_request` | The body is over the limit or not JSON. | Send a small JSON body with `Content-Type: application/json`. |
| `429 Rate limit exceeded` | More than 10 calls in 60 seconds with one key on these routes. | Back off and retry. Honour `Retry-After` when present. |
| `503 kyc_verify_unavailable` | A temporary fault. | Retry after the `Retry-After` delay. |

The shared error envelope and the rate-limit rules are described in [Errors](https://noxtica.com/docs/api/errors/) and [Authentication](https://noxtica.com/docs/api/authentication/).

## Related

- [Identity verification integration](https://noxtica.com/docs/identity-verification-integration/): hosted sessions, the route that captures documents for you.
- [Invitations and hosted verification settings](https://noxtica.com/docs/identity-verification-invitations/): send a hosted link from Backoffice without code.
- [Identity verification events and webhooks](https://noxtica.com/docs/identity-verification-webhooks/): every identity event and its payload.
- [Verification data: storage, access and deletion](https://noxtica.com/docs/identity-verification-data/): how verification data is kept and deleted.
- [Identity API reference](https://noxtica.com/docs/api/reference/kyc/): the generated field-level reference.
