Tento překlad je vytvořen strojově a čeká na kontrolu.Přepnout do angličtiny
Na této stránce

Identity verification events and webhooks

Identity verification sends signed HTTPS events to endpoints you register in Backoffice, so your server does not have to poll for outcomes. A Backoffice administrator registers the endpoint. A backend engineer verifies each delivery, removes duplicates and reads the session status before acting.

What it is

Availability: Controlled availability. Events belong to identity verification, which your account team provisions for your organisation. Events marked “when enabled for your organisation” depend on settings your account team controls.

An event is a notification, not a decision. Each delivery is a POST with a JSON envelope and four headers, signed with the signing secret of the endpoint that receives it. The six identity events are:

EventFires whenDelivery
kyc.session.completedA hosted session reaches completed, unverified or error. A session in review_pending fires it later, when a reviewer approves or overrides the case.Retried
kyc.session.cancelledThe customer cancels on the hosted page. Subscribe to it explicitly. When enabled for your organisation.Retried
kyc.verification.completedA verification reaches a final state, whether it came from a hosted session or from a direct API call.Retried
kyc.erasure.completedAn erasure request reaches completed. A partial erasure sends nothing. When erasure is enabled for your organisation.Retried
kyc.document.expiry_warningA verified document is 90, 60 or 30 days from expiry, or has expired. When enabled for your organisation.Single attempt
kyc.reverification.dueA scheduled re-verification becomes due. When enabled for your organisation.Single attempt

“Retried” means up to 5 more attempts (step 5). “Single attempt” means one try with no retry. A session that expires sends no event. Read its status to see expired. Events for business and merchant verification (kyb.entity.*, kym.*) are on Business and merchant verification. The generic envelope and header reference is on Webhooks.

Prerequisites

  • Identity verification provisioned for your organisation. See Identity verification integration.
  • A Backoffice user with the administrator role. The Webhooks page is restricted to administrators, and every change asks for fresh step-up verification (MFA).
  • A public HTTPS endpoint. Localhost, private addresses, internal names and URLs that carry a user name or password are refused. A custom HTTPS port is allowed.
  • Room for the endpoint: an organisation can register up to 25 endpoints, and each URL can be registered once.
  • A server-side secret store for the signing secret. The secret is shown once.
  • An endpoint that answers within 5 seconds. That is the default wait for each attempt; bounds can change.

Steps

1. Register the endpoint

  1. Open Backoffice → Integrations & API → Webhooks (/integrations/webhooks). Stay on the “Manage” tab. The “Integration” tab holds the in-product guide.
  2. Under “Add endpoint”, enter the “Endpoint URL”. Add a “Description (optional)” of up to 200 characters if you want one.
  3. Under “Events to deliver”, tick the identity events you need. Select at least one event.
  4. Select “Add endpoint” and complete the step-up prompt.
  5. Copy the secret from “Copy your signing secret now” into your server secret store, then select “I’ve copied it”. Backoffice never shows it again.

The Backoffice label for each identity event:

EventLabel in “Events to deliver”
kyc.session.completedHosted KYC session completed
kyc.session.cancelledHosted KYC session cancelled by the end user
kyc.verification.completedKYC verification completed
kyc.erasure.completedKYC erasure completed
kyc.document.expiry_warningKYC document expiry warning
kyc.reverification.dueKYC re-verification due

Subscriptions are per event. An endpoint receives the events you ticked, and an event type added to the platform later is not added to an existing endpoint.

2. Receive and acknowledge deliveries

Every delivery is an HTTPS POST with Content-Type: application/json and these headers:

HeaderValue
X-Noxtica-Signaturesha256= followed by the lowercase hex HMAC-SHA256 signature
X-Noxtica-TimestampThe time of this attempt in epoch milliseconds. It changes on every retry.
X-Noxtica-EventThe event type, equal to type in the body
X-Noxtica-DeliveryThe delivery id, equal to id in the body and to data.deliveryId. It stays the same across retries.

The body is one envelope:

FieldMeaning
idThe delivery id. Treat it as an opaque string.
typeThe event type.
created_atWhen the event occurred, in epoch milliseconds. It equals data.occurredAt.
tenant_idYour organisation id.
dataThe event fields in step 4. schemaVersion ("1") sits inside data, not on the envelope.

Answer with a 2xx status and do the work afterwards. Noxtica does not follow redirect responses.

3. Verify the signature

The signature is an HMAC-SHA256 over <timestamp>.<raw body>, keyed with the endpoint’s signing secret as UTF-8 text. 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 enforces no timestamp window. 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);
}

Save the verifier above as verify-noxtica-webhook.mjs. This receiver keeps the raw body, deduplicates and acknowledges quickly:

import { createServer } from 'node:http';
import { verifyNoxticaWebhook } from './verify-noxtica-webhook.mjs';

const seen = new Set(); // replace with a durable store shared by all your instances

createServer(async (req, res) => {
  const chunks = [];
  for await (const chunk of req) chunks.push(chunk);
  const rawBody = Buffer.concat(chunks).toString('utf8');

  const valid = verifyNoxticaWebhook({
    secret: process.env.NOXTICA_WEBHOOK_SECRET,
    timestamp: req.headers['x-noxtica-timestamp'],
    signature: req.headers['x-noxtica-signature'],
    rawBody,
  });
  if (!valid) return res.writeHead(401).end();

  const deliveryId = req.headers['x-noxtica-delivery'];
  if (!seen.has(deliveryId)) {
    seen.add(deliveryId);
    void handleEvent(JSON.parse(rawBody)); // step 5
  }
  res.writeHead(204).end();
}).listen(3000);

The same check in Python:

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)

4. Handle each event

Every identity data object carries schemaVersion ("1"), occurredAt (epoch milliseconds) and deliveryId. No identity event carries your subjectRef, a name or a document value. reasonCodes come from a closed catalog; see Server API reference. Ignore fields you do not recognise.

kyc.session.completed

The delivery id is <sessionId>:<webhookId>.

FieldMeaning
sessionIdThe sessionId you stored when you created the session.
statuscompleted, unverified or error.
verificationIdThe verification case id, or null when no verification ran.
reasonCodesAn array of closed reason codes. It can be empty.
nvidOptional. The Noxtica Verification ID, when one was issued. See Noxtica Verification ID.
email, emailVerifiedOptional. The address the customer entered and whether they confirmed it. Present when email verification is enabled for your organisation and an address was entered. It is personal data.
{
  "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": [],
    "occurredAt": 1790762445000,
    "deliveryId": "<sessionId>:<webhookId>"
  }
}

A finished capture currently waits in review_pending. This event follows when a reviewer approves or overrides the case in Backoffice, normally within a few minutes of the decision.

kyc.session.cancelled

The delivery id is <sessionId>:<webhookId>. It fires once, when the customer’s cancel moves the session to cancelled_by_user. A repeated cancel, a refused cancel and an expiry send nothing. Persist the reason from the event: the status read does not return it.

FieldMeaning
sessionIdThe session id.
statusAlways cancelled_by_user.
cancelReasonnot_now, privacy_concern, no_document, technical_problem, other, or null.
cancelledAtWhen the customer cancelled, in epoch milliseconds.
email, emailVerifiedOptional, as above.
{
  "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>"
  }
}

kyc.verification.completed

The delivery id is <verificationId>:<webhookId>. A hosted session runs a verification too, so an endpoint subscribed to both events can receive this one and kyc.session.completed for the same session. Correlate by verificationId. The order between the two is not fixed.

FieldMeaning
verificationIdThe verification id. It equals session.verificationId on the status read.
verdictverified, unverified or error.
reasonCodesAn array of closed reason codes. It can be empty.
idTypeThe document type asserted on the request.
{
  "id": "<verificationId>:<webhookId>",
  "type": "kyc.verification.completed",
  "created_at": 1790762440000,
  "tenant_id": "<tenant-id>",
  "data": {
    "schemaVersion": "1",
    "verificationId": "<verificationId>",
    "verdict": "unverified",
    "reasonCodes": ["<reason-code>"],
    "idType": "passport",
    "occurredAt": 1790762440000,
    "deliveryId": "<verificationId>:<webhookId>"
  }
}

kyc.erasure.completed

It fires when an erasure request reaches completed. It deliberately carries no subject identifier, so the notice cannot re-link to the person. Noxtica does not delete verification details or evidence automatically. An administrator deletes verifications in Backoffice → Identity Verification → KYC Verifications, which does not send this event, or works an erasure request in KYC Erasure, where a request is approved by an administrator other than the requester. See Identity verification data.

FieldMeaning
requestIdThe id of the erasure request. It is not a subject id.
outcomeAlways completed.
{
  "id": "<deliveryId>",
  "type": "kyc.erasure.completed",
  "created_at": 1790900000000,
  "tenant_id": "<tenant-id>",
  "data": {
    "schemaVersion": "1",
    "requestId": "<erasure-request-id>",
    "outcome": "completed",
    "occurredAt": 1790900000000,
    "deliveryId": "<deliveryId>"
  }
}

kyc.document.expiry_warning and kyc.reverification.due

A scheduled job sends both, when enabled for your organisation. The delivery id is <scheduleRowId>:<webhookId>. Match verificationId to the original verification. Each threshold fires once and is attempted once, so a failed attempt is not retried. Reconcile from your own records if you miss one.

FieldEventMeaning
verificationIdbothThe verification whose document is expiring, or that is due again.
alertTypeexpiryexpiry_90d, expiry_60d, expiry_30d or expiry_expired.
documentExpiresAtexpiryWhen the document expires or expired, in epoch milliseconds.
dueAtre-verificationWhen the re-verification became due, in epoch milliseconds.
{
  "id": "<scheduleRowId>:<webhookId>",
  "type": "kyc.document.expiry_warning",
  "created_at": 1798538000000,
  "tenant_id": "<tenant-id>",
  "data": {
    "schemaVersion": "1",
    "verificationId": "<verificationId>",
    "alertType": "expiry_30d",
    "documentExpiresAt": 1801130000000,
    "occurredAt": 1798538000000,
    "deliveryId": "<scheduleRowId>:<webhookId>"
  }
}

5. Deduplicate, tolerate order and read the status

Delivery is at least once. Noxtica answers each outcome as follows:

Your responseWhat happens
2xxDelivered.
4xx, including 408 and 429Final. Not retried. Return a 4xx when you mean it.
3xx, 5xx, a timeout or a network errorRetried, up to 5 more attempts. The spacing is not published.
All retries failMarked dead. The endpoint stays enabled and Noxtica does not send the delivery again.
The URL is no longer a public HTTPS targetRefused and not retried.
  • Deduplicate on X-Noxtica-Delivery. Record it before you act.
  • Do not rely on order. Two events for the same session can arrive in either order.
  • There is no redelivery button and no replay API. To catch up, read the state: for hosted sessions, call the status read, and for a verification, open it in Backoffice → Identity Verification → KYC Verifications.
  • Treat the event as a prompt, and the status read as the authority:
async function handleEvent(event) {
  if (event.type !== 'kyc.session.completed') return;
  const response = await fetch(`https://collect.noxtica.com/v1/kyc/sessions/${event.data.sessionId}`, {
    headers: { Authorization: `Bearer ${process.env.NOXTICA_SERVER_KEY}` },
  });
  const { session } = await response.json();
  // Act on session.status. The "completed" status is the sole approval.
}

6. Rotate, disable or delete an endpoint

On the “Manage” tab of Backoffice → Integrations & API → Webhooks, each endpoint has these buttons:

  • “Rotate secret” asks you to confirm “Rotate signing secret?”. The current secret stops working immediately, with no overlap, and the new one is shown once. Update your receiver straight away. Retries still waiting are signed with the new secret.
  • “Disable” stops deliveries to the endpoint and keeps it listed. “Enable” turns it back on.
  • “Delete” removes the endpoint; deliveries to it stop immediately.

The tab has no editor for the event list of an existing endpoint. To change the events, delete the endpoint and add it again; the new endpoint has a new secret.

Verify it works

  1. On the endpoint row, select “Send test”. Backoffice posts one signed sample, once, and shows “Test delivery succeeded” with the status and time. The row then shows the last status, for example ok.
  2. Confirm that your receiver passed the signature check. The sample has the type submission.scored. It cannot produce an identity event, so it proves reachability and your signature code, not your identity handler.
  3. Test the handler with a signed sample of your own for each identity event, or finish a hosted session from your server or from Backoffice → Identity Verification → KYC Invitations (“Create test session”).
  4. For a session or verification event, open Backoffice → Identity Verification → KYC → KYC Processes, select “View” on the process, and read the “Webhook deliveries” table. Its columns are Event, State, Attempts and Created. delivered means your endpoint answered 2xx, dead means every retry failed, and terminal means a 4xx or a refused URL ended it. The endpoint row’s last status reflects “Send test” and non-identity events, not these deliveries.

Troubleshooting

SymptomCauseFix
“Send test” succeeds but no identity event arrivesThe test sends submission.scored. The endpoint may not be subscribed to the identity event, may be disabled, or the event is not enabled for your organisation.Check “Events to deliver”, select “Enable” if needed, and ask your account team about gated events.
Signature check failsThe body was parsed and re-serialised, the secret is old after a rotation, the timestamp is read in seconds, or created_at was used instead of the header.Hash the raw body, use the current secret, and use X-Noxtica-Timestamp in milliseconds.
Identity events never arrive for an endpoint that was created long agoThe endpoint was created before encrypted secret storage. Identity events are not delivered to it, and the delivery ends as terminal.Select “Rotate secret” once, then update your receiver.
Events stop after you return 429Any 4xx is final and is never retried.Return 2xx and queue the work.
An event arrives minutes after the reviewer’s decisionReview decisions are sent on a periodic schedule.Wait a few minutes. Read the status for the current state.
No event for review_pending or expiredNeither state sends an event.Read the session status.
The same event arrives twiceDelivery is at least once, and an unconfirmed delivery can be sent again.Deduplicate on X-Noxtica-Delivery.
A delivery shows deadEvery retry failed.Fix the endpoint, then read the state from the status read.
“Send test” shows blocked or http_0The URL is not a public HTTPS target.Use a public HTTPS URL without credentials in it.
“That URL is already registered as a webhook endpoint”The organisation has the URL already.Use the existing endpoint, or delete it before you add it again.
“Webhook endpoints are limited to 25 per tenant”The endpoint limit is reached.Delete an endpoint you no longer use.