Skip to main content

KYC Webhooks

KYC webhook events notify you when a customer's ZenPays KYC verification status changes. These are fired for customers going through identity verification in the ramp checkout widget.

Events

EventDescription
kyc.approvedCustomer's identity verification passed
kyc.declinedCustomer's identity verification was rejected
kyc.in_reviewVerification flagged for manual review
kyc.expiredPrevious verification has expired
note

Intermediate states like pending, in_progress, and resubmitted are not delivered as webhooks — they would be noise for most integrations. If you need that granularity, poll GET /ramp-intents/:id/kyc-status instead.

Payload

{
"event_type": "kyc.approved",
"user_id": "usr_abc123",
"kyc_status": "approved",
"aml_status": "clear",
"decline_reason": null,
"merchant_id": "mer_abc123",
"affected_intent_ids": ["ri_1710345678000_a1b2c3d4e5f6g7h8"],
"didit_session_id": "8a9b2c1d-e3f4-...",
"occurred_at": "2026-03-16T14:30:00.000Z"
}

Payload Fields

FieldTypeDescription
event_typestringThe KYC event: kyc.approved, kyc.declined, kyc.in_review, or kyc.expired
user_idstringThe userId you passed when creating the ramp intent
kyc_statusstringVerification state: approved, declined, in_review, or expired
aml_statusstring | nullAML screening result: clear, flagged, rejected, pending_review, or null if not yet evaluated
decline_reasonstring | nullReason code when terminal: kyc_rejected, kyc_expired, or a provider-specific string. null for non-decline events.
merchant_idstringYour merchant ID
affected_intent_idsstring[]Ramp intents that this status change applied to. May be empty on late approvals — see "Multi-merchant fan-out & late approval" below.
didit_session_idstring | nullThe underlying Didit verification session ID, for cross-referencing with our support team.
occurred_atstringISO 8601 timestamp of the status change

Multi-merchant fan-out & late approval

Two behaviors that surprise integrators if you don't model them:

  1. Multi-merchant fan-out. A single customer can have ramp intents across multiple merchants. When their KYC status changes, every merchant that has any intent for that user receives the event independently. Each receives only the affected_intent_ids belonging to their merchant.

  2. Late approval after a prior decline. If a user is declined today and approved tomorrow (e.g., after manual re-review), the original failed intents stay terminally cancelled — we do not auto-resume them. The merchant still receives kyc.approved, but with affected_intent_ids: []. Treat this as an instruction to update your user verification cache so future intents proceed without re-KYC, not as an event about a specific transaction.

Usage

KYC webhooks are useful for:

  • Updating user records — mark a user as KYC-verified in your system when kyc.approved is received, so future ramp intents can pass kycVerified: true.
  • Handling rejections — notify the customer or trigger support flows on kyc.declined. The decline_reason field tells you whether it was a hard rejection (kyc_rejected) or a session expiry (kyc_expired).
  • Monitoring review queues — track kyc.in_review events for compliance reporting; pair with aml_status to know whether identity was approved but AML flagged for review.

Example

function handleWebhookEvent(event: any) {
switch (event.event_type) {
case 'kyc.approved':
// Mark user as KYC verified in your database for future ramp intents
await updateUserKycStatus(event.user_id, 'verified')

if (event.affected_intent_ids.length > 0) {
// The user had active intents that just unblocked — surface success
// in any UI showing those intents.
await notifyIntentsUnblocked(event.affected_intent_ids)
} else {
// Late approval after a prior decline — no active intent to resume.
// Merchant may want to invite the customer to start a new transaction.
await inviteRetry(event.user_id)
}
break

case 'kyc.declined':
// Hard rejection — notify the customer, offer support
await notifyCustomer(event.user_id, 'KYC verification was not successful', {
reason: event.decline_reason,
affectedIntents: event.affected_intent_ids,
})
break

case 'kyc.in_review':
// Identity OK but flagged for review (often AML-driven)
await flagForReview(event.user_id, {
amlStatus: event.aml_status,
affectedIntents: event.affected_intent_ids,
})
break

case 'kyc.expired':
// User will need to re-verify on the next ramp intent
await updateUserKycStatus(event.user_id, 'expired')
break
}
}
Skip KYC on repeat transactions

Once you receive kyc.approved for a user, store that status in your system. For subsequent ramp intents, pass kycVerified: true so the customer doesn't have to verify again.