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, andkyb.business.review_requiredevents carry the full business data in thedatafield (matching theGET /v1/kyb/businesses/{id}API response): legal form, directors, beneficial owners with UBO fields, the KYB inputprofile(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-Signatureheader) - 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, andkyb.step.completeddo not establish successful KYB verification. - For
kyb.business.*, usedata.verification_status:verified,rejected, orreview. Review requires a reviewer decision and is not approval. - For
kyb.session.completed, usedata.result:approvedmeans approval;rejectedordeclinedmeans rejection.statusdescribes the process. Historical rejections can carrystatus: failed; a completed process can havestatus: completedfor either final decision. decision_sourcedistinguishesautomaticfrommanual_review. Manual decisions includedecision_idand, 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, anddecision_reasonswhen registry coverage, signature findings or other signals matter to your integration.ai_confirmationis not the final decision. business_is_current_session: falsemeans 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. Usekyb.business.*events for changes to the current business verdict.- The terminal event's nested
business.verification_statusdescribes 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
| Event | Description |
|---|---|
kyb.session.created | KYB session created (the user opened the verification link) |
kyb.session.started | KYB session started (the user began entering data) |
kyb.session.completed | Final KYB session decision (approval or rejection) |
kyb.session.expired | KYB session expired |
kyb.session.cancelled | KYB session cancelled |
kyb.business.verified | Business successfully verified |
kyb.business.rejected | Business rejected |
kyb.business.review_required | Business routed to manual review by an operator |
kyb.step.started | Verification step started (for example, sending the AI analysis request) |
kyb.step.completed | Verification step completed (for example, AI analysis) |
kyb.step.failed | Verification step finished with an error |
kyb.step.skipped | Verification 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:
| Field | Type | Description |
|---|---|---|
business_id | string | UUID of the business in KnowaKYC |
external_id | string | External identifier of the business in your system |
session_id | string | UUID of the current verification session |
last_session_id | string | UUID of the latest session (same as session_id for the first verification) |
company_name | string | Company name |
registration_number | string | Company registration number |
tax_number | string | INN / BIN |
country | string | ISO country code of registration |
legal_form | string | Legal form |
company_status | string | Company status in the state register (active, liquidating, liquidated, etc.) |
status_date | string | Date the current status was assigned |
status_info | string | Additional status information |
liquidation_date | string | Liquidation date (when applicable) |
legal_address | string | Legal address |
actual_address | string | Operating address |
main_url | string | Official website address |
profile | object | Company profile fields entered during the KYB flow (email, phone, trade_name, website, business_type, activity fields, ownership structure, identifiers) |
registration_date | string | State registration date |
director_name | string | Director's full name |
director_title | string | Director's title |
directors | array | List of directors [{name, title}] |
beneficiaries | array | List of beneficial owners [{name, share, type, country, pep}] |
activities | object | Activity types {main, additional} |
ai_confirmation | boolean | AI confirmation result |
ai_summary | string | Short AI analysis summary |
ai_notes | string | Additional AI analysis notes |
ai_sources | array | Sources used during AI analysis |
director_unavailable_reason_info | string | Reason director data is unavailable |
beneficiaries_unavailable_reason_info | string | Reason beneficial owner data is unavailable |
activities_unavailable_reason_info | string | Reason activity data is unavailable |
main_url_unavailable_reason_info | string | Reason website information is unavailable |
status_evidence_sources | array | Sources for the company status data |
director_evidence_sources | array | Sources for the director data |
beneficiaries_evidence_sources | array | Sources for the beneficial owner data |
activities_evidence_sources | array | Sources for the activity data |
main_url_evidence_sources | array | Sources for the website data |
risk_score | number | Risk score (0–100) |
rejection_reasons | array | Rejection reasons (for the rejected status) |
last_verified_at | string | Last verification timestamp (RFC 3339) |
verification_status | string | Verification status: verified, rejected, review, pending |
business_document_key | string | Business document key in object storage |
metadata | object | Additional metadata |
created_at | string | Record creation timestamp (RFC 3339) |
updated_at | string | Record 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:
| Attempt | Approximate delay |
|---|---|
| 1 | ~5 s |
| 2 | ~10 s |
| 3 | ~20 s |
| 4 | ~40 s |
| 5 | Final 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.