Next CapNext Cap

KYC Verification

Verify customer identities with document, liveness, and face match checks. Server-to-server, enterprise-only, with signed webhooks and a hosted applicant flow.

What is KYC Verification?

KYC Verification runs real identity checks on your applicants — a government ID document scan, a liveness check, and a face match between the selfie and the document photo — and returns one clear, auditable decision. It is a server-to-server API: your backend creates a verification, hands the applicant a hosted flow to complete on their device, and you receive the outcome by webhook or polling.

  • One clear decision — every verification resolves to approved, declined, or review, with machine-readable reason codes.
  • Custom flows — choose exactly which checks each applicant runs, in the dashboard, and reference them by flowId.
  • Fraud signals — non-blocking riskFlags surface potential fraud so you can apply your own risk rules.
  • Built for compliance — every applicant grants explicit, recorded consent, and documents and selfies are purged after their retention window.
KYC Verification is an Enterprise feature. Manage keys, flows, and webhooks under Dashboard → Identity (KYC). This is distinct from Identity Verification (the in-chat HMAC sign-in that links a logged-in user to the widget).

Authentication

All KYC API calls use a KYC API key (prefix nextcap_kk_) in the X-Api-Key header. Mint one under Settings → API keys → KYC keys. When you create a key you pick the flow it runs, the same way a widget key loads one widget, so you never pass a flow id at request time. You are shown the key and a signing secret exactly once, the secret is what verifies your webhooks.

These are server-to-server keys. Requests that carry a browser Origin are rejected. Never embed a KYC key in front-end code.

Step 1 — Create a verification

From your backend, create a verification when a user reaches the point where you need to verify them. The response includes a short-lived verifyUrl you send the applicant to.

curl -X POST https://api.nextcap.ai/api/v1/external/kyc/verifications \
-H "X-Api-Key: nextcap_kk_your_key" \
-H "Content-Type: application/json" \
-d '{
"applicantEmail": "user@example.com",
"metadata": { "orderId": "A-1234" }
}'
201 Createdjson
{
"data": {
"id": "vrf_9a2b7c1d4e5f",
"status": "pending",
"clientToken": "a1b2c3…",
"verifyUrl": "https://verify.nextcap.ai/v/vrf_9a2b7c1d4e5f?token=a1b2c3…",
"expiresAt": "2026-06-25T10:30:00.000Z"
}
}
Request body
applicantEmailstring

Optional. Applicant email, used for the flow and dashboard display.

localestring

Optional. BCP-47 locale for the hosted flow, e.g. en, fr, ar.

returnUrlstring

Optional. Where to send the applicant after they finish the hosted flow.

metadataobject

Optional. Arbitrary key/values echoed back to you, e.g. your own order or user id.

The flow is decided by your key, so there is no flowId to send. To run a different flow, mint a second key bound to it and swap the key. (Legacy keys with no bound flow fall back to your org's default flow and still accept a flowId in the body.)

Step 2 — Hand off the applicant

Redirect the applicant's browser (or open in a new tab / in-app webview) to the verifyUrl. It is of the form /v/<id>?token=<clientToken> and walks them through each step on their own device, where the camera lives. The link expires with the session, so create it close to when the user will use it.

The clientToken authenticates the applicant's browser to that one session only. It is safe to put in the URL you hand the user; your secret key never leaves your server.

Step 3 — Receive the result

Verifications complete asynchronously. Learn the outcome two ways — use webhooks for push, and polling as a fallback or reconciliation.

Option A — Webhooks (recommended)

Register an endpoint with the key whose secret should sign deliveries. You can also manage endpoints under Dashboard → Identity (KYC) → Webhooks.

POST /external/kyc/webhooksbash
curl -X POST https://api.nextcap.ai/api/v1/external/kyc/webhooks \
-H "X-Api-Key: nextcap_kk_your_key" \
-H "Content-Type: application/json" \
-d '{
"url": "https://crm.example.com/hooks/kyc",
"events": ["verification.completed", "fraud.flagged"]
}'
Events
verification.completedevent

Fires on every terminal outcome (always).

verification.approvedevent

The applicant passed.

verification.rejectedevent

The applicant was declined.

verification.manual_reviewevent

Landed in manual review (a person should decide).

fraud.flaggedevent

One or more risk flags were raised.

Each delivery is a signed JSON envelope:

POST to your endpointjson
{
"event": "verification.completed",
"timestamp": "2026-06-25T10:00:00.000Z",
"id": "whd_3f1c…",
"data": {
"verificationId": "vrf_9a2b7c1d4e5f",
"status": "approved",
"decision": "approved",
"declineType": null,
"reasonCodes": [],
"riskFlags": [],
"mode": "live",
"createdAt": "2026-06-25T09:58:00.000Z",
"completedAt": "2026-06-25T10:00:00.000Z"
}
}
Delivery headers
X-Nextcap-Signaturestring

HMAC-SHA256 of the raw request body, hex-encoded, using your key's secret.

X-Nextcap-Eventstring

The event name.

X-Nextcap-Deliverystring

A stable delivery id (whd_…); dedupe on it if you process asynchronously.

Verify the signature

Recompute the HMAC over the raw request body and compare in constant time. Reject anything that does not match.

const crypto = require("crypto");
// The secret shown once when you minted your KYC API key.
const SECRET = process.env.NEXTCAP_KYC_SECRET;
function verify(rawBody, signatureHeader) {
const expected = crypto
.createHmac("sha256", SECRET)
.update(rawBody)
.digest("hex");
const a = Buffer.from(expected);
const b = Buffer.from(signatureHeader || "");
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
// Express: mount express.raw({ type: "application/json" }) so req.body is the
// EXACT bytes we signed.
app.post("/hooks/kyc", (req, res) => {
if (!verify(req.body, req.header("X-Nextcap-Signature"))) {
return res.status(401).end();
}
const event = JSON.parse(req.body.toString("utf8"));
// event.data.verificationId, event.data.decision …
res.status(200).end();
});
We retry a failed delivery up to 3 times with exponential backoff. Respond 2xx within ~8 seconds to acknowledge; anything else counts as a failure.

Option B — Polling

Read a verification's current status on demand. Returns the same data shape as the webhook. While still pending, decision is null.

GET /external/kyc/verifications/{'{'}id{'}'}bash
curl https://api.nextcap.ai/api/v1/external/kyc/verifications/vrf_9a2b7c1d4e5f \
-H "X-Api-Key: nextcap_kk_your_key"
200 OKjson
{
"data": {
"verificationId": "vrf_9a2b7c1d4e5f",
"status": "approved",
"decision": "approved",
"declineType": null,
"reasonCodes": [],
"riskFlags": [],
"mode": "live",
"createdAt": "2026-06-25T09:58:00.000Z",
"completedAt": "2026-06-25T10:00:00.000Z"
}
}

The decision

  • approved — all required checks passed.
  • declined — a required check failed. declineType is retry (the applicant can try again) or final.
  • review — inconclusive; a human should decide. reasonCodes explains why.

reasonCodes are machine-friendly labels (for example BAD_FACE_MATCHING, EXPIRATION_DATE). riskFlags are non-blocking fraud signals you can act on in your own risk engine.

Endpoints

POST/external/kyc/verificationsAPI Key

Create a verification session. Returns the hosted verifyUrl + a short-lived client token.

GET/external/kyc/verifications/:idAPI Key

Polling fallback. Read a verification's current status and decision.

POST/external/kyc/webhooksAPI Key

Register a webhook endpoint. Deliveries are signed with this key's secret.

GET/external/kyc/webhooksAPI Key

List your registered webhook endpoints and their delivery health.

DELETE/external/kyc/webhooks/:idAPI Key

Remove a webhook endpoint.