Skip to content

Company Verification (KYB)

KYB (Know Your Business) verifies a company as an entity: its registration documents, an authorized signatory's identity, and an AML/sanctions screen of the registered name. It runs on a separate set of endpoints from individual KYC (/v1/verify, /v1/sessions/*) but shares the same authentication, webhook delivery, and review model.

How it works

A company verification aggregates three independent checks into one decision:

  1. Documents — every document your country's requirements matrix marks required must be uploaded and accepted (registration certificate, proof of address, etc.).
  2. Signatory identity — the authorized signatory completes a normal individual KYC check (ID + selfie) through a hosted link you send them.
  3. AML screening — the registered company name is screened for sanctions/PEP/watchlist hits the moment the verification is created.

The terminal decision is computed, not asserted: REJECT dominates NEEDS_REVIEW dominates APPROVED. Any required document rejected, or the signatory rejected, rejects the whole verification. Anything still incomplete (a document not yet accepted, the signatory not yet done) parks the verification in NEEDS_REVIEW rather than guessing.

Create → AML screen (name) + resolve required documents

   ├── Upload documents (you, or the signatory via the hosted link)
   ├── Signatory completes ID + selfie via verifyUrl

   └── Every required doc accepted + signatory approved + AML clear
          → status: COMPLETED, decision: APPROVED
       Any required doc rejected, or signatory rejected
          → status: COMPLETED, decision: REJECTED
       Still incomplete, or an AML hit
          → status: NEEDS_REVIEW

NEEDS_REVIEW verifications resolve automatically as documents get accepted and the signatory finishes — or an operator can manually review them. Either path fires the verification.completed webhook once the outcome is terminal.


1. Create a Company Verification

POST /v1/company-verifications

Headers:

HeaderTypeRequiredDescription
X-API-KeystringYesYour API key
Content-TypestringYesapplication/json
Idempotency-KeystringNoRepeat the same key to safely retry without creating a duplicate — see Idempotent replay

Request Body:

json
{
  "country": "SZ",
  "registeredName": "Acme Trading (Pty) Ltd",
  "registrationNumber": "REG-2019-004521",
  "registeredAddress": "12 Somhlolo Road, Mbabane",
  "taxId": "TIN-882910",
  "incorporationDate": "2019-03-14T00:00:00.000Z",
  "externalRef": "biz_acme_001"
}
FieldTypeRequiredDescription
countrystringYesISO 3166-1 alpha-2 country code. Must have an active requirements matrix (see Supported countries) — an unsupported country fails loudly rather than silently skipping requirements.
registeredNamestringYesLegal/registered company name. This is what gets AML-screened.
registrationNumberstringNoCompany registration number
registeredAddressstringNoRegistered office address
taxIdstringNoTax identification number
incorporationDatestringNoISO 8601 date. Affects which documents are required — see Required Documents
externalRefstringNoYour internal reference ID

Response 202 Accepted:

json
{
  "success": true,
  "data": {
    "verificationId": "cv_a1b2c3d4e5f6g7h8i9j0k1l2",
    "status": "pending",
    "requiredDocuments": [
      { "docType": "company_registration", "label": "Certificate of Incorporation / Registration", "required": true, "sortOrder": 10 },
      { "docType": "form_j", "label": "Form J — Return of Directors & Officers", "required": true, "sortOrder": 20 },
      { "docType": "form_c", "label": "Form C — Notice of Registered Office & Postal Address", "required": true, "sortOrder": 30 },
      { "docType": "trading_license", "label": "Municipal trading license", "required": true, "sortOrder": 40 },
      { "docType": "proof_of_address", "label": "Utility bill / lease (≤3 months)", "required": true, "sortOrder": 50 },
      { "docType": "director_id", "label": "Authorized signatory ID", "required": true, "sortOrder": 60 },
      { "docType": "tax_clearance", "label": "SRA tax clearance / TIN", "required": false, "sortOrder": 70 }
    ],
    "signatorySessionToken": "eyJhbGciOiJIUzI1NiIs...",
    "verifyUrl": "https://verify.yeboverify.com#session=eyJhbGciOiJIUzI1NiIs..."
  }
}
FieldTypeDescription
verificationIdstringUnique company verification ID (prefixed cv_)
statusstringpending immediately after creation
requiredDocumentsarrayThe resolved document checklist for this company — see Required Documents
signatorySessionTokenstringSession token for the signatory's individual identity check. Same shape as a normal /v1/sessions/create token.
verifyUrlstringSend the authorized signatory here to complete their ID + selfie check.

verifyUrl carries the session token in the URL fragment

#session=..., not ?session=.... The token is deliberately kept out of the query string so it never reaches your server logs, analytics, or the Referer header of any page the signatory navigates to next. If you're constructing this URL yourself (rather than using the one returned in the response), preserve the #.

Idempotent replay

Pass the same Idempotency-Key header on a retried request (e.g. after a network timeout) and you get back the existing verification instead of a duplicate — no second AML screen, no second billed usage event. The response is 200 OK instead of 202 Accepted, and every field (including a freshly-derived signatorySessionToken and verifyUrl, since session tokens expire) reflects the original record's current state.

Errors

CodeHTTP StatusDescription
KYB_COUNTRY_UNSUPPORTED400No active requirements matrix exists for country yet
VALIDATION_ERROR400Request body failed validation (e.g. country isn't a 2-letter code)
NOT_AUTHENTICATED401Missing/invalid X-API-Key

A 402 (quota exceeded) is possible too — a company verification is metered as one billable company_verification usage event, separate from the signatory's individual KYC event.


Required Documents

Which documents are required varies by country and, for some document types, by how old the company is. createCompanyVerification's response already gives you the resolved checklist for this specific company — you don't need to look up the matrix separately.

Supported countries

Today Eswatini (SZ) is the only seeded country. Requesting any other country returns KYB_COUNTRY_UNSUPPORTED. Reach out if you need another country enabled — the matrix is per-country configurable, not hardcoded.

Eswatini (SZ) document set

docTypeLabelRequired
company_registrationCertificate of Incorporation / RegistrationAlways
form_jForm J — Return of Directors & OfficersAlways
form_cForm C — Notice of Registered Office & Postal AddressOnly if the company is ≥1 year old (unknown incorporation date defaults to required)
trading_licenseMunicipal trading licenseAlways
proof_of_addressUtility bill / lease (≤3 months)Always
director_idAuthorized signatory IDAlways
tax_clearanceSRA tax clearance / TINOptional

The form_c condition is the one exception to "always required": pass incorporationDate on create so a genuinely young company isn't blocked waiting on a document it may not have yet. If you omit incorporationDate, it's treated as required (unknown age never waives a requirement).


Uploading Documents

Documents can come from either side of the relationship — whichever fits your flow:

From your backend (API key auth)

POST /v1/company-verifications/:verificationId/documents

Headers:

HeaderTypeRequiredDescription
X-API-KeystringYesYour API key
Content-TypestringYesmultipart/form-data

Request Body (multipart/form-data):

FieldTypeRequiredDescription
documentfileYesThe document file (JPEG, PNG, or PDF, max 10MB)
docTypestringYesOne of the docType values from requiredDocuments (e.g. company_registration)

Response 201 Created:

json
{
  "success": true,
  "data": {
    "documentId": "clxxxxxxxxxxxx",
    "docType": "company_registration",
    "status": "uploaded"
  }
}

From the hosted signatory flow (session auth)

When the signatory is completing documents through the verifyUrl link (e.g. a hosted upload step in verify-app alongside their ID + selfie), use the session-authenticated counterpart instead of your API key:

POST /v1/sessions/company-documents

Request Body (multipart/form-data):

FieldTypeRequiredDescription
sessionstringYesThe signatorySessionToken (or the token from the verifyUrl fragment)
docTypestringYesOne of the required docType values
documentfileYesThe document file

Response:

json
{
  "success": true,
  "docType": "company_registration",
  "status": "uploaded"
}

Both paths land the document in the same place — a document uploaded by the signatory is immediately visible to you via GET /v1/company-verifications/:verificationId. Uploaded documents start in uploaded status; they only count toward the requiredDocuments checklist once an operator marks them accepted via review.


The Signatory Identity Check

Every company verification has exactly one linked individual verification: the authorized signatory's ID + selfie check, run through the same hosted flow individual KYC uses (see Mobile Integration and Getting Started).

  • Send the signatory to the verifyUrl from the create response (or from GET /v1/company-verifications/:verificationId if they need it again).
  • They complete the normal ID + selfie capture in their browser — nothing KYB-specific from their side.
  • When their individual verification completes, it's automatically linked back to the company verification and the aggregate decision recomputes — you don't call anything to "attach" it.
  • The linked result surfaces on the company verification as signatoryVerificationId and, once decided, on the verification.completed webhook's company.signatoryDecision.

If the signatory abandons the flow or their individual check ends in NEEDS_REVIEW, the company verification stays NEEDS_REVIEW too — it can't reach a terminal decision without a resolved signatory.


Reading Verifications

Get a Company Verification

GET /v1/company-verifications/:verificationId

Response:

json
{
  "success": true,
  "data": {
    "id": "clxxxxxxxxxxxx",
    "verificationId": "cv_a1b2c3d4e5f6g7h8i9j0k1l2",
    "externalRef": "biz_acme_001",
    "country": "SZ",
    "registeredName": "Acme Trading (Pty) Ltd",
    "registrationNumber": "REG-2019-004521",
    "status": "NEEDS_REVIEW",
    "decision": null,
    "decisionReason": null,
    "amlDecision": "clear",
    "amlScore": 0,
    "signatoryVerificationId": "vrf_id_xyz789abc",
    "documents": [
      {
        "id": "clyyyyyyyyyyyy",
        "docType": "company_registration",
        "status": "uploaded",
        "signedUrl": "https://cdn.yeboverify.com/company-verifications/...",
        "createdAt": "2026-05-08T12:00:00.000Z"
      }
    ],
    "reviewHistory": [],
    "webhookSent": false,
    "createdAt": "2026-05-08T12:00:00.000Z",
    "completedAt": null
  }
}

documents[].signedUrl is a time-limited (1 hour) signed URL to view the uploaded file — re-fetch this endpoint if it expires. reviewHistory is the append-only audit trail of every review decision applied to this verification (mirrors the individual KYC audit trail).

List Company Verifications

GET /v1/company-verifications

Query Parameters:

ParameterTypeDefaultDescription
pagenumber1Page number
limitnumber20Results per page (max 100)
statusstringFilter by PENDING, PROCESSING, COMPLETED, FAILED, NEEDS_REVIEW

Response:

json
{
  "success": true,
  "data": {
    "items": [
      {
        "verificationId": "cv_a1b2c3d4e5f6g7h8i9j0k1l2",
        "registeredName": "Acme Trading (Pty) Ltd",
        "status": "NEEDS_REVIEW",
        "decision": null,
        "createdAt": "2026-05-08T12:00:00.000Z"
      }
    ],
    "total": 1,
    "page": 1,
    "limit": 20
  }
}

Manually Review a Verification

Operators (or your own back office, via the API key) resolve a NEEDS_REVIEW company verification by accepting/rejecting each uploaded document. The terminal decision is then recomputed from the aggregate — you never set decision directly, the same way individual KYC review works.

POST /v1/company-verifications/:verificationId/review

Request Body:

json
{
  "documentReviews": [
    { "documentId": "clyyyyyyyyyyyy", "status": "accepted" },
    { "documentId": "clzzzzzzzzzzzz", "status": "rejected", "note": "Certificate is expired" }
  ]
}
FieldTypeRequiredDescription
documentReviewsarrayYesAt least one verdict
documentReviews[].documentIdstringYesA document ID belonging to this verification
documentReviews[].statusstringYesaccepted or rejected
documentReviews[].notestringNoReviewer note, max 1000 chars

Reviewer identity is derived entirely from the authenticated API key (name/prefix) — there's no reviewedBy free-text field to spoof.

Response:

json
{
  "success": true,
  "data": {
    "verificationId": "cv_a1b2c3d4e5f6g7h8i9j0k1l2",
    "status": "COMPLETED",
    "decision": "REJECTED"
  }
}

Errors:

CodeHTTP StatusDescription
NOT_FOUND404Verification doesn't exist for your business
INVALID_DOCUMENT400One or more documentIds don't belong to this verification
NOT_REVIEWABLE409Verification isn't PENDING/NEEDS_REVIEW (already terminal, or a concurrent review won the race)

A review doesn't have to immediately produce a terminal decision — if you accept some documents but the signatory hasn't finished yet, the verification stays NEEDS_REVIEW and will recompute again automatically once the signatory result lands.


Resend Webhook

If your endpoint missed the verification.completed delivery for a terminal company verification, re-trigger it manually:

POST /v1/company-verifications/:verificationId/resend-webhook

Returns 404 if not found for your business, 409 if the verification hasn't reached a terminal decision yet, 400 if you don't have a webhookUrl configured (see Webhooks).


Webhook Payload

KYB delivers through the exact same verification.completed webhook individual KYC uses — same signing, same retry policy, same X-YeboVerify-Signature-V2 scheme — distinguished by a type: "company" field and a company block instead of the person-specific fields (faceScore, ocrConfidence, extractedData).

json
{
  "event": "verification.completed",
  "eventId": "evt_1a2b3c4d5e6f7a8b9c0d1e2f",
  "type": "company",
  "verificationId": "cv_a1b2c3d4e5f6g7h8i9j0k1l2",
  "externalRef": "biz_acme_001",
  "status": "completed",
  "decision": "approved",
  "confidence": "high",
  "aml": {
    "decision": "clear",
    "score": 0,
    "hits": []
  },
  "company": {
    "registeredName": "Acme Trading (Pty) Ltd",
    "registrationNumber": "REG-2019-004521",
    "country": "SZ",
    "signatoryVerificationId": "vrf_id_xyz789abc",
    "signatoryDecision": "approved",
    "documents": [
      { "docType": "company_registration", "status": "accepted" },
      { "docType": "form_j", "status": "accepted" }
    ]
  },
  "timestamp": "2026-05-08T12:05:00.000Z"
}
FieldTypeDescription
typestring"company" — the KYB discriminator. Individual KYC payloads send type: "individual"; switch on it before assuming person-shaped fields exist.
company.registeredName / registrationNumber / countrystringEchoes what you submitted at creation
company.signatoryVerificationIdstring | nullThe linked individual verification ID
company.signatoryDecisionstring | nullapproved, rejected, needs_review, or null if the signatory hasn't finished
company.documentsarrayEvery uploaded document's final docType/status
amlobjectSame AML block individual payloads carry — see Webhooks → AML

See Webhooks for signature verification (X-YeboVerify-Signature-V2), retry policy, and eventId-based dedupe — all of it applies identically to type: "company" deliveries.


End-to-End Example

bash
# 1. Create the company verification
curl -X POST https://api.yeboverify.com/v1/company-verifications \
  -H "X-API-Key: your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "country": "SZ",
    "registeredName": "Acme Trading (Pty) Ltd",
    "registrationNumber": "REG-2019-004521",
    "incorporationDate": "2019-03-14T00:00:00.000Z",
    "externalRef": "biz_acme_001"
  }'
# → { verificationId: "cv_...", requiredDocuments: [...], verifyUrl: "https://verify.yeboverify.com#session=..." }

# 2. Upload the documents you already have on file
curl -X POST https://api.yeboverify.com/v1/company-verifications/cv_.../documents \
  -H "X-API-Key: your_api_key" \
  -F "docType=company_registration" \
  -F "document=@registration_certificate.pdf"

# 3. Send the signatory the verifyUrl to complete their ID + selfie check
#    (they do this in a browser — nothing further to call here)

# 4. Once documents are uploaded, accept/reject them
curl -X POST https://api.yeboverify.com/v1/company-verifications/cv_.../review \
  -H "X-API-Key: your_api_key" \
  -H "Content-Type: application/json" \
  -d '{ "documentReviews": [{ "documentId": "cl...", "status": "accepted" }] }'

# 5. Listen for verification.completed on your webhook, or poll:
curl https://api.yeboverify.com/v1/company-verifications/cv_... \
  -H "X-API-Key: your_api_key"

Identity Verification API for Africa