Handling customeridentification.success and Failure Events
Paystack fires customeridentification.success when a customer identity verification (typically BVN in Nigeria) completes successfully, and customeridentification.failed when verification cannot be completed. Your webhook handler should update the customer KYC status in your database, gate features that require verified identity, and provide clear recovery paths when verification fails.
What Triggers Customer Identification Events
Paystack customer identification events are not automatic. They fire in response to a specific API call you make to verify a customer's identity. The flow works like this:
- A customer signs up on your platform and makes payments. Paystack creates a customer record.
- At some point, you need to verify their identity. This might be because they want a dedicated virtual account (DVA), or because your compliance requirements demand KYC for certain transaction thresholds, or because CBN regulations require it.
- You call the Paystack Customer Identification API with the customer code and their identification details (BVN, bank account number, or other ID).
- Paystack verifies the information asynchronously. This can take a few seconds to a few minutes.
- When verification completes, Paystack sends a webhook: either
customeridentification.successorcustomeridentification.failed.
The key point: verification is asynchronous. You cannot get the result from the API call itself. You must handle it through webhooks. If your webhook handler is not set up or is broken, you will never know whether the verification succeeded.
This is especially important if you use dedicated virtual accounts. Paystack requires customer identification before assigning a DVA. The flow is: identify the customer, wait for the customeridentification.success webhook, then create the dedicated account. See handling dedicatedaccount.assign events for the next step.
This article is part of the Paystack webhooks engineering guide.
The customeridentification.success Payload
When identity verification succeeds, Paystack sends a webhook with this structure:
{
"event": "customeridentification.success",
"data": {
"customer_id": 12345678,
"customer_code": "CUS_abc123def456",
"email": "amina@example.com",
"identification": {
"country": "NG",
"type": "bvn",
"value": "123****789"
},
"reason": "Identification was successful"
}
}
Key fields to note:
- customer_code - This is how you link the verification back to the customer in your system. Use this as the lookup key, not the email (customers can have multiple emails).
- identification.type - The method used for verification. Common values are
bvn(Bank Verification Number, Nigeria),bank_account, and sometimes government-issued ID types depending on the country. - identification.value - Partially masked. Paystack does not send the full BVN in the webhook payload. This is by design for security.
- identification.country - The country code (e.g.,
NGfor Nigeria,GHfor Ghana).
What the payload does not include: the customer's full name, date of birth, or other personal details from the BVN record. If you need those, you would have captured them during your own onboarding flow before triggering verification. The webhook only confirms that the identity check passed.
Handling Successful Identification
When you receive customeridentification.success, update the customer's KYC status in your database and unlock any features that require a verified identity.
async function handleIdentificationSuccess(data) {
const { customer_code, identification, email } = data;
// Update KYC status
const result = await db.query(
'UPDATE customers SET kyc_status = $1, kyc_method = $2, ' +
'kyc_country = $3, kyc_verified_at = NOW() ' +
'WHERE paystack_customer_code = $4 RETURNING id',
['verified', identification.type, identification.country, customer_code]
);
if (result.rows.length === 0) {
// Customer not found in our database. Log and investigate.
console.error(
'KYC success for unknown customer: ' + customer_code
);
return;
}
const customerId = result.rows[0].id;
// If this customer was waiting for a DVA, trigger DVA creation now
const pending = await db.query(
'SELECT id FROM dva_requests WHERE customer_id = $1 AND status = $2',
[customerId, 'awaiting_kyc']
);
if (pending.rows.length > 0) {
await dvaQueue.add('create-dva', {
customerId: customerId,
customerCode: customer_code,
});
await db.query(
'UPDATE dva_requests SET status = $1 WHERE customer_id = $2 AND status = $3',
['kyc_complete', customerId, 'awaiting_kyc']
);
}
// Log the verification for compliance records
await db.query(
'INSERT INTO kyc_audit_log (customer_id, event, method, country, ' +
'occurred_at) VALUES ($1, $2, $3, $4, NOW())',
[customerId, 'verification_success', identification.type, identification.country]
);
}
A few important points about this handler:
Do not store the BVN. The identification.value field is already masked, but even if it were not, storing raw BVN numbers creates a compliance liability. Store the verification status (verified or failed), the method, and the timestamp. That is all you need.
Check for the customer in your database. It is possible (though rare) to receive a verification event for a customer code that does not exist in your system. This can happen if you triggered verification from a different environment, or if there is a data sync issue. Log it and investigate rather than silently ignoring it.
Trigger dependent workflows. If the customer was waiting for KYC to clear before getting a DVA, this is the moment to kick off that process. Chain the workflows through a queue, not inline.
The customeridentification.failed Payload and Handling
When verification fails, the payload looks like this:
{
"event": "customeridentification.failed",
"data": {
"customer_id": 12345678,
"customer_code": "CUS_abc123def456",
"email": "amina@example.com",
"identification": {
"country": "NG",
"type": "bvn",
"value": "123****789"
},
"reason": "BVN does not match customer details"
}
}
The reason field tells you why verification failed. Common reasons include:
- BVN does not match customer details - The name or date of birth associated with the BVN does not match what was provided.
- Invalid BVN - The BVN number itself is not valid.
- Verification service unavailable - The external verification service (like NIBSS in Nigeria) was down. This is a temporary failure, not a data problem.
Your handling depends on the reason:
async function handleIdentificationFailed(data) {
const { customer_code, identification, reason, email } = data;
// Update KYC status
await db.query(
'UPDATE customers SET kyc_status = $1, kyc_failure_reason = $2, ' +
'kyc_failed_at = NOW() WHERE paystack_customer_code = $3',
['failed', reason, customer_code]
);
// Log for compliance
await db.query(
'INSERT INTO kyc_audit_log (customer_id, event, method, country, ' +
'reason, occurred_at) VALUES (' +
'(SELECT id FROM customers WHERE paystack_customer_code = $1), ' +
'$2, $3, $4, $5, NOW())',
[customer_code, 'verification_failed', identification.type,
identification.country, reason]
);
// Determine if this is retryable
const isServiceError = reason.toLowerCase().includes('unavailable') ||
reason.toLowerCase().includes('timeout') ||
reason.toLowerCase().includes('service');
if (isServiceError) {
// Schedule an automatic retry after 30 minutes
await retryQueue.add(
'retry-kyc',
{ customerCode: customer_code },
{ delay: 30 * 60 * 1000 }
);
await emailQueue.add('kyc-delay', {
to: email,
message: 'We are still verifying your identity. ' +
'This may take a little longer than usual. ' +
'We will notify you once it is complete.',
});
} else {
// Data mismatch. Customer needs to correct their information.
await emailQueue.add('kyc-failed', {
to: email,
reason: reason,
retryUrl: '/account/verify-identity',
message: 'Your identity verification could not be completed. ' +
'Please check your details and try again.',
});
}
}
The distinction between service errors and data errors is important. A service error means you should retry automatically. A data error means the customer submitted wrong information and needs to correct it. Retrying with the same wrong data will just fail again.
Security Considerations for Identity Events
Identity verification webhooks are security-critical. If an attacker can forge a customeridentification.success event to your endpoint, they can bypass your KYC controls. A few things to get right:
Always verify the webhook signature. This applies to all webhooks, but it is especially important for identity events. A forged customeridentification.success could let an unverified customer access features they should not have. See verifying the x-paystack-signature header.
Do not expose failure reasons to the customer verbatim. The reason from Paystack is a technical message. Translate it into a user-friendly message. Showing internal error details can leak information about your verification pipeline.
Log everything for compliance. Regulatory requirements in Nigeria (CBN) and other markets require you to keep records of KYC attempts, both successful and failed. Log the timestamp, method, country, and outcome. Do not log the raw identification value, even if it is masked.
Rate-limit verification retries. If a customer submits wrong details and keeps retrying, they might be trying to brute-force a match. Set a maximum number of verification attempts per customer (three to five is reasonable). After that, require manual review by your compliance team.
async function canRetryVerification(customerCode) {
const result = await db.query(
'SELECT COUNT(*) as attempts FROM kyc_audit_log ' +
'WHERE customer_id = (SELECT id FROM customers ' +
'WHERE paystack_customer_code = $1) ' +
'AND event = $2 AND occurred_at > NOW() - INTERVAL '24 hours'',
[customerCode, 'verification_failed']
);
const attempts = parseInt(result.rows[0].attempts, 10);
return attempts < 5; // Allow up to 5 attempts per 24 hours
}
If you store customer data that is subject to data protection regulations (like NDPR in Nigeria or POPIA in South Africa), make sure your KYC audit log retention policy aligns with those regulations. You may need to keep records for 5 to 7 years, or you may need to delete them upon customer request, depending on the regulation.
Integration with Dedicated Virtual Accounts
The most common reason to use customer identification is as a prerequisite for Paystack Dedicated Virtual Accounts (DVAs). The typical flow is:
- Customer signs up and wants to pay via bank transfer.
- You call the Paystack Customer Identification API with their BVN.
- You receive
customeridentification.successvia webhook. - You call the Paystack Create Dedicated Account API.
- You receive
dedicatedaccount.assign.successvia webhook with the virtual account details. - You display the virtual account number to the customer.
Steps 3 through 5 happen through webhooks. Your code needs to chain these events together without blocking. The recommended approach is to use a state machine in your database:
// After customeridentification.success:
// State: awaiting_kyc -> kyc_complete
// Trigger DVA creation:
// State: kyc_complete -> dva_requested
// After dedicatedaccount.assign.success:
// State: dva_requested -> dva_active
// Database table to track this:
// dva_requests(id, customer_id, status, created_at, updated_at)
// status: awaiting_kyc | kyc_complete | dva_requested | dva_active | failed
Each webhook handler updates the state and triggers the next step if appropriate. This way, if any webhook is missed or delayed, you can see exactly where the process stalled and recover.
For the DVA webhook handler itself, see handling dedicatedaccount.assign events.
Testing Identification Events
Identity verification events are tricky to test because they depend on external verification services. Here is how to approach it:
In Paystack test mode, the identification API accepts test data and fires the webhook events. Use the test BVN numbers from the Paystack documentation. These are predefined numbers that will return predictable success or failure results.
For local development, simulate the webhooks with curl:
SECRET="sk_test_your_secret_key"
# Simulate a successful identification
BODY='{"event":"customeridentification.success","data":{"customer_id":12345,"customer_code":"CUS_test123","email":"test@example.com","identification":{"country":"NG","type":"bvn","value":"123****789"},"reason":"Identification was successful"}}'
SIG=$(echo -n "$BODY" | openssl dgst -sha512 -hmac "$SECRET" | awk '{print $2}')
curl -X POST http://localhost:3000/webhooks/paystack -H "Content-Type: application/json" -H "x-paystack-signature: $SIG" -d "$BODY"
For unit tests, mock the database and queue, pass test payloads directly to your handler functions, and assert the correct state changes. Test both the success path and the failure path, including the distinction between service errors (retryable) and data errors (customer must fix their input).
For the full testing setup, see simulating Paystack webhook events for automated tests.
Key Takeaways
- ✓Paystack fires customeridentification.success and customeridentification.failed events when identity verification (BVN, bank account, or ID) completes or cannot be completed.
- ✓Identification is triggered when you call the Paystack customer identification API, not automatically on customer creation.
- ✓The success payload includes the customer code, identified fields, and the identification method used (BVN, bank account, or ID number).
- ✓Store the identification status in your database and use it to gate features that require verified identity, such as dedicated virtual accounts or higher transaction limits.
- ✓When verification fails, the payload includes a reason. Display this to the customer and let them retry with corrected information.
- ✓Never store raw BVN numbers in your database. Store only the verification status and the customer code.
- ✓Always verify the webhook signature before trusting identification results. A forged success event could bypass your KYC controls.
Frequently Asked Questions
- Does Paystack automatically verify customer identity when I create a customer?
- No. Customer creation and identity verification are separate steps. You create a customer through the normal payment flow or via the API, and then trigger verification separately by calling the Customer Identification API with the customer code and their identification details (such as BVN). The verification webhook fires only after you explicitly initiate it.
- Can I use customer identification in countries other than Nigeria?
- Customer identification is primarily used in Nigeria for BVN verification, which is required for features like Dedicated Virtual Accounts. Paystack supports identification in other markets (Ghana, South Africa, Kenya) depending on available verification services. Check the Paystack documentation for supported identification types per country.
- What should I do if the customeridentification.failed event has no useful reason?
- Sometimes the reason field is generic or empty. In that case, log the event, notify the customer that verification could not be completed, and suggest they try again. If failures persist, contact Paystack support with the customer code and timestamps from your logs. The issue may be on the verification provider side.
- Is it safe to store the identification value from the webhook payload?
- The value in the webhook payload is already masked (e.g., 123****789), so storing it does not expose the full BVN. However, best practice is to store only the verification status, method, and timestamp. You do not need the masked value for any business logic. Minimizing stored personal data reduces your compliance burden.
- How long does Paystack identity verification take?
- Verification typically completes within seconds to a few minutes. However, it can take longer if the external verification service (like NIBSS for BVN in Nigeria) is experiencing delays. Build your UI to show the customer a pending state and update it when the webhook arrives, rather than making them wait on a loading screen.
Ready to build real-world apps?
Join the McTaba Labs full-stack marathon (4 months full-time · 6 months part-time). Learn M-Pesa, USSD, and WhatsApp engineering while shipping 8 production apps.
Apply to the McTaba Marathon