API referenceKYC requests

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" }
  }
}
FieldTypeRequiredNotes
typeenumyesINDIVIDUAL or BUSINESS
purposestringnoWhy 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
externalRefstringnoYour foreign key. Returned on every event
subTenantNamestringnoPartner mode: display name of the end customer this request belongs to. Max 200 chars
subTenantExternalIdstringnoPartner mode: your own internal id for that end customer. Filterable on the list endpoint and echoed on every webhook. Max 200 chars
formIdstringnoCreate 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
faceCheckSkippablebooleannoOmitted: 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.externalRefstringnoPer-applicant foreign key, separate from the request ref
applicant.fullNamestringnoIf omitted, collected via hosted session
applicant.dateOfBirthstring (ISO date)noYYYY-MM-DD
applicant.nationalitystring (ISO 3166-1 alpha-2)noe.g. US, GB, FR
applicant.emailstringno
applicant.phonestring (E.164)no
applicant.emailVerifiedbooleannoSet true if you've already verified the email on your side
applicant.phoneVerifiedbooleannoSame — skips Certivant's phone verification step
applicant.metadataobjectnoArbitrary 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 KYC or verification does 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 with 422 instead.
  • 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:

FieldValuesMeaning
purposeQualityinformativeThe reason says what is being bought, financed or opened.
genericToo vague to classify precisely.
unusableCould not be understood.
warnings (only when relevant)purpose_too_generic · purpose_unusable · purpose_contains_personal_dataWhat 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.fields lists each failing path.
  • 403 FORBIDDEN — key lacks kyc:write, or (error.reason = "form_not_allowed") the key is restricted to specific forms and the request has no formId or names another form.
  • 404 NOT_FOUND — formId is not a form of your organization.
  • 409 CONFLICT — your externalRef is 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) and purpose is too generic or unusable. Say what is being bought or financed. Also returned by POST /api/v1/kyc/forms/:formId/send.

List requests

GET /api/v1/kyc/requests    Scope: kyc:read

Query parameters

ParamTypeDefaultNotes
statusenum—Filter by PENDING_UPLOAD, PROCESSING, APPROVED, REJECTED, ESCALATED, CANCELLED
typeenum—INDIVIDUAL or BUSINESS
externalRefstring—Exact-match filter on your foreign key
subTenantExternalIdstring—Partner mode: return only requests tagged with this end-customer id
searchstring—Free-text search across applicant name and email
pageinteger11-indexed page number
limitinteger20Max 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

PathNotes
documents[].parsedDataOCR-extracted fields (vary by document type): fullName, dateOfBirth, documentNumber, expiryDate, nationality, issuingCountry, address. null until OCR runs
documents[].statuspending · processing · done · failed
checks[].checkTypeocr · face_match · liveness · sanctions · llm_consistency
checks[].statuspending · running · passed · failed · skipped · error
checks[].score0.0–1.0 (null for pass/fail-only checks). details carries the raw provider result
riskScore.overallScoreComposite 0–100
riskScore.decisionapprove · 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.disqualifiersTier-1 codes that force a rejection
riskScore.verdictMeta.reviewTriggersTier-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 — pendingDocTypes is non-empty. Upload the missing documents first.
  • 409 CONFLICT — request is not in PENDING_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" }
FieldTypeRequiredNotes
decisionenumyesAPPROVED or REJECTED
notestringyes1–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 in ESCALATED state. 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 in REJECTED state. 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 (false if 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 }
FieldTypeRequiredNotes
fulfilledbooleanyestrue = 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 be APPROVED, REJECTED, ESCALATED or PENDING_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.

CallScopeEffect
GET /api/v1/kyc/requests/:id/monitoringkyc:readState: enrolled, status (active, paused, cancelled), frequency, enrolledAt, lastScreenedAt, nextScreenAt, nextBillingAt, openAlerts
POST /api/v1/kyc/requests/:id/monitoringkyc:manageStart. Body { "frequency": "daily" | "weekly" | "monthly" } (default monthly). Returns { "success": true, "frequency": "monthly", "chargedCents": 0 }
DELETE /api/v1/kyc/requests/:id/monitoringkyc:manageStop

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.