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)
  1. 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.
  2. Certivant invites the applicant (email and/or SMS, in their language) and returns the join link to you as well.
  3. 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: false when you already collected those consents in your own app.
  4. When the host records the decision you receive conference.completed with the decision, its effect on the verification and your externalRef.

Before you start

RequirementWhere
API key with kyc:write (schedule / change) and kyc:read (read)KYC → Developer
Your plan includes web conferencingYour subscription. Otherwise 402
Live ID Verification enabled in your settingsKYC → Settings → Live ID Verification. Otherwise 403
Your account is verified by CertivantOnboarding. Otherwise 403
The host has manual-approval permissionThe 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 }
}
FieldTypeRequiredNotes
kycRequestIdstringone ofAn existing verification request in your organization.
applicantobjectone ofCreate the applicant + verification inline instead (see below). An applicant with the same email in your organization is reused.
host.userId / host.emailstringyes (one)The member who runs the session. Must have manual-approval permission.
externalRefstringnoYour reference code (order / case id). Echoed on every response and every webhook. Max 200.
titlestringnoShown to the host. Max 200.
scheduledForISO date-timeone ofWhen the session takes place. Must be in the future.
startNowbooleanone oftrue = schedule for right now (the applicant is invited immediately).
expiresAtISO date-timenoEnd of the join window. Default: scheduledFor + 24 h.
languageenumnoen, 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.
consentScreenbooleannoDefault 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.
faceCheckSkippablebooleannoOmitted: 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.
applicantFileUploadEnabledbooleannoDefault 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.smsbooleannoHow 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

StatusWhen
400Validation 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.
402Your plan doesn't include live verification.
403Key lacks kyc:write; account pending verification; live verification disabled in settings; or the host lacks manual-approval permission.
404kycRequestId doesn't exist in your organization.
409The 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

QueryNotes
statusscheduled, 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.
externalRefExact match on your reference code.
kycRequestIdConferences of one verification.
scheduledFrom / scheduledToISO date-times, inclusive, on scheduledFor.
createdFrom / createdToISO date-times, inclusive, on creation time.
page / limitDefault 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
}
FieldNotes
statusscheduled → in_progress (the host joined) → completed (decision recorded or session ended), or cancelled.
expiredtrue when a scheduled session's join window closed without the session taking place (no-show). Filter with status=expired.
consent.givenThe primary applicant's answer: null until they answer, false if they refused (the session is then cancelled). requiredPurposes always contains video_recording.
decision.decisionThe host's verdict: verified, rejected, needs_more_info or inconclusive. null until decided.
decision.outcomeWhat it did to the verification: approved, rejected, or escalated (needs_more_info / inconclusive park it for review). requestStatus is the verification's status.
recording.availabletrue once the recording is assembled and stored or delivered to your storage. Media itself is never exposed by this API.
certificateThe 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.

EventFires when
conference.scheduledA conference is created (scheduled for later, or start-now).
conference.rescheduledA scheduled conference moves to a new time. Carries previousScheduledFor.
conference.cancelledA conference is cancelled — by your API call, or because the applicant refused consent. Carries cancellation.
conference.startedThe host starts the session (first join). Once per start — host reconnects don't repeat it.
conference.completedThe 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.reason and cancellation.cancelledBy (api, host, applicant, system). A refused consent arrives as cancelledBy: "applicant", reason: "applicant_refused_consent".
  • conference.completed — decision (as in the conference object). The session may still be running when the decision is recorded, so endedAt can be null, and the recording is usually still being assembled (recording.available: false) — read GET /conferences/:id later 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.