Live verification API
Schedule a live ID verification session for your customer from your own system, track it, cancel or reschedule it, and receive the host's decision by webhook — all keyed by your own reference code.
A live ID verification is a human-hosted live video session: one of your team members
(the host) meets the applicant on camera, checks their ID live and records a
decision. Until now these sessions could only be scheduled from the dashboard. The
endpoints below let your system schedule a session for one of your customers,
follow it, and get the outcome back — by API and by
webhook — keyed by your own reference code (externalRef).
Every endpoint lives under /api/v1/kyc/conferences and is authenticated with
an API key (Authorization: Bearer ck_live_…). See
Authentication.
Live ID Verification (this page — a live video session with a human host) is a different feature from Live ID Validation (biometric logon for a returning user, see Live ID Validation).
How it works
sequenceDiagram
participant You as Your system
participant C as Certivant
participant A as Applicant
participant H as Host (your team)
You->>C: POST /conferences (externalRef, host, scheduledFor)
C-->>You: 201 conference + applicantJoinUrl
C->>A: Invitation email / SMS (join link)
C-->>You: webhook conference.scheduled
H->>C: Joins from the dashboard
C-->>You: webhook conference.started
A->>C: Consents + joins the session
H->>C: Records the decision
C-->>You: webhook conference.completed (decision, externalRef)
- You schedule the session for an existing verification request, or create the applicant inline. You name the host — a member of your organization who runs the session from the dashboard.
- Certivant invites the applicant (email and/or SMS, in their language) and returns the join link to you as well.
- Before entering the session the applicant answers the consent window. Consent
to video recording is always collected; the rest of the window can be
skipped with
consentScreen: falsewhen you already collected those consents in your own app. - When the host records the decision you receive
conference.completedwith the decision, its effect on the verification and yourexternalRef.
Before you start
| Requirement | Where |
|---|---|
API key with kyc:write (schedule / change) and kyc:read (read) | KYC → Developer |
| Your plan includes web conferencing | Your subscription. Otherwise 402 |
| Live ID Verification enabled in your settings | KYC → Settings → Live ID Verification. Otherwise 403 |
| Your account is verified by Certivant | Onboarding. Otherwise 403 |
| The host has manual-approval permission | The owner, or a role / user listed in your manual-approval settings. Otherwise 403 |
| A webhook URL (optional, for outcomes) | KYC → Settings → Webhooks — see Webhooks → Setup |
All date-times are ISO-8601 with a timezone — 2026-10-06T14:00:00Z or
2026-10-06T10:00:00-04:00. A date-time without a timezone is rejected, so a
session can never land at the wrong hour because of a server's local zone.
Responses are always in UTC (…Z).
Schedule a live verification
POST /api/v1/kyc/conferences Scope: kyc:write
Request body
{
"kycRequestId": "req_01J…",
"host": { "email": "compliance@yourcompany.com" },
"externalRef": "order-10042",
"title": "Account opening — video check",
"scheduledFor": "2026-10-06T10:00:00-04:00",
"language": "fr",
"consentScreen": true,
"applicantFileUploadEnabled": false,
"channels": { "email": true, "sms": true }
}
| Field | Type | Required | Notes |
|---|---|---|---|
kycRequestId | string | one of | An existing verification request in your organization. |
applicant | object | one of | Create the applicant + verification inline instead (see below). An applicant with the same email in your organization is reused. |
host.userId / host.email | string | yes (one) | The member who runs the session. Must have manual-approval permission. |
externalRef | string | no | Your reference code (order / case id). Echoed on every response and every webhook. Max 200. |
title | string | no | Shown to the host. Max 200. |
scheduledFor | ISO date-time | one of | When the session takes place. Must be in the future. |
startNow | boolean | one of | true = schedule for right now (the applicant is invited immediately). |
expiresAt | ISO date-time | no | End of the join window. Default: scheduledFor + 24 h. |
language | enum | no | en, fr, es, de, it, pt, nl, pl, ro, tr, ar, zh, ja, ko, hi, vi, id, ru, uk, sv. Language of the invitation, saved as the applicant's preferred language (any text without a translation in that language is sent in English). Default: the applicant's saved language, else your organization's default. |
consentScreen | boolean | no | Default true — the applicant confirms sharing personal information, photo capture and video recording. false = the window asks only for video-recording consent, which can never be disabled. |
faceCheckSkippable | boolean | no | Omitted: the request's form setting, else your organization's setting (the face check is required unless you changed it). false forces the face check to be required in the consent window. true offers the non-biometric alternative, only where the law requires one (e.g. Québec): the applicant may decline the face check, no selfie or liveness check runs, and the host compares the face with the ID visually in the call. The window does not include the face check when consentScreen is false. |
applicantFileUploadEnabled | boolean | no | Default false — during the session the applicant can only take photos with their camera. true also allows sending files from their device. The host can change it during the session. |
channels.email / channels.sms | boolean | no | How the invitation is delivered. Default: email; SMS to a newly created applicant's phone. For an existing applicant, their phone on file is texted only when sms: true. Both false = nothing is sent — deliver applicantJoinUrl yourself. |
Inline applicant (instead of kycRequestId):
{
"applicant": {
"fullName": "Jane Doe",
"email": "jane@example.com",
"phone": "+14155551234",
"emailVerified": false,
"phoneVerified": false,
"externalRef": "customer-881"
},
"host": { "userId": "usr_01H…" },
"externalRef": "order-10042",
"startNow": true
}
SMS invitations are billed like dashboard invitations (your plan's included SMS first, then your credit balance). If neither covers it the SMS is skipped and the email still goes out.
Example
curl -X POST https://app.certivant.com/api/v1/kyc/conferences \
-H "Authorization: Bearer $CERTIVANT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"kycRequestId": "req_01J...",
"host": { "email": "compliance@yourcompany.com" },
"externalRef": "order-10042",
"scheduledFor": "2026-10-06T10:00:00-04:00",
"language": "en"
}'
Response 201
The conference object, plus:
{
"id": "cmf…",
"kycRequestId": "req_01J…",
"externalRef": "order-10042",
"status": "scheduled",
"scheduledFor": "2026-10-06T14:00:00.000Z",
"expiresAt": "2026-10-07T14:00:00.000Z",
"applicantJoinUrl": "https://app.certivant.com/forms/conference/3f9c…",
"invitation": { "email": true, "sms": false }
}
applicantJoinUrl is returned only here and by
resend-invitation — Certivant stores just a hash of
the link, so it can't be read back later. applicantWasReused is added for an
inline applicant. invitation shows the channels the invitation was dispatched
on.
Errors
| Status | When |
|---|---|
400 | Validation failed (error.issues), both / neither of kycRequestId and applicant, both / neither of scheduledFor and startNow, a time in the past, expiresAt not after scheduledFor, or the host isn't a member of your organization. |
402 | Your plan doesn't include live verification. |
403 | Key lacks kyc:write; account pending verification; live verification disabled in settings; or the host lacks manual-approval permission. |
404 | kycRequestId doesn't exist in your organization. |
409 | The verification already has a joinable conference. The body carries its conferenceId — reschedule or cancel it instead. A retried POST therefore never invites the customer twice. |
Get a live verification
GET /api/v1/kyc/conferences/:id Scope: kyc:read
Returns the conference object. An id that belongs to
another organization is a 404.
curl https://app.certivant.com/api/v1/kyc/conferences/cmf… \
-H "Authorization: Bearer $CERTIVANT_API_KEY"
Look up by your reference code
GET /api/v1/kyc/conferences/by-external-ref/:externalRef Scope: kyc:read
The most recent conference carrying that externalRef (URL-encode it).
404 when none exists.
curl https://app.certivant.com/api/v1/kyc/conferences/by-external-ref/order-10042 \
-H "Authorization: Bearer $CERTIVANT_API_KEY"
List live verifications
GET /api/v1/kyc/conferences Scope: kyc:read
| Query | Notes |
|---|---|
status | scheduled, in_progress, completed, cancelled, or expired (a still-scheduled session whose join window closed without the session taking place — a no-show). scheduled excludes expired ones. |
externalRef | Exact match on your reference code. |
kycRequestId | Conferences of one verification. |
scheduledFrom / scheduledTo | ISO date-times, inclusive, on scheduledFor. |
createdFrom / createdTo | ISO date-times, inclusive, on creation time. |
page / limit | Default 1 / 20, limit max 100. |
Newest first, with a stable tiebreak so paging never skips or repeats a row.
curl "https://app.certivant.com/api/v1/kyc/conferences?status=completed&createdFrom=2026-10-01T00:00:00Z" \
-H "Authorization: Bearer $CERTIVANT_API_KEY"
{
"data": [ { "id": "cmf…", "status": "completed", "externalRef": "order-10042" } ],
"pagination": { "page": 1, "limit": 20, "total": 1, "totalPages": 1 }
}
Reschedule
POST /api/v1/kyc/conferences/:id/reschedule Scope: kyc:write
{
"scheduledFor": "2026-10-08T15:30:00+02:00",
"expiresAt": "2026-10-09T15:30:00+02:00",
"title": "Rescheduled video check",
"externalRef": "order-10042"
}
Only a scheduled conference can be moved (409 once it started, completed or
was cancelled). The new time must be in the future and before the end of the
join window — pass a later expiresAt to extend the window. title and
externalRef are optional: omit to keep, null to clear. Returns the
conference object plus previousScheduledFor, and fires
conference.rescheduled.
Rescheduling does not notify the applicant — call resend-invitation to send them the new time.
Cancel
POST /api/v1/kyc/conferences/:id/cancel Scope: kyc:write
{ "reason": "Customer cancelled the order" }
The body is optional. Cancels a scheduled conference: the applicant's link
stops working and conference.cancelled fires with your reason. A session that
is already in_progress returns 409 — the host ends it from the session. The
verification request itself is left unchanged.
Resend the invitation
POST /api/v1/kyc/conferences/:id/resend-invitation Scope: kyc:write
{ "channels": { "email": true, "sms": false }, "language": "es" }
Issues a fresh join link (the previous one stops working) and re-sends the
invitation on channels — default email, plus SMS when a phone is on file. With
both channels false nothing is sent: use the returned applicantJoinUrl
yourself (copyOnly: true). Allowed while the conference is scheduled or
in_progress and its join window is open, otherwise 409.
The conference object
{
"id": "cmf…",
"kycRequestId": "req_01J…",
"applicantId": "cma…",
"organizationId": "org_01H…",
"externalRef": "order-10042",
"title": "Account opening — video check",
"status": "completed",
"expired": false,
"scheduledFor": "2026-10-06T14:00:00.000Z",
"expiresAt": "2026-10-07T14:00:00.000Z",
"startedAt": "2026-10-06T14:01:12.000Z",
"endedAt": "2026-10-06T14:14:40.000Z",
"durationSeconds": 808,
"createdAt": "2026-10-01T09:00:00.000Z",
"updatedAt": "2026-10-06T14:14:40.000Z",
"host": { "userId": "usr_01H…", "name": "Sam Reviewer", "email": "compliance@yourcompany.com" },
"applicant": {
"id": "cma…",
"fullName": "Jane Doe",
"email": "jane@example.com",
"externalRef": "customer-881",
"joinedAt": "2026-10-06T14:02:03.000Z"
},
"additionalApplicants": [],
"additionalKycRequestIds": [],
"consent": {
"screenEnabled": true,
"requiredPurposes": ["share_information", "photo_capture", "video_recording"],
"given": true,
"at": "2026-10-06T14:01:58.000Z",
"version": "2026-10-04"
},
"decision": {
"decision": "verified",
"outcome": "approved",
"requestStatus": "APPROVED",
"notes": "ID checked on camera, matches the applicant.",
"decidedByUserId": "usr_01H…",
"documentDecisions": { "verified": 2, "rejected": 0, "needsResubmission": 0 }
},
"recording": { "available": true, "deliveryStatus": "delivered", "durationSeconds": 790, "sha256": "9b1f…" },
"certificate": null
}
| Field | Notes |
|---|---|
status | scheduled → in_progress (the host joined) → completed (decision recorded or session ended), or cancelled. |
expired | true when a scheduled session's join window closed without the session taking place (no-show). Filter with status=expired. |
consent.given | The primary applicant's answer: null until they answer, false if they refused (the session is then cancelled). requiredPurposes always contains video_recording. |
decision.decision | The host's verdict: verified, rejected, needs_more_info or inconclusive. null until decided. |
decision.outcome | What it did to the verification: approved, rejected, or escalated (needs_more_info / inconclusive park it for review). requestStatus is the verification's status. |
recording.available | true once the recording is assembled and stored or delivered to your storage. Media itself is never exposed by this API. |
certificate | The verification's Certificate of Verification when one was issued: id, certificateNumber, verificationUrl. |
Webhooks
Live verifications emit five events to your webhook URL (the form's override URL when the verification came from a form with one, else your organization's default). They fire for every conference — scheduled from the dashboard or the API — signed and retried exactly like every other Certivant webhook.
| Event | Fires when |
|---|---|
conference.scheduled | A conference is created (scheduled for later, or start-now). |
conference.rescheduled | A scheduled conference moves to a new time. Carries previousScheduledFor. |
conference.cancelled | A conference is cancelled — by your API call, or because the applicant refused consent. Carries cancellation. |
conference.started | The host starts the session (first join). Once per start — host reconnects don't repeat it. |
conference.completed | The host records the final decision. Carries decision. |
A resend-invitation is not a new conference.scheduled.
There is no no_show event yet. A session that never takes place stays
scheduled and is reported with expired: true once its join window
closes — poll GET /conferences?status=expired to find no-shows.
Payload
Unlike the kyc.request.* events, conference events use a conference-shaped
body. Every event carries the ids, your externalRef, the status and the
timestamps:
{
"event": "conference.completed",
"conferenceId": "cmf…",
"kycRequestId": "req_01J…",
"applicantId": "cma…",
"organizationId": "org_01H…",
"externalRef": "order-10042",
"title": "Account opening — video check",
"status": "completed",
"scheduledFor": "2026-10-06T14:00:00.000Z",
"expiresAt": "2026-10-07T14:00:00.000Z",
"startedAt": "2026-10-06T14:01:12.000Z",
"endedAt": null,
"durationSeconds": null,
"createdAt": "2026-10-01T09:00:00.000Z",
"updatedAt": "2026-10-06T14:13:55.000Z",
"host": { "userId": "usr_01H…", "name": "Sam Reviewer", "email": "compliance@yourcompany.com" },
"consent": {
"screenEnabled": true,
"requiredPurposes": ["share_information", "photo_capture", "video_recording"],
"given": true,
"at": "2026-10-06T14:01:58.000Z"
},
"decision": {
"decision": "rejected",
"outcome": "rejected",
"requestStatus": "REJECTED",
"notes": "Face does not match the ID photo.",
"decidedByUserId": "usr_01H…",
"documentDecisions": { "verified": 0, "rejected": 1, "needsResubmission": 0 }
},
"recording": { "available": false, "deliveryStatus": "pending", "durationSeconds": null, "sha256": null },
"certificate": null,
"timestamp": "2026-10-06T14:13:55.000Z"
}
Event-specific fields:
conference.rescheduled—previousScheduledFor.conference.cancelled—cancellation.reasonandcancellation.cancelledBy(api,host,applicant,system). A refused consent arrives ascancelledBy: "applicant",reason: "applicant_refused_consent".conference.completed—decision(as in the conference object). The session may still be running when the decision is recorded, soendedAtcan benull, and the recording is usually still being assembled (recording.available: false) — readGET /conferences/:idlater for the final recording state.
Payloads never contain media, recording links or join tokens.
Verify the signature
Each delivery is signed with your webhook signing secret. The recommended header
is X-Certivant-Signature-V2: t=<unix>,v1=<hex>, where v1 is the HMAC-SHA256 of
`${t}.${rawBody}`. Reject timestamps more than 300 seconds off and
de-duplicate on X-Certivant-Event-Id. (The older X-KYC-Signature /
X-KYC-Timestamp / X-KYC-Event-Id headers are still sent.)
import { createHmac, timingSafeEqual } from "node:crypto";
export function verifyCertivantWebhook(rawBody: string, headers: Headers, secret: string) {
const parts = (headers.get("X-Certivant-Signature-V2") ?? "").split(",").map((p) => p.split("="));
const t = parts.find(([k]) => k === "t")?.[1] ?? "";
if (!t || Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
return parts.some(
([k, v]) => k === "v1" && v?.length === expected.length && timingSafeEqual(Buffer.from(v), Buffer.from(expected)),
);
}
// In your handler:
const event = JSON.parse(rawBody);
if (event.event === "conference.completed") {
await orders.markIdentityChecked(event.externalRef, event.decision.outcome);
}
Full details and other languages: Signature verification.
Rate limits
These endpoints share your organization's API rate limit with the rest of the v1 API — see Rate limits.
Hosted sessions
Generate a short-lived URL where the applicant completes document capture, liveness, and identity steps.
Blocklist API
Add, list and remove blocklist entries from your own system — email addresses, phone numbers, document numbers and IP addresses or ranges, or the face, devices and IP behind an existing verification.