Denne oversettelsen er maskingenerert og venter på gjennomgang.Bytt til engelsk
På denne siden

Identity verification integration

This guide takes a backend engineer from a provisioned account to a working hosted identity verification flow. Your server creates a session, your customer completes the steps on a page hosted by Noxtica, and your server reads the outcome by a status read or a signed event. Your server never receives the customer’s answers or capture directly.

Results are automated signals. You decide what each outcome means for your customer; Noxtica hands you the evidence and an audit trail.

Availability

Identity verification has Controlled availability. A tenant needs the module provisioned by the account team, and the account team confirms what applies to your tenant. Steps described here as optional apply only when they are enabled for your organisation. See the product page for the product overview and the limits of the result.

How the flow works

  1. Your server creates a session with your own opaque customer reference. The response carries a sessionId and a single-use link. Store the sessionId next to your customer record.
  2. You send the customer to the link: a full-page redirect, a new tab, an email or message, or a QR code.
  3. The customer completes the steps on the hosted page in their own browser.
  4. Noxtica processes the submission. A finished capture currently waits in the review queue in your Backoffice (review_pending) until someone in your organisation records a decision.
  5. Your server learns the outcome by reading the session status, or from a signed kyc.session.completed event. Read the status before you act on an event.
  6. If you set a return link, the customer can go back to your site from the hosted page after a completed outcome. The return link is a convenience, not proof that verification succeeded.

Before you start

Complete these once per environment:

  • Module provisioned. Your account team provisions identity verification for your tenant. Until then every request below receives a plain 404.
  • Hosted verification page configured. In the Backoffice, open KYC → Invitations → Settings and fill in the business name, privacy notice link and purpose text the customer sees. The KYC pages show a readiness checklist that names anything still missing. Until the page is configured the customer sees a “Verification isn’t ready yet” message.
  • Legal basis recorded by your organisation. An authorised person in your organisation records the legal basis for hosted identity verification in the same Settings page. Noxtica does not choose one for you. Until it is recorded, session creation is refused.
  • A server key with the identity permission. In the Backoffice, open API & SDK → Server API Keys and create a key with the “Verify identity documents (KYC)” permission ticked. A new key defaults to a narrower permission that cannot create sessions. The secret is shown once, so save it in your server secret store. Production keys start with sk_prod_, and keys for a non-production environment start with sk_test_.
  • Approved return hosts (optional). If you want a return link, add the host names of your return pages under “Approved return destinations” in the same Settings page. Saving that page asks for fresh step-up verification.
  • An events endpoint (optional). If you do not want to poll, register an HTTPS endpoint under Integrations → Webhooks, subscribe it to the identity events you need, and save the signing secret when it is shown. The secret is shown once.

Create a session

Send the request from your server. Never place the server key in a browser or a mobile app.

POST https://collect.noxtica.com/v1/kyc/sessions
Authorization: Bearer sk_prod_your_server_key
Content-Type: application/json
Idempotency-Key: your-retry-key

Request fields

The body is a JSON object. Field names are exact: an unknown field is ignored without an error, so a misspelt name silently falls back to its default.

FieldTypeRequiredNotes
subjectRefstringYesYour own opaque reference for the customer, 1 to 256 characters. Use an identifier from your system, not a name or an email address. Status reads return it to you; events never carry it.
expiresInSecondsintegerNoHow long an unopened link stays valid: 300 to 604800, default 86400. A value outside the range is rejected with a 400, not adjusted.
returnUrlstringNoAn https address whose host exactly matches an approved return host, up to 2048 characters. Any other host is rejected with return_url_not_allowed.
purposestringNoShort text shown to the customer on the hosted page, up to 500 characters. When you omit it the default purpose from your hosted page settings is used.
idTypestringNoAn optional hint for the document type. Leave it out unless your account team asks you to send it; the customer chooses on the page.
hostedOptionsobjectNoPer-session switches for optional steps. Leave it out to use your defaults. If you send it, send the complete object your account team supplies; a partial object is rejected with a 400.

The request has no field for language, webhook address, or customer contact details. The hosted page follows the customer’s browser language, and you register webhook endpoints in the Backoffice.

Response

A new session returns 200 with this shape:

{
  "ok": true,
  "sessionId": "<uuid>",
  "url": "https://verify.noxtica.com/s/<token>",
  "expiresAt": 1790848845000,
  "requestId": "<request-id>"
}
  • sessionId is a UUID. Store it with your customer record.
  • url is the single-use link. It is returned once, in this response, and no later call returns it again. Treat it as a secret: do not log it and do not put it in an analytics address.
  • expiresAt is a time in epoch milliseconds, not seconds.
  • requestId identifies the request. Quote it when you contact support.

Idempotency

Idempotency-Key is optional and must be 8 to 128 characters when present. Without it, every call creates a new session. With it, a retry that carries the same key returns the same sessionId with "replayed": true and no url. The key alone identifies the session: a different body under the same key returns the original session.

If a request times out and you never saw the link, you cannot recover it. Create a new session with a new key and let the old one expire.

Examples

curl -sS https://collect.noxtica.com/v1/kyc/sessions \
  -H "Authorization: Bearer $NOXTICA_SERVER_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"subjectRef":"cust_example","returnUrl":"https://merchant.example.com/verification/return"}'
const response = await fetch('https://collect.noxtica.com/v1/kyc/sessions', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.NOXTICA_SERVER_KEY}`,
    'Content-Type': 'application/json',
    'Idempotency-Key': crypto.randomUUID(),
  },
  body: JSON.stringify({
    subjectRef: 'cust_example',
    returnUrl: 'https://merchant.example.com/verification/return',
  }),
});

const body = await response.json();
if (!response.ok) throw new Error(`session creation failed: ${response.status} ${body.error}`);

// Store body.sessionId with your customer record, then send body.url to the customer.
import json
import os
import urllib.request
import uuid

request = urllib.request.Request(
    "https://collect.noxtica.com/v1/kyc/sessions",
    data=json.dumps(
        {
            "subjectRef": "cust_example",
            "returnUrl": "https://merchant.example.com/verification/return",
        }
    ).encode(),
    headers={
        "Authorization": "Bearer " + os.environ["NOXTICA_SERVER_KEY"],
        "Content-Type": "application/json",
        "Idempotency-Key": str(uuid.uuid4()),
    },
    method="POST",
)

with urllib.request.urlopen(request) as response:
    body = json.load(response)

# Store body["sessionId"] with your customer record, then send body["url"] to the customer.

Choose the delivery that fits your journey:

  • Redirect. Send the customer’s browser to url as a full page. This is the simplest path.
  • New tab. Open url in a new tab or window from your own page.
  • Email or message. Send the link through your own channel.
  • QR code. Show the link as a QR code so the customer can continue on a phone. The hosted page also offers a QR code and a copy-link option for moving to a phone.

What the hosted page does and does not do:

  • It cannot be embedded. The hosted page refuses to load inside an iframe. Open it as a full page or in a new tab.
  • It does not message your page. The page does not post a message to the window that opened it, and it does not close itself. Do not wait for a script on your side to learn the outcome.
  • It follows the customer’s language. The language comes from the customer’s browser. You cannot set it, and the link accepts no parameters beyond its own path.
  • Some browsers cannot be used. Some in-app browsers are not supported. The page tells the customer how to continue in their regular browser.

The return link is optional.

  • Set returnUrl when you create the session. Its host must be on your approved return hosts.
  • After a completed outcome, the hosted page shows a plain “Return to your business” link to exactly that address. There is no automatic redirect, and no query parameters are added, so the address carries no session id or status.
  • The link is not shown after a review, unverified, error, cancelled or expired outcome.
  • Invitations created from the Backoffice carry no return link.

Because a finished capture currently waits for a decision, most customers do not reach a return link straight away. Design your journey so that it does not depend on the customer coming back: show your own “we will let you know” state, and finish through the status read or the event. Correlate with the sessionId you stored, and never treat a visit to your return page as proof of completion.

Lifetimes

An unopened link stays valid until its own expiry, one day by default. After the customer first opens the link, the customer has 30 minutes to finish, or until the original expiry if that comes sooner. A session that waits in review_pending does not expire while it waits for a decision. KYC Hosted Sessions explains expiry and the 410 responses the hosted page itself returns.

Get the outcome

Two routes lead to the outcome: a status read, and a signed event. Use both. The event saves you from polling, and the status read is the authority.

Read the session status

GET https://collect.noxtica.com/v1/kyc/sessions/{sessionId}
Authorization: Bearer sk_prod_your_server_key

A session you created always returns 200, including an expired one, which reads "status": "expired". A session id that does not exist, or that belongs to another organisation, returns 404.

{
  "ok": true,
  "session": {
    "id": "<uuid>",
    "status": "review_pending",
    "subjectId": "cust_example",
    "expiresAt": 1790763900000,
    "completedAt": 1790762445000,
    "verificationId": "<verification-id>",
    "reasonCodes": ["<reason-code>"],
    "returnUrl": "https://merchant.example.com/verification/return",
    "decision": {
      "outcome": "review_pending",
      "reasonCodes": ["<reason-code>"],
      "decidedAt": 1790762445000
    }
  },
  "requestId": "<request-id>"
}
  • Everything sits under session. session.id is the sessionId from creation, and subjectId is the subjectRef you sent.
  • Timestamps are epoch milliseconds, or null when the moment has not happened.
  • decision appears once the session has an outcome. Its outcome is approved, rejected, review_pending or error.
  • verificationId identifies the verification case, so you can find it in the KYC pages of the Backoffice.
  • When Noxtica Verification IDs are enabled for your organisation, the session also carries nvid.
  • The response can carry more fields than this page lists. Ignore fields you do not use and do not fail on unknown ones.

Poll with a delay that grows between reads and stop at a final status. Do not poll in a tight loop; rate limits apply.

Statuses

StatusMeaningEvent
pendingCreated. The customer has not opened the link.None
openedThe customer opened the link and is working through the steps.None
verifyingThe customer submitted and processing is running.None
review_pendingProcessing finished and a decision is needed in your Backoffice review queue. Not a rejection, and not an approval.None yet. kyc.session.completed follows when a decision is recorded.
completedA decision approved the session.kyc.session.completed with status completed
unverifiedProcessing finished with a negative outcome.kyc.session.completed with status unverified
errorProcessing could not finish, because of a fault or a refusal.kyc.session.completed with status error
expiredThe link or the capture window lapsed before a decision.None. Read the status to observe expiry.
cancelled_by_userThe customer chose to cancel on the hosted page. This applies when cancelling is enabled for your organisation.kyc.session.cancelled

Only completed is an approval. Treat every other status as not approved until you have read a later one.

Receive events

Events are notifications. They save you from polling, and they are not the decision.

EventWhen it is sent
kyc.session.completedA session reaches completed, unverified or error. This includes the moment a reviewer decision resolves a review_pending session.
kyc.session.cancelledThe customer cancelled on the hosted page. Subscribe to it explicitly. It applies when cancelling is enabled for your organisation.

Other identity event types exist. This page covers the two that describe a hosted session.

Review decisions are relayed on a periodic schedule, so allow a few minutes between a decision and its event. An expired session sends no event: use the status read to observe expiry.

The Backoffice “Send test” button sends a sample of a different event type. It cannot produce an identity event, so test your handler with a signed sample of your own.

Every delivery is an HTTPS POST with these headers:

HeaderValue
X-Noxtica-Signaturesha256= followed by the lowercase hex signature
X-Noxtica-TimestampThe time of this attempt, in epoch milliseconds
X-Noxtica-EventThe event type
X-Noxtica-DeliveryThe delivery id, identical to the id in the body

The body is a JSON envelope with id, type, created_at, tenant_id and data. schemaVersion sits inside data, not on the envelope.

{
  "id": "<sessionId>:<webhookId>",
  "type": "kyc.session.completed",
  "created_at": 1790762445000,
  "tenant_id": "<tenant-id>",
  "data": {
    "schemaVersion": "1",
    "sessionId": "<sessionId>",
    "status": "completed",
    "verificationId": "<verification-id>",
    "reasonCodes": ["<reason-code>"],
    "occurredAt": 1790762445000,
    "deliveryId": "<sessionId>:<webhookId>"
  }
}
{
  "id": "<sessionId>:<webhookId>",
  "type": "kyc.session.cancelled",
  "created_at": 1790762300000,
  "tenant_id": "<tenant-id>",
  "data": {
    "schemaVersion": "1",
    "sessionId": "<sessionId>",
    "status": "cancelled_by_user",
    "cancelReason": "not_now",
    "cancelledAt": 1790762300000,
    "occurredAt": 1790762300000,
    "deliveryId": "<sessionId>:<webhookId>"
  }
}
  • cancelReason is null or one of not_now, privacy_concern, no_document, technical_problem and other.
  • When Noxtica Verification IDs are enabled for your organisation, a completed event also carries data.nvid. Other optional steps can add fields to data, so ignore fields you do not recognise.
  • Events never carry your subjectRef. Match an event to your customer by sessionId.

Verify the signature

The signature is an HMAC-SHA256 with your endpoint’s signing secret over the timestamp, a dot, and the raw request body: ${timestamp}.${rawBody}. Verify it before you parse or trust anything in the body.

  • Use the body exactly as received. Do not parse and re-serialise it.
  • Compare in constant time.
  • Check the timestamp header, not created_at. Every retry is signed again with a fresh timestamp.
  • Noxtica does not enforce a timestamp window on its side, so enforce your own. Five minutes is a sensible tolerance.
import { createHmac, timingSafeEqual } from 'node:crypto';

const TOLERANCE_MS = 5 * 60 * 1000;

export function verifyNoxticaWebhook({ secret, timestamp, signature, rawBody, now = Date.now() }) {
  const sentAt = Number(timestamp);
  if (!Number.isSafeInteger(sentAt) || Math.abs(now - sentAt) > TOLERANCE_MS) return false;

  const expected = createHmac('sha256', secret).update(`${timestamp}.${rawBody}`).digest('hex');
  const provided = String(signature).replace(/^sha256=/, '').toLowerCase();

  const expectedBytes = Buffer.from(expected);
  const providedBytes = Buffer.from(provided);
  return expectedBytes.length === providedBytes.length && timingSafeEqual(expectedBytes, providedBytes);
}
import hashlib
import hmac
import time

TOLERANCE_MS = 5 * 60 * 1000


def verify_noxtica_webhook(secret: str, timestamp: str, signature: str, raw_body: bytes) -> bool:
    if not (timestamp.isascii() and timestamp.isdigit()):
        return False
    if abs(time.time() * 1000 - int(timestamp)) > TOLERANCE_MS:
        return False

    signed = timestamp.encode() + b"." + raw_body
    expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
    provided = signature.removeprefix("sha256=").lower()
    return hmac.compare_digest(expected, provided)

Delivery, retries and duplicates

  • A 2xx response marks the delivery as delivered.
  • Any 4xx response is final. The delivery is not retried, including 408 and 429. Return a 4xx only when you mean it.
  • A redirect, a 5xx response, a timeout or a network error is retried, up to five more attempts. After that the delivery is marked failed and is not sent again. The retry spacing is not published, so do not build on it.
  • Each attempt waits up to five seconds for your response, and redirects are not followed. Acknowledge quickly and process afterwards.
  • Delivery is at least once. Deduplicate on X-Noxtica-Delivery, which stays the same across retries.
  • Order is not fixed. Two events for the same session can arrive in either order.

A notification is not authority

When an event arrives, verify it, record the delivery id, and then read GET /v1/kyc/sessions/{sessionId}. Act on the status that read returns, not on the event body. This keeps your state correct when events are late, repeated or out of order, and it means a forged or replayed event cannot approve anyone.

Noxtica Verification ID

When Noxtica Verification IDs are enabled for your organisation, a session that completes or waits for review is given an ID in the form NVID-XXXX-XXXX-XXXX-XXXX. The customer sees it on the final page, and you read it from session.nvid or data.nvid. The ID is not a credential and does not sign anyone in. A person who has it can check its status on the verification site. That public lookup accepts requests only from the verification site itself, so your server cannot call it: read the ID from the session status or the event instead.

Errors

Every error body is JSON and includes a requestId. Quote it when you contact support.

StatusErrorMeaningWhat to do
400invalid_requestMalformed JSON, a missing or over-long subjectRef, a bad expiresInSeconds, purpose or Idempotency-Key.Fix the request. Check field names and limits.
400invalid_hosted_optionshostedOptions is not a complete, valid object.Send the complete object or leave it out.
400invalid_id_typeidType is not accepted for this session.Leave idType out.
400return_url_not_allowedreturnUrl is not https or its host is not an approved return host.Add the host in the hosted page settings or leave returnUrl out.
400 or 403an error ending in _not_enabled, or email_verification_unavailableYou asked for an optional step that is not enabled for your organisation.Leave that step out of hostedOptions, or ask your account team.
401invalid_api_keyThe key is missing, malformed, unknown or revoked.Check the Authorization header, and that the key matches the environment you call.
403API key does not have write:identity scopeThe key lacks the identity permission.Create a key with “Verify identity documents (KYC)” ticked.
403kyc_legal_basis_receipt_requiredYour organisation has not recorded the legal basis for hosted identity verification.Record it in the Backoffice, then retry.
404V1 endpoint not foundThe module is not provisioned for your tenant, or the path is wrong. A status read also returns it for an unknown session id.Check the path and session id. If both are right, ask your account team. Do not retry in a loop.
413body_too_largeThe request body is too large.Send a small JSON object.
415invalid_requestThe Content-Type is not JSON.Send Content-Type: application/json.
429Rate limit exceededToo many requests.Back off and retry. Honour retryAfter or Retry-After when present.
500server_misconfiguredA server-side configuration problem.Contact support with the requestId.
503tenant_module_gate_unavailable, kyc_legal_basis_receipt_unavailable, invalid_request with a reasonA temporary fault.Retry with a growing delay, and honour Retry-After when present.
503hosted_verify_not_configuredThe hosted verification service is not set up for this environment.Contact your account team.

A status read can also return 400 with Invalid sessionId when the id cannot be decoded.

Go-live checklist

  • The readiness checklist in the Backoffice KYC pages shows no missing item, and a test session runs end to end.
  • The legal basis is recorded by an authorised person in your organisation, and the notices and consents you owe your customers are in place.
  • The server key has the identity permission, lives in a server secret store, and is never sent to a browser or an app.
  • Your server creates every session, sends an Idempotency-Key, stores the sessionId with the customer, and never logs the link.
  • Your return page makes no assumption about the outcome, and your journey works when the customer never comes back.
  • Your events endpoint uses HTTPS, verifies the signature over the raw body, enforces a timestamp tolerance, deduplicates on the delivery id, answers quickly and reads the session status before it acts.
  • A scheduled status read covers sessions that send no event, such as an expired session or one in review_pending.
  • Your code handles every status. Only completed is an approval, and review_pending is neither an approval nor a rejection.
  • A person in your organisation reviews any adverse decision before it affects a customer. Results can be wrong in either direction and are not legal proof of identity.
  • The Privacy Policy and the Security page describe how Noxtica handles personal data. This page does not.