Developer docs
Add a live face check to sign-up, login or withdrawals. One REST call starts it, the player does it in their browser, and your backend gets the verdict by signed webhook.
How it works
- Your backend creates a session for one player:
registerthe first time,verifyafter that. - The player does the check at the session's link (redirect, iframe or popup) or on your page with the web SDK: they fit their face to a mask, follow random head turns and blink.
- Netrik decides: liveness, anti-spoof and, for
verify, a match against the registered face. Your backend gets the result by webhook, or asks for it.
A session is one check for one player. It lasts 15 minutes and allows 5 attempts for failures a better try can fix (light, the moves, liveness). A failure no retry can fix ends it at once (the face is already registered, no such user, the face doesn't match). The player only ever sees passed or failed; the reasons and scores go to you.
Quickstart
- Sign in to the dashboard and open Integration: copy your API key and set your webhook URL.
- If you'll use an iframe, a popup or the web SDK, add your site under Allowed origins.
- Create a session from your backend, send the player to its
url, and handle thesession.completedwebhook. - Use Run a test on the Integration page to try the whole flow yourself.
Authentication
Every API call sends your key as a Bearer token: Authorization: Bearer fk_live_…. The base URL is https://verify.netrik.online.
Keep the key on your server, never in a web page or app. You can see it again on the Integration page after confirming it's you, and make a new one at any time (the old one stops working at once).
Create a session
POST /v1/sessions
| Field | Type | |
|---|---|---|
userId | string, required | Your id for the player: 1–64 of A–Z a–z 0–9 . _ @ -. Use a stable id, not an email you may change. |
type | string, required | register (stores their face) or verify (checks it's the same person). |
check | string | 3d: head turns, then a blink. 2d: two blinks. Default: your Verification rules setting. |
metadata | object | Anything up to 1 KB, returned with the result (an order id, the action being confirmed…). |
curl https://verify.netrik.online/v1/sessions \
-H "Authorization: Bearer fk_live_…" \
-H "Content-Type: application/json" \
-d '{ "userId": "user-123", "type": "register", "metadata": { "orderId": 42 } }'{
"sessionId": "ses_…",
"token": "…",
"url": "https://verify.netrik.online/?session=…",
"expiresAt": "2026-11-01T12:15:00.000Z"
}Every response carries your credits left in the x-netrik-credits header. Give url to the player, or token to your page for the web SDK.
Show the check
Send the player to the session's url in one of three ways. It works on any modern phone or laptop browser with a camera, in 8 languages, with nothing to install.
// your backend created the session; send the player there
res.redirect(session.url);For an iframe or popup, add your site under Allowed origins: only those sites may embed the check, and they receive the outcome as a postMessage { source: "netrik", sessionId, passed, status }. The iframe needs allow="camera". Treat the message as a hint for your UI; the webhook is what you trust.
Web SDK
Runs the check on your own page, no iframe: it covers the page with the full-screen check and resolves when it's over. Add your site under Allowed origins first; requests from any other site are refused.
<script type="module">
import { start } from "https://verify.netrik.online/sdk.js";
// token: from POST /v1/sessions, passed to this page by your backend
const result = await start({ session: token });
// { sessionId, isPassed: true | false,
// status: "passed" | "failed" | "expired" | "error" | "cancelled", error? }
</script>cancelled: the player closed it. error: no camera, or the session can't be used (error says why). The check runs in a shadow root, so your CSS and ours stay apart. Plain scripts can call window.Netrik.start once the module has loaded. Like the postMessage, the result is a UI hint: trust the webhook.
Get a session
GET /v1/sessions/:sessionId returns the same data object as the webhook. Use it if you'd rather poll, or to double-check a webhook.
curl https://verify.netrik.online/v1/sessions/ses_… \
-H "Authorization: Bearer fk_live_…"| status | |
|---|---|
pending | Not finished yet. |
passed | The check passed. |
failed | A failure no retry can fix, or all 5 attempts used. |
expired | 15 minutes passed without a result. |
Webhooks
Set your endpoint on the Integration page, where Send test sends a webhook.test. Answer 2xx quickly; anything else is retried after 1, 5 and 30 minutes and 2 hours. The URL must be https:// on a public address.
POST https://your-backend.example/netrik
x-netrik-signature: v1=5f2c…
x-netrik-timestamp: 1793472000
x-netrik-delivery: ses_…
{
"id": "ses_…",
"type": "session.completed",
"createdAt": "…",
"data": {
"sessionId": "ses_…", "userId": "user-123", "type": "register", "check": "3d",
"status": "passed", "attempts": 1,
"result": { "passed": true, "reason": "registered", "real": 0.82, "live": 0.82, "age": 31 },
"metadata": { "orderId": 42 },
"createdAt": "…", "expiresAt": "…", "completedAt": "…"
}
}| Event | When | data |
|---|---|---|
session.completed | A session passed, failed or expired. | The session, with result and metadata. |
billing.low_credits | Your balance dropped to a fifth of your plan's credits. | { credits, plan } |
billing.payment_received | A USDT payment was confirmed on chain. | { paymentId, kind, item, amount, txHash } |
billing.renewal_due | Your paid month ends soon. | { plan, paidUntil } |
billing.plan_unpaid | Your paid month ended unpaid. | { plan, paidUntil, blockedAt } |
webhook.test | You pressed Send test. | { sessionId: null, message } |
Use x-netrik-delivery (the event id) to ignore a retry you've already handled.
Verify signatures
Each delivery is signed with your webhook signing secret (Integration page). x-netrik-signature is v1= plus the hex HMAC-SHA256 of `${x-netrik-timestamp}.${rawBody}`. Compare in constant time, and reject old timestamps so a captured request can't be replayed.
import crypto from "node:crypto";
// rawBody: the request body exactly as received (not re-serialised JSON)
function fromNetrik(req, rawBody, secret) {
const ts = String(req.headers["x-netrik-timestamp"] ?? "");
if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false; // older than 5 minutes
const expected = "v1=" + crypto.createHmac("sha256", secret).update(`${ts}.${rawBody}`).digest("hex");
const got = String(req.headers["x-netrik-signature"] ?? "");
return got.length === expected.length && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(got));
}Results
| Field | |
|---|---|
passed | true or false: the decision. |
reason | Why, in words: registered, ok, face does not match the registered face, face already registered, username already registered, username not registered, account is inactive, liveness check failed, or which move wasn't done. For your logs and support, not for matching in code: use passed and status. |
similarity | verify only: how close the face is to the registered one, 0–1. |
real, live | Anti-spoof and liveness scores, 0–1. Thresholds are set under Verification rules. |
age | Estimated age in years from the selfie, or null. An estimate, off by a few years either way: a signal, not proof of age. |
A face can only be registered once per business: registering the same face under a second userId fails with face already registered, which is how multi-accounting is caught.
Errors and limits
Errors are JSON: { "error": "…" }.
| Status | Meaning |
|---|---|
| 400 | A field is missing or invalid; error says which. |
| 401 | Missing or wrong API key. |
| 402 | Out of credits, or a register would pass your plan's stored users (verifies of existing users go on). |
| 403 | Your business is disabled. |
| 404 | No such session (sessions belong to the key that made them). |
| 429 | Too many sessions: 60 a minute, plus your plan's daily cap (200 on Free). Wait for Retry-After seconds. |
Credits
Each check costs credits ($0.01 each): a registration 10, a verification 5. A session is charged once, passed or failed, and each failed attempt the player retries after costs 1; a session that expires unused is free. You start with 500 free credits; plans and prices are on the pricing page.
Photos
Netrik keeps a small face photo from each registration and check, sealed with your public key: create a key pair (or upload your own public key) on the Integration page. Only your private key, in your browser, can open them; Netrik can't. Check photos are deleted after your plan's period (Free 7 days, Starter 30 days, Growth 90 days, Scale 180 days).
Stuck? Email hello@netrik.online.