KYC requests
Complete API reference for Certivant KYC requests — create, list, submit, approve, reject, escalate, and resubmit individual or business identity verification requests.
A KYC request represents a single attempt to verify the identity of one person (INDIVIDUAL) or the identity of a business along with its directors and beneficial owners (BUSINESS). Every endpoint below lives under /api/v1/kyc/requests.
All endpoints require an API key with the relevant scope. See Authentication.
Create a request
POST /api/v1/kyc/requests Scope: kyc:write
Creates a new request in PENDING_UPLOAD state. Applicant data is optional — if you don't pass it, the applicant fills it in via the hosted session.
Request body
{
"type": "INDIVIDUAL",
"purpose": "Financing an iPhone 16, 24 months",
"externalRef": "your-id-here",
"subTenantName": "Acme Corp",
"subTenantExternalId": "cust_8842",
"applicant": {
"externalRef": "your-applicant-id",
"fullName": "Jane Doe",
"dateOfBirth": "1990-04-12",
"nationality": "US",
"email": "jane@example.com",
"phone": "+14155551234",
"emailVerified": false,
"phoneVerified": false,
"metadata": { "anyKey": "anyValue" }
}
}
| Field | Type | Required | Notes |
|---|---|---|---|
type | enum | yes | INDIVIDUAL or BUSINESS |
purpose | string | no | Why you are verifying this person, in a few words — e.g. Financing an iPhone 16, 24 months, Opening an account. 3–300 chars. Stored encrypted. If you send none, Certivant uses the form's default reason (formId), else "New customer verification" (always stored in English). A specific reason is recommended: it makes the fraud check useful. See Reason for the verification |
externalRef | string | no | Your foreign key. Returned on every event |
subTenantName | string | no | Partner mode: display name of the end customer this request belongs to. Max 200 chars |
subTenantExternalId | string | no | Partner mode: your own internal id for that end customer. Filterable on the list endpoint and echoed on every webhook. Max 200 chars |
formId | string | no | Create the verification through one of your forms (its document requirements apply and the request is attributed to it). The form's kind must match type. Required for an API key restricted to specific forms, and it must be one of them — see Form restriction |
faceCheckSkippable | boolean | no | Omitted: the form's setting (formId), else your organization's setting (the face check is required unless you changed it). false forces the face check to be required. 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 then runs, the ID is still collected, and the request goes to manual review with a live ID verification recommended. A form can turn this on with "Let applicants skip the face check"; the organization setting is in Settings → KYC/B. |
applicant.externalRef | string | no | Per-applicant foreign key, separate from the request ref |
applicant.fullName | string | no | If omitted, collected via hosted session |
applicant.dateOfBirth | string (ISO date) | no | YYYY-MM-DD |
applicant.nationality | string (ISO 3166-1 alpha-2) | no | e.g. US, GB, FR |
applicant.email | string | no | |
applicant.phone | string (E.164) | no | |
applicant.emailVerified | boolean | no | Set true if you've already verified the email on your side |
applicant.phoneVerified | boolean | no | Same — skips Certivant's phone verification step |
applicant.metadata | object | no | Arbitrary JSON. Returned on the GET endpoint |
Partner / sub-tenant attribution. If you run verifications for
many end customers inside one organization (e.g. an e-sign platform
verifying signers for hundreds of businesses), set subTenantName
and subTenantExternalId on each request. You can then filter the
list endpoint by subTenantExternalId, both fields are echoed on
every webhook, and the dashboard exposes a sub-tenant filter — all
without standing up a separate organization per customer.
Reason for the verification
Every request has a reason: why you are verifying the person, in a few words. Sending one is optional, but a specific reason is recommended because it makes the fraud check useful. Certivant reads the reason and matches it to one entry of a reason list of about 110 reasons — for example phone or device financing, mobile plan, SIM activation, personal loan, credit card, car rental, crypto, bank account, home rental, mortgage, notary / deed signing, insurance, employment or age verification. Each reason belongs to a family: Phones & devices, Mobile lines & SIM, Loans & credit, Vehicles, High-value goods, Money movement & crypto, Account opening, Home rental, Property & mortgage, Other (no money at risk), or Unclear.
- Only the family is compared. The fraud-prevention network compares the family across companies — never the text, never the specific reason. The text itself is stored encrypted, is never shared with another company, and travels only over HTTPS.
- Only money or credit at risk counts. A request counts toward fraud patterns only when money or credit is at risk. A notary signing, a mortgage, insurance, utilities, employment or healthcare is recorded but never counts: someone who signs a deed at a notary and finances a phone a month later is not flagged.
- Sensitive reasons are never shared. Health, legal, immigration, gambling, debt trouble, dating, religion or donations, death or estate, and age-restricted goods are stored as "Other".
- Be specific. A reason such as
KYCorverificationdoes not say what the person is buying. By default the request is still created, with a warning. If your organization turned on Reject vague reasons (Settings → Fraud prevention → Fraud network rules), it is refused with422instead. - Leave personal details out. E-mail addresses, phone, card and ID numbers and dates are removed before any AI reads the reason — don't put them in it.
The create response tells you how the reason was read:
| Field | Values | Meaning |
|---|---|---|
purposeQuality | informative | The reason says what is being bought, financed or opened. |
generic | Too vague to classify precisely. | |
unusable | Could not be understood. | |
warnings (only when relevant) | purpose_too_generic · purpose_unusable · purpose_contains_personal_data | What to fix in the reason. The last one means personal details were found and removed. |
The same fields come back from POST /api/v1/kyc/forms/:formId/send. For the full picture, see Fraud Network.
The reason is chosen in this order: the purpose you send, then the form's default reason (set it in the form editor), then "New customer verification", always stored in English with exactly that text. Integrations that send no reason, such as e-signature plugins, therefore never get an error. The default reason is never refused (not even with Reject vague reasons), produces no warning, and carries no money at risk, so it does not feed the fraud network's patterns.
Response (201)
{
"id": "req_01J...",
"status": "PENDING_UPLOAD",
"type": "INDIVIDUAL",
"externalRef": "your-id-here",
"subTenantName": "Acme Corp",
"subTenantExternalId": "cust_8842",
"pendingDocTypes": ["GOVERNMENT_ID", "SELFIE"],
"purposeQuality": "informative",
"applicant": { ... },
"createdAt": "2026-05-27T10:00:00.000Z"
}
pendingDocTypes reflects your organization's KYC settings — what's required depends on whether you've enabled liveness, proof of address, etc.
purposeQuality is informative, generic or unusable. When the reason needs your attention, a warnings array is added with purpose_too_generic, purpose_unusable and/or purpose_contains_personal_data.
Errors
400 VALIDATION_ERROR— body failed Zod validation.error.fieldslists each failing path.403 FORBIDDEN— key lackskyc:write, or (error.reason = "form_not_allowed") the key is restricted to specific forms and the request has noformIdor names another form.404 NOT_FOUND—formIdis not a form of your organization.409 CONFLICT— yourexternalRefis already used by another request in this organization. Use a different ref or query for the existing request.422— your organization rejects vague reasons (Reject vague reasons) andpurposeis too generic or unusable. Say what is being bought or financed. Also returned byPOST /api/v1/kyc/forms/:formId/send.
List requests
GET /api/v1/kyc/requests Scope: kyc:read
Query parameters
| Param | Type | Default | Notes |
|---|---|---|---|
status | enum | — | Filter by PENDING_UPLOAD, PROCESSING, APPROVED, REJECTED, ESCALATED, CANCELLED |
type | enum | — | INDIVIDUAL or BUSINESS |
externalRef | string | — | Exact-match filter on your foreign key |
subTenantExternalId | string | — | Partner mode: return only requests tagged with this end-customer id |
search | string | — | Free-text search across applicant name and email |
page | integer | 1 | 1-indexed page number |
limit | integer | 20 | Max 100 |
Response
{
"data": [
{ "id": "req_...", "status": "APPROVED", "type": "INDIVIDUAL", "createdAt": "...", "applicant": {...} },
...
],
"pagination": {
"total": 248,
"page": 1,
"limit": 20,
"pages": 13
}
}
Use externalRef filtering to fetch a request when you only know
your own foreign key — no need to store Certivant's req_… id if
your externalRef is unique.
Get a single request
GET /api/v1/kyc/requests/:id Scope: kyc:read
Returns the complete verification — the applicant, every uploaded document (with the OCR-extracted fields), every automated check, the full risk breakdown, fraud signals, and the audit log. This is everything the dashboard's detail screen renders, so you can build your own review UI and never send staff into Certivant.
Response
{
"id": "req_...",
"status": "ESCALATED",
"type": "INDIVIDUAL",
"externalRef": "user-9281",
"subTenantName": null,
"subTenantExternalId": null,
"reviewedBy": null,
"reviewNote": null,
"completedAt": null,
"createdAt": "2026-05-27T10:00:00.000Z",
"updatedAt": "2026-05-27T10:03:12.000Z",
"applicant": {
"id": "app_...",
"fullName": "Alex Singh",
"dateOfBirth": "1988-11-03T00:00:00.000Z",
"nationality": "GBR",
"email": "alex@example.com",
"emailVerified": true,
"phone": "+447700900100",
"phoneVerified": false,
"metadata": {}
},
"documents": [
{
"id": "doc_...",
"documentType": "id_card",
"side": "front",
"status": "done",
"mimeType": "image/jpeg",
"fileSize": 412938,
"parsedData": {
"fullName": "Alex Singh",
"dateOfBirth": "1988-11-03",
"documentNumber": "D1234567",
"expiryDate": "2030-01-15",
"nationality": "GBR",
"issuingCountry": "GBR",
"address": "12 High St, London"
},
"createdAt": "2026-05-27T10:01:00.000Z"
}
],
"checks": [
{ "id": "chk_1", "checkType": "ocr", "provider": "aws_textract", "status": "passed", "score": 0.95, "startedAt": "...", "finishedAt": "..." },
{ "id": "chk_2", "checkType": "face_match", "provider": "azure_face", "status": "passed", "score": 0.88 },
{ "id": "chk_3", "checkType": "liveness", "provider": "azure_face", "status": "passed", "score": 0.92 },
{ "id": "chk_4", "checkType": "sanctions", "provider": "ofac", "status": "failed", "score": 0.0 }
],
"riskScore": {
"overallScore": 62,
"decision": "escalate",
"autoDecision": true,
"factors": [
{ "factor": "document_authenticity", "impact": 1.0, "description": "Barcode + metadata valid" }
],
"rejectionReasons": [],
"verdictMeta": {
"factorVerdicts": [
{ "id": "1.1", "section": "Identity Document", "label": "Document authenticity", "weight": 15, "state": "pass", "evidence": ["barcode_valid"] },
{ "id": "4.2", "section": "Screening", "label": "Sanctions / PEP", "weight": 20, "state": "fail", "evidence": ["ofac_possible_match"] }
],
"disqualifiers": [],
"reviewTriggers": ["sanctions_possible_match"],
"effectiveThresholdApprove": 80
}
},
"fraudSignals": [
{ "signalType": "device_reuse", "severity": "low", "details": { "matchedFingerprint": "..." } }
],
"auditLogs": [
{ "id": "log_1", "actorType": "api", "actorId": "kyk_prod_ab", "eventType": "request.created", "payload": {}, "createdAt": "..." },
{ "id": "log_2", "actorType": "system", "actorId": "orchestrator", "eventType": "request.escalated", "payload": { "reason": "sanctions" }, "createdAt": "..." }
]
}
Field reference
| Path | Notes |
|---|---|
documents[].parsedData | OCR-extracted fields (vary by document type): fullName, dateOfBirth, documentNumber, expiryDate, nationality, issuingCountry, address. null until OCR runs |
documents[].status | pending · processing · done · failed |
checks[].checkType | ocr · face_match · liveness · sanctions · llm_consistency |
checks[].status | pending · running · passed · failed · skipped · error |
checks[].score | 0.0–1.0 (null for pass/fail-only checks). details carries the raw provider result |
riskScore.overallScore | Composite 0–100 |
riskScore.decision | approve · reject · escalate. autoDecision: false = a human overrode |
riskScore.verdictMeta.factorVerdicts[] | The PASS/FAIL/N-A matrix that explains why the decision landed where it did — state is pass · fail · n/a |
riskScore.verdictMeta.disqualifiers | Tier-1 codes that force a rejection |
riskScore.verdictMeta.reviewTriggers | Tier-2 codes that force escalation |
fraudSignals[] | signalType (same_face · velocity · document_reuse · device_reuse), severity (low · medium · high) |
auditLogs[] | Tamper-evident history; actorType is api · system · user |
Errors
404 NOT_FOUND— no request with that id in this organization. Requests belong to organizations; you cannot read another org's data even with a valid key.
Displaying a verification in your own app
The detail response is designed to be rendered directly — one call gives you everything the dashboard shows, so your reviewers never have to log in to Certivant:
GET /api/v1/kyc/requests/:id → applicant data, documents[].parsedData, checks[], and riskScore.verdictMeta.
For each document, GET /api/v1/kyc/requests/:id/documents/:docId/url → a 15-minute signed URL. Render images with <img src> and PDFs with <iframe> — the URL is presigned and needs no Authorization header. See Documents → Get a download URL.
Show checks[] (pass/fail + score) and riskScore.verdictMeta.factorVerdicts[] so a reviewer sees exactly why a request escalated, then act with POST /requests/:id/decision.
Fetch document URLs on demand, right before you display them. They expire after 15 minutes and must never be logged, cached, or exposed to unauthenticated users.
Submit for processing
POST /api/v1/kyc/requests/:id/submit Scope: kyc:submit
Releases a PENDING_UPLOAD request into the automated processing pipeline. The request must have all required documents uploaded (pendingDocTypes is empty) before you can submit.
If your organization requires email and/or phone verification (KYC/B
Settings), make sure each required channel is verified before you
submit — set applicant.emailVerified / applicant.phoneVerified: true
on create (when you've validated them yourself) or have the applicant
complete the code step in a hosted session. An unverified required
channel fails the contact-verification check during processing.
Response
{
"id": "req_...",
"status": "PROCESSING",
"message": "Request submitted; you will receive a webhook when processing completes."
}
Errors
400 BAD_REQUEST—pendingDocTypesis non-empty. Upload the missing documents first.409 CONFLICT— request is not inPENDING_UPLOAD. You can only submit once.
Manual decision (escalated requests)
POST /api/v1/kyc/requests/:id/decision Scope: kyc:manage
Override an ESCALATED request with a human decision. This endpoint exists for internal tooling — most decisions are made through the dashboard.
Request body
{ "decision": "APPROVED", "note": "Confirmed identity against internal CRM record" }
| Field | Type | Required | Notes |
|---|---|---|---|
decision | enum | yes | APPROVED or REJECTED |
note | string | yes | 1–1000 chars. Stored in the audit log; visible to all reviewers |
Response
{ "id": "req_...", "status": "APPROVED", "decision": "APPROVED", "reviewNote": "..." }
Errors
409 CONFLICT— request is not inESCALATEDstate. You cannot override an auto-approval.
Resubmit a rejected request
POST /api/v1/kyc/requests/:id/resubmit Scope: kyc:write
Resets a REJECTED request to PENDING_UPLOAD so the applicant can retry. Prior documents are cleared; the applicant uploads fresh ones.
Response
{ "id": "req_...", "status": "PENDING_UPLOAD" }
Errors
409 CONFLICT— request is not inREJECTEDstate. Only rejected requests can be resubmitted; cancelled or approved ones cannot.
Resubmit is a privileged operation. Make sure your organization's policy allows the applicant to retry. Some regulated workflows require a fresh request rather than a resubmission so the audit trail clearly shows separate attempts.
Delete a request
DELETE /api/v1/kyc/requests/:id Scope: kyc:write
Permanently deletes the request and everything attached to it: it cascades to the request's documents, checks and risk scores, removes the uploaded ID images/PDFs from storage, and drops the applicant record when no other request references it. Use this to honour a customer's delete request or to purge a test verification.
Response
{
"success": true,
"documentsDeleted": 3,
"applicantDeleted": true
}
documentsDeleted— how many stored files were removed.applicantDeleted— whether the applicant was orphan-cleaned (falseif they still have other requests).
Errors
404 NOT_FOUND— no request with that ID in your organization.
This is irreversible — the uploaded documents are gone. The append-only audit log is retained (its rows are a soft reference), so the record that a request once existed and was deleted survives for compliance. If you only want the applicant's PII erased, use Delete an applicant instead.
Report the outcome
POST /api/v1/kyc/requests/:id/outcome Scope: kyc:write
Optional. Tell Certivant whether the purchase or credit the verification was for was actually delivered or cancelled. Call it once the request has been decided.
Request body
{ "fulfilled": true }
| Field | Type | Required | Notes |
|---|---|---|---|
fulfilled | boolean | yes | true = the purchase or credit was delivered. false = it was cancelled |
Response
{ "id": "req_...", "fulfilled": true, "recorded": true }
recorded: false means the request is not in the fraud network yet (for example, it has no identity keys). Nothing is lost — send the outcome again later if you wish. You can also call the endpoint again at any time to change the answer.
Errors
404 NOT_FOUND— no request with that id in this organization.409 CONFLICT— the request is not decided yet. Its status must beAPPROVED,REJECTED,ESCALATEDorPENDING_SECOND_REVIEW.
Why it helps. In the fraud network a delivered purchase counts fully, while a cancelled one does not count at all. A customer who started three phone financings and cancelled two of them is therefore not mistaken for someone who walked away with three phones. Only the outcome is shared — nothing else leaves your organization.
Get fraud signals
GET /api/v1/kyc/requests/:id/fraud-signals Scope: kyc:read
Returns the device, network, and behavioural fraud signals collected during the verification.
Response
[
{
"signalType": "device_reused",
"severity": "medium",
"details": { "previousRequestIds": ["req_..."], "matchedFingerprint": "..." }
},
{
"signalType": "vpn_detected",
"severity": "low",
"details": { "asn": 13335, "isp": "Cloudflare" }
}
]
Severity is low, medium, or high. High-severity signals automatically escalate the request to manual review.
Ongoing AML monitoring
An approved verification can be re-screened for new sanctions, PEP and adverse-media hits (see Ongoing monitoring). Monitoring is a plan feature and billed per monitored person per year.
| Call | Scope | Effect |
|---|---|---|
GET /api/v1/kyc/requests/:id/monitoring | kyc:read | State: enrolled, status (active, paused, cancelled), frequency, enrolledAt, lastScreenedAt, nextScreenAt, nextBillingAt, openAlerts |
POST /api/v1/kyc/requests/:id/monitoring | kyc:manage | Start. Body { "frequency": "daily" | "weekly" | "monthly" } (default monthly). Returns { "success": true, "frequency": "monthly", "chargedCents": 0 } |
DELETE /api/v1/kyc/requests/:id/monitoring | kyc:manage | Stop |
POST returns 409 for a verification that isn't approved, 403 when your plan doesn't include monitoring and 402 when there is no credit for a person beyond the included quota.
New hits arrive as the kyc.monitoring.alert webhook (see Webhook events): data.monitoring has alertType (sanctions_match, pep_match, adverse_media, sanctions_unverifiable), severity, and for a PEP pepTier — 1 heads of state and ministers … 4 relatives and close associates — or adverseMedia: true. data.sanctions.listsChecked names the lists screened; the screening vendors are never named.
The verification itself carries the same pepTier / adverseMedia under data.sanctions in its decision webhook.
Delete an applicant (GDPR erasure)
DELETE /api/v1/kyc/applicants/:applicantId Scope: kyc:manage
Permanently deletes an applicant and every request linked to them. This is the right-to-erasure endpoint under GDPR Article 17. The audit log preserves a tombstone with the deletion timestamp but no personal data.
Response
{ "success": true, "deletedRequestIds": ["req_...", "req_..."] }
Erasure is irreversible. Confirm with your compliance team before calling this endpoint — depending on your jurisdiction, you may have a regulatory obligation to retain verification records for 5–7 years, which conflicts with deletion.
Forms
List a tenant's KYC and KYB verification forms via the Certivant REST API — discover which form to run a verification against, filter by type, and read each form's slug, status, and embeddable flag.
KYB requests
Certivant KYB API reference — attach company details, directors, and ultimate beneficial owners to a BUSINESS request, trigger registry lookups, and manage per-officer KYC sub-requests.