Skip to Content
KnowaKYC
KYB API

KYB webhooks

The KYB webhook system delivers business verification event notifications to your server in real time. The delivery mechanism mirrors KYC webhooks: HMAC-SHA256 signing, exponential backoff between retries, and idempotency support.

Key properties:

  • 12 event types covering the full KYB verification lifecycle
  • The kyb.business.verified, kyb.business.rejected, and kyb.business.review_required events carry the full business data in the data field (matching the GET /v1/kyb/businesses/{id} API response): legal form, directors, beneficial owners with UBO fields, the KYB input profile (email, phone, trade_name, business_type, and related profile fields), rejection reasons, last verification timestamp, and other fields
  • HMAC-SHA256 signing for delivery integrity (the X-Onekyc-Signature header)
  • Automatic retries with exponential backoff (up to 5 attempts)
  • SHA-256 idempotency keys to prevent duplicate processing
  • Delivery log in the Webhooks section of the admin panel

You configure endpoints in the Webhooks section of the admin panel (the same section as KYC). When creating or editing an endpoint, choose the KYB events category and pick the event types you need (or select "All events" to also receive KYB events).


Interpreting the decision

  • Company data, company_status: active, and kyb.step.completed do not establish successful KYB verification.
  • For kyb.business.*, use data.verification_status: verified, rejected, or review. Review requires a reviewer decision and is not approval.
  • For kyb.session.completed, use data.result: approved means approval; rejected or declined means rejection. status describes the process. Historical rejections can carry status: failed; a completed process can have status: completed for either final decision.
  • decision_source distinguishes automatic from manual_review. Manual decisions include decision_id and, when available, decision_reason_codes. Missing fields on historical events do not establish automatic approval.
  • A manual approval does not rewrite individual check results. Inspect verifications, nested KYB evidence, beneficiary_checks, and decision_reasons when registry coverage, signature findings or other signals matter to your integration. ai_confirmation is not the final decision.
  • business_is_current_session: false means this is a historical session: do not use its approval to replace the current company decision. An absent flag means this relationship was not established. Use kyb.business.* events for changes to the current business verdict.
  • The terminal event's nested business.verification_status describes this session's decision; underlying lookup results remain available as evidence. Manual rejection codes are distinct from technical check findings.

The decision, KYB business projection and required events are persisted in one transaction. Event-persistence failures return an operation error; HTTP delivery remains asynchronous through the outbox. A 2xx response confirms your server accepted the request, not that its business handling was correct.

Retries can cause repeated or out-of-order deliveries. Use the event ID, session ID and decision timestamp. Each subscribed endpoint receives its own delivery of the same event; matching data does not establish two decisions. Completing a reopened session produces a new event even if its final status is unchanged.

Event list

EventDescription
kyb.session.createdKYB session created (the user opened the verification link)
kyb.session.startedKYB session started (the user began entering data)
kyb.session.completedFinal KYB session decision (approval or rejection)
kyb.session.expiredKYB session expired
kyb.session.cancelledKYB session cancelled
kyb.business.verifiedBusiness successfully verified
kyb.business.rejectedBusiness rejected
kyb.business.review_requiredBusiness routed to manual review by an operator
kyb.step.startedVerification step started (for example, sending the AI analysis request)
kyb.step.completedVerification step completed (for example, AI analysis)
kyb.step.failedVerification step finished with an error
kyb.step.skippedVerification step skipped because an upstream dependency did not pass

Payload format

Every KYB event uses a single envelope format, identical to KYC:

{
  "id": "evt_uuid",
  "type": "kyb.business.verified",
  "api_version": "2026-02-01",
  "created_at": "2026-02-24T10:05:00Z",
  "tenant_id": "tenant_uuid",
  "data": { ... }
}

Example: kyb.business.verified

The data field contains a full snapshot of the business (matching the field set returned by GET /v1/kyb/businesses/{id}), including the profile block with fields entered during the KYB flow:

{
  "id": "evt_kyb_biz_001",
  "type": "kyb.business.verified",
  "api_version": "2026-02-01",
  "created_at": "2026-02-24T10:05:00Z",
  "tenant_id": "tenant_uuid",
  "data": {
    "business_id": "biz_uuid",
    "external_id": "company-001",
    "session_id": "sess_uuid",
    "last_session_id": "sess_uuid",
    "company_name": "Acme Corp Ltd",
    "registration_number": "KZ123456789",
    "tax_number": "123456789012",
    "country": "KZ",
    "legal_form": "TOO",
    "company_status": "active",
    "status_date": "2015-06-15",
    "registration_date": "2015-06-15",
    "legal_address": "Almaty, Primernaya St. 1",
    "actual_address": "Almaty, Primernaya St. 1",
    "main_url": "https://acmecorp.kz",
    "profile": {
      "email": "compliance@acmecorp.kz",
      "phone": "+77271234567",
      "trade_name": "Acme",
      "website": "https://acmecorp.kz",
      "business_type": "b2b",
      "business_activity": "Software development",
      "ownership_structure": "Private company"
    },
    "director_name": "Ivanov Ivan Ivanovich",
    "director_title": "CEO",
    "directors": [{ "name": "Ivanov Ivan Ivanovich", "title": "CEO" }],
    "beneficiaries": [{ "name": "Petrov P.P.", "share": "60%", "type": "natural_person", "country": "KZ", "pep": false }],
    "ai_confirmation": true,
    "ai_summary": "Company has been operating since 2015, no adverse findings.",
    "ai_sources": ["gosreestr.kz", "kgd.gov.kz"],
    "status_evidence_sources": ["gosreestr.kz"],
    "risk_score": 8.5,
    "rejection_reasons": [],
    "last_verified_at": "2026-02-24T10:05:00Z",
    "verification_status": "verified",
    "business_document_key": "business-docs/tenant_uuid/biz_uuid/charter.pdf",
    "created_at": "2026-02-24T09:00:00Z",
    "updated_at": "2026-02-24T10:05:00Z"
  }
}

data field reference:

FieldTypeDescription
business_idstringUUID of the business in KnowaKYC
external_idstringExternal identifier of the business in your system
session_idstringUUID of the current verification session
last_session_idstringUUID of the latest session (same as session_id for the first verification)
company_namestringCompany name
registration_numberstringCompany registration number
tax_numberstringINN / BIN
countrystringISO country code of registration
legal_formstringLegal form
company_statusstringCompany status in the state register (active, liquidating, liquidated, etc.)
status_datestringDate the current status was assigned
status_infostringAdditional status information
liquidation_datestringLiquidation date (when applicable)
legal_addressstringLegal address
actual_addressstringOperating address
main_urlstringOfficial website address
profileobjectCompany profile fields entered during the KYB flow (email, phone, trade_name, website, business_type, activity fields, ownership structure, identifiers)
registration_datestringState registration date
director_namestringDirector's full name
director_titlestringDirector's title
directorsarrayList of directors [{name, title}]
beneficiariesarrayList of beneficial owners [{name, share, type, country, pep}]
activitiesobjectActivity types {main, additional}
ai_confirmationbooleanAI confirmation result
ai_summarystringShort AI analysis summary
ai_notesstringAdditional AI analysis notes
ai_sourcesarraySources used during AI analysis
director_unavailable_reason_infostringReason director data is unavailable
beneficiaries_unavailable_reason_infostringReason beneficial owner data is unavailable
activities_unavailable_reason_infostringReason activity data is unavailable
main_url_unavailable_reason_infostringReason website information is unavailable
status_evidence_sourcesarraySources for the company status data
director_evidence_sourcesarraySources for the director data
beneficiaries_evidence_sourcesarraySources for the beneficial owner data
activities_evidence_sourcesarraySources for the activity data
main_url_evidence_sourcesarraySources for the website data
risk_scorenumberRisk score (0–100)
rejection_reasonsarrayRejection reasons (for the rejected status)
last_verified_atstringLast verification timestamp (RFC 3339)
verification_statusstringVerification status: verified, rejected, review, pending
business_document_keystringBusiness document key in object storage
metadataobjectAdditional metadata
created_atstringRecord creation timestamp (RFC 3339)
updated_atstringRecord update timestamp (RFC 3339)

Fields with empty or zero values may be absent from the payload.


Example: kyb.business.rejected

The data field carries the full business data, including the rejection_reasons array describing the rejection reasons:

{
  "id": "evt_kyb_biz_002",
  "type": "kyb.business.rejected",
  "api_version": "2026-02-01",
  "created_at": "2026-02-24T10:08:00Z",
  "tenant_id": "tenant_uuid",
  "data": {
    "business_id": "biz_uuid",
    "external_id": "company-002",
    "session_id": "sess_uuid_2",
    "company_name": "Shell Corp",
    "registration_number": "XX999999",
    "country": "XX",
    "legal_form": "LLC",
    "risk_score": 72.3,
    "ai_confirmation": false,
    "rejection_reasons": [
      "company_not_found_in_registry",
      "high_risk_jurisdiction"
    ],
    "verification_status": "rejected",
    "created_at": "2026-02-24T09:00:00Z",
    "updated_at": "2026-02-24T10:08:00Z"
  }
}

Example: kyb.business.review_required

The data field carries the full business data (including directors, beneficiaries, legal_form, and the rest):

{
  "id": "evt_kyb_biz_003",
  "type": "kyb.business.review_required",
  "api_version": "2026-02-01",
  "created_at": "2026-02-24T10:10:00Z",
  "tenant_id": "tenant_uuid",
  "data": {
    "business_id": "biz_uuid",
    "external_id": "company-003",
    "session_id": "sess_uuid_3",
    "company_name": "Ambiguous LLC",
    "registration_number": "KZ111222333",
    "country": "KZ",
    "legal_form": "AO",
    "directors": [{ "name": "Sidorov S.S.", "title": "Director" }],
    "beneficiaries": [{ "name": "OOO Holding", "share": "100%", "type": "legal_entity" }],
    "risk_score": 45.0,
    "ai_confirmation": false,
    "verification_status": "review",
    "created_at": "2026-02-24T09:00:00Z",
    "updated_at": "2026-02-24T10:10:00Z"
  }
}

Example: kyb.session.completed

{
  "id": "evt_kyb_sess_001",
  "type": "kyb.session.completed",
  "api_version": "2026-02-01",
  "created_at": "2026-02-24T10:04:00Z",
  "tenant_id": "tenant_uuid",
  "data": {
    "session_id": "sess_uuid",
    "flow_id": "flow_uuid",
    "external_id": "company-001",
    "status": "completed",
    "scope": "kyb",
    "result": "approved",
    "business_id": "biz_uuid",
    "completed_at": "2026-02-24T10:04:00Z"
  }
}

Signature verification

The mechanism is identical to KYC webhooks. Every request carries the header:

X-Onekyc-Signature: t=<unix_timestamp>,v1=<hex_signature>

During secret rotation, v2=<new_signature> is added. For the full description, see KYC webhooks.

Signature verification algorithm:

const crypto = require('crypto');

function verifyWebhook(rawBody, signatureHeader, secret) {
  const parts = signatureHeader.split(',');
  const timestamp = parts.find(p => p.startsWith('t=')).split('=')[1];
  const v1Signature = parts.find(p => p.startsWith('v1=')).split('=')[1];

  // Verify the timestamp is fresh (≤ 5 minutes old)
  if (Math.floor(Date.now() / 1000) - parseInt(timestamp) > 300) {
    throw new Error('Webhook timestamp is too old');
  }

  // Build the signed string: timestamp.raw_body
  const signedPayload = `${timestamp}.${rawBody}`;
  const expected = crypto
    .createHmac('sha256', secret)
    .update(signedPayload)
    .digest('hex');

  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1Signature));
}

Retry policy

If delivery fails (a non-2xx response or a 30-second timeout), KnowaKYC retries with exponential backoff. The base delay is 5 seconds, doubled on each attempt, with a maximum interval of 24 hours:

AttemptApproximate delay
1~5 s
2~10 s
3~20 s
4~40 s
5Final attempt, then DLQ if delivery still fails

Delays vary by ±20% (jitter). The system performs up to 5 total delivery attempts. After all attempts are exhausted, the event is moved to the dead-letter queue. You can trigger redelivery manually from the Webhooks section of the admin panel.

Terminal business evidence

kyb.session.completed includes available business_id, external_id, top-level company_name and the business profile, for both automatic and reviewer decisions. The profile contains available registration, tax, jurisdiction, address, legal form, director/beneficiary and lookup evidence. It uses this session's collected evidence or its associated persisted business; another session's business profile is not substituted.

Available current verifications, document analysis, uploaded media_files, decision reasons, custom steps, risk/device summaries and beneficiary KYC checks are included. Lookup results preserve outcome, reason, coverage, consulted sources and next actions. Provider raw payloads, internal references and storage paths are excluded from document analysis.

For business people, is_pep and is_sanctioned are optional. false means an observed negative finding; an absent field means unknown or unchecked. Do not coerce absence to false.

Review comment webhooks

New comments on KYC review tasks emit verification.comment.created; new comments on KYB review tasks emit kyb.review.comment.created. Comments created in the tenant dashboard, the platform review console, and the tenant API use the same contract. These events do not change the verification decision or complete the session.

Subscribe to the exact event type for the required scope. Existing * subscriptions also receive these events. Delivery uses the existing webhook signature, environment filtering, retries, and delivery logs. Events are delivered at least once; consumers must deduplicate using the event id or idempotency_key. Arrival order is not guaranteed.

Example event data for KYC:

{
  "scope": "kyc",
  "task_id": "550e8400-e29b-41d4-a716-446655440010",
  "session_id": "550e8400-e29b-41d4-a716-446655440001",
  "flow_id": "550e8400-e29b-41d4-a716-446655440002",
  "external_id": "customer_123",
  "applicant_id": "550e8400-e29b-41d4-a716-446655440099",
  "comment": {
    "id": "550e8400-e29b-41d4-a716-446655440011",
    "content": "Please provide a clearer document photo.",
    "created_at": "2026-10-16T08:00:00.123456Z",
    "author_type": "admin_user"
  }
}

For KYB, scope is kyb and business_id replaces applicant_id when present. Author types are admin_user, superadmin_user, api_key, and system. Employee identifiers and names are not included in the webhook. All task comments are client-visible; decision internal_notes and resubmission client_message are separate fields and do not produce comment events. Only newly created comments generate these events; existing comments are not replayed.

Retrieve comments with GET /tenant/v1/kyc/review-tasks/{task_id}/comments (reviews:read) or GET /tenant/v1/kyb/review-tasks/{task_id}/comments (kyb.reviews:read). Creating a comment uses POST at the same path and the corresponding write scope. The successful creation response adds comment_id and created_at, matching the saved comment and webhook snapshot. Each successful POST creates a new comment; webhook event deduplication does not make repeated creation requests idempotent. After an uncertain request outcome, reconcile using the comments API before resubmitting.

The comment, webhook event, and outbox entry are committed together. Failure to persist the event rolls back comment creation. HTTP delivery happens asynchronously after commit, so a successful comment response is not proof of successful HTTP webhook delivery; use delivery logs.

On this page