The Customers API lets you register individuals or businesses on Tsara's platform.
Customers can later be linked to wallets, reserved accounts, or bank accounts, and verified using BVN, NIN, or CAC.
π Overview
What You Can Do
- Create individual or business customers
- Store KYC information securely
- Link customers to wallets, reserved accounts, and transfers
- Verify identities using BVN, NIN, or CAC
- Retrieve and update customer information
- Track verification status
Customer Types
| Type | Description | Required Info | Verification Method | Use Case |
|---|---|---|---|---|
| Individual | Personal customers | Name, email, phone, BVN/NIN | BVN or NIN verification | User wallets, reserved accounts, personal transfers |
| Business | Corporate entities | Business name, email, CAC, TIN | CAC verification | Business accounts, vendor payments, marketplace sellers |
Customer Status Lifecycle
ββββββββββββββββββββββββ
β pending_verification β Initial state when customer is created
ββββββββββββ¬ββββββββββββ
β
βββββββΊ verified (Identity verification successful)
β
βββββββΊ rejected (Verification failed or invalid details)
Status Descriptions
| Status | Description | Next Actions |
|---|---|---|
pending_verification | Customer created, identity not verified yet | Initiate verification via Identity API |
verified | Identity successfully verified | Can create reserved accounts, full platform access |
rejected | Verification failed or documents invalid | Review failure reason, retry with correct info |
π§ Create Individual Customer
Endpoint
POST /customers
Authentication
Requires Secret Key in Authorization header (server-side only).
Description
Registers a new individual customer with their personal information. The customer can later be verified and linked to wallets or reserved accounts.
Request Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
type | string | Required | Must be individual for personal customers |
first_name | string | Required | Customer's first name (2-50 characters) |
last_name | string | Required | Customer's last name (2-50 characters) |
email | string | Required | Valid email address (used for notifications) |
phone | string | Required | Phone number in international format (e.g., +2348012345678) |
bvn | string | Optional | 11-digit Bank Verification Number (required for BVN verification) |
nin | string | Optional | 11-digit National Identification Number (alternative to BVN) |
date_of_birth | string | Optional | Date of birth in YYYY-MM-DD format |
address | string | Optional | Residential address |
city | string | Optional | City of residence |
state | string | Optional | State of residence |
country | string | Optional | Country code (e.g., NG for Nigeria) |
metadata | object | Optional | Custom key-value data for internal reference (max 10 keys) |
Request Body Example
{
"type": "individual",
"first_name": "John",
"last_name": "Doe",
"email": "[email protected]",
"phone": "+2348012345678",
"bvn": "22334455667",
"date_of_birth": "1990-05-15",
"address": "123 Main Street",
"city": "Lagos",
"state": "Lagos",
"country": "NG",
"metadata": {
"user_id": "user_12345",
"ref_code": "INV-001",
"source": "mobile_app"
}
}Example Request
curl -X POST "https://sandbox.tsara.ng/v1/customers" \
-H "Authorization: Bearer YOUR_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{
"type": "individual",
"first_name": "John",
"last_name": "Doe",
"email": "[email protected]",
"phone": "+2348012345678",
"bvn": "22334455667",
"metadata": {
"user_id": "user_12345",
"ref_code": "INV-001"
}
}'Response Fields
| Field | Type | Description |
|---|---|---|
success | boolean | Whether request was successful |
data | object | Customer details |
data.id | string | Unique customer ID (e.g., cus_123) - use this for all future operations |
data.type | string | Customer type (individual) |
data.first_name | string | Customer's first name |
data.last_name | string | Customer's last name |
data.email | string | Customer's email address |
data.phone | string | Customer's phone number |
data.bvn | string | BVN (masked for security: 223344****7) |
data.date_of_birth | string | Date of birth |
data.address | string | Residential address |
data.status | string | Verification status: pending_verification, verified, or rejected |
data.metadata | object | Custom metadata you provided |
data.created_at | string | ISO 8601 timestamp |
data.updated_at | string | ISO 8601 timestamp |
request_id | string | Unique request identifier for debugging |
Response Example
{
"success": true,
"data": {
"id": "cus_123",
"type": "individual",
"first_name": "John",
"last_name": "Doe",
"email": "[email protected]",
"phone": "+2348012345678",
"bvn": "223344****7",
"date_of_birth": "1990-05-15",
"address": "123 Main Street",
"city": "Lagos",
"state": "Lagos",
"country": "NG",
"status": "pending_verification",
"metadata": {
"user_id": "user_12345",
"ref_code": "INV-001",
"source": "mobile_app"
},
"created_at": "2025-10-17T12:00:00Z",
"updated_at": "2025-10-17T12:00:00Z"
},
"request_id": "req_abc"
}π’ Create Business Customer
Endpoint
POST /customers
Authentication
Requires Secret Key in Authorization header (server-side only).
Description
Registers a new business customer for corporate payments, wallets, or settlement accounts. Businesses must provide CAC registration details for verification.
Request Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
type | string | Required | Must be business for corporate customers |
business_name | string | Required | Registered business name (5-100 characters) |
email | string | Required | Business email address |
phone | string | Required | Business phone number in international format |
cac_number | string | Required | Corporate Affairs Commission registration number (e.g., RC123456) |
tax_id | string | Optional | Tax Identification Number (TIN) |
industry | string | Optional | Business industry (e.g., technology, retail, finance) |
address | string | Optional | Business address |
city | string | Optional | City of operation |
state | string | Optional | State of operation |
country | string | Optional | Country code (default: NG) |
contact_person | object | Optional | Primary contact person details |
contact_person.first_name | string | Optional | Contact person's first name |
contact_person.last_name | string | Optional | Contact person's last name |
contact_person.email | string | Optional | Contact person's email |
contact_person.phone | string | Optional | Contact person's phone |
metadata | object | Optional | Custom key-value data for internal reference |
Request Body Example
{
"type": "business",
"business_name": "Acme Technologies Ltd",
"email": "[email protected]",
"phone": "+2348123456789",
"cac_number": "RC123456",
"tax_id": "TIN123456",
"industry": "technology",
"address": "456 Business Plaza",
"city": "Lagos",
"state": "Lagos",
"country": "NG",
"contact_person": {
"first_name": "Jane",
"last_name": "Smith",
"email": "[email protected]",
"phone": "+2348098765432"
},
"metadata": {
"account_manager": "Chris",
"tier": "enterprise"
}
}Example Request
curl -X POST "https://sandbox.tsara.ng/v1/customers" \
-H "Authorization: Bearer YOUR_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{
"type": "business",
"business_name": "Acme Technologies Ltd",
"email": "[email protected]",
"phone": "+2348123456789",
"cac_number": "RC123456",
"tax_id": "TIN123456",
"metadata": {
"account_manager": "Chris"
}
}'Response Fields
| Field | Type | Description |
|---|---|---|
success | boolean | Whether request was successful |
data | object | Customer details |
data.id | string | Unique customer ID (e.g., cus_789) |
data.type | string | Customer type (business) |
data.business_name | string | Registered business name |
data.email | string | Business email |
data.phone | string | Business phone |
data.cac_number | string | CAC registration number |
data.tax_id | string | Tax identification number |
data.industry | string | Business industry |
data.address | string | Business address |
data.contact_person | object | Contact person details |
data.status | string | Verification status |
data.metadata | object | Custom metadata |
data.created_at | string | Creation timestamp |
data.updated_at | string | Last update timestamp |
request_id | string | Request identifier |
Response Example
{
"success": true,
"data": {
"id": "cus_789",
"type": "business",
"business_name": "Acme Technologies Ltd",
"email": "[email protected]",
"phone": "+2348123456789",
"cac_number": "RC123456",
"tax_id": "TIN123456",
"industry": "technology",
"address": "456 Business Plaza",
"city": "Lagos",
"state": "Lagos",
"country": "NG",
"contact_person": {
"first_name": "Jane",
"last_name": "Smith",
"email": "[email protected]",
"phone": "+2348098765432"
},
"status": "pending_verification",
"metadata": {
"account_manager": "Chris",
"tier": "enterprise"
},
"created_at": "2025-10-17T12:00:00Z",
"updated_at": "2025-10-17T12:00:00Z"
},
"request_id": "req_def"
}π Retrieve a Customer
Endpoint
GET /customers?id={customer_id}
Authentication
Requires Secret Key in Authorization header.
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | Required | Customer ID to retrieve (e.g., cus_123) |
Example Request
curl -X GET "https://sandbox.tsara.ng/v1/customers?id=cus_123" \
-H "Authorization: Bearer YOUR_SECRET_KEY"Response Fields
| Field | Type | Description |
|---|---|---|
success | boolean | Whether request was successful |
data | object | Customer details (same structure as create response) |
data.id | string | Customer ID |
data.type | string | Customer type |
data.status | string | Current verification status |
data.verification_details | object | Verification results (if verified) |
data.wallets | array | Associated wallet IDs |
data.reserved_accounts | array | Associated reserved account IDs |
Response Example
{
"success": true,
"data": {
"id": "cus_123",
"type": "individual",
"first_name": "John",
"last_name": "Doe",
"email": "[email protected]",
"phone": "+2348012345678",
"bvn": "223344****7",
"status": "verified",
"verification_details": {
"method": "bvn",
"verified_at": "2025-10-17T12:30:00Z",
"verified_name": "JOHN VICTOR DOE"
},
"wallets": ["wallet_abc"],
"reserved_accounts": ["racct_xyz"],
"metadata": {
"user_id": "user_12345"
},
"created_at": "2025-10-17T12:00:00Z",
"updated_at": "2025-10-17T12:30:00Z"
}
}π List Customers
Endpoint
GET /customers
Authentication
Requires Secret Key in Authorization header.
Description
Retrieve paginated list of all your customers with optional filtering.
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
page | integer | Optional | Page number (default: 1) |
limit | integer | Optional | Results per page (default: 20, max: 100) |
type | string | Optional | Filter by type: individual or business |
status | string | Optional | Filter by status: pending_verification, verified, or rejected |
email | string | Optional | Filter by email address |
phone | string | Optional | Filter by phone number |
Example Request
curl -X GET "https://sandbox.tsara.ng/v1/customers?page=1&limit=20&status=verified" \
-H "Authorization: Bearer YOUR_SECRET_KEY"Response Fields
| Field | Type | Description |
|---|---|---|
success | boolean | Whether request was successful |
data | array | Array of customer objects |
data[].id | string | Customer ID |
data[].type | string | Customer type |
data[].email | string | Customer email |
data[].status | string | Verification status |
data[].created_at | string | Creation timestamp |
pagination | object | Pagination details |
pagination.page | integer | Current page |
pagination.limit | integer | Results per page |
pagination.total | integer | Total customers |
pagination.total_pages | integer | Total pages |
Response Example
{
"success": true,
"data": [
{
"id": "cus_123",
"type": "individual",
"first_name": "John",
"last_name": "Doe",
"email": "[email protected]",
"phone": "+2348012345678",
"status": "verified",
"created_at": "2025-10-17T12:00:00Z"
},
{
"id": "cus_456",
"type": "business",
"business_name": "Acme Technologies Ltd",
"email": "[email protected]",
"phone": "+2348123456789",
"status": "pending_verification",
"created_at": "2025-10-17T13:00:00Z"
}
],
"pagination": {
"page": 1,
"limit": 20,
"total": 2,
"total_pages": 1
}
}βοΈ Update a Customer
Endpoint
PATCH /customers?id={customer_id}
Authentication
Requires Secret Key in Authorization header.
Description
Update customer information. You can update contact details, address, or metadata. BVN/NIN/CAC cannot be changed once set.
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | Required | Customer ID to update |
Request Parameters
| Parameter | Type | Description |
|---|---|---|
email | string | New email address |
phone | string | New phone number |
address | string | Updated address |
city | string | Updated city |
state | string | Updated state |
metadata | object | Updated metadata (merges with existing) |
Request Body Example
{
"email": "[email protected]",
"phone": "+2348099999999",
"address": "789 New Address",
"metadata": {
"tier": "premium",
"updated_reason": "customer_request"
}
}Example Request
curl -X PATCH "https://sandbox.tsara.ng/v1/customers?id=cus_123" \
-H "Authorization: Bearer YOUR_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{
"email": "[email protected]",
"phone": "+2348099999999"
}'Response Example
{
"success": true,
"data": {
"id": "cus_123",
"type": "individual",
"first_name": "John",
"last_name": "Doe",
"email": "[email protected]",
"phone": "+2348099999999",
"status": "verified",
"updated_at": "2025-10-17T14:00:00Z"
}
}π Webhook Events
When customer verification completes, Tsara will notify your configured webhook endpoint.
Event Types
| Event Type | Description | When Triggered |
|---|---|---|
customer.verified | Customer identity verified successfully | BVN/NIN/CAC verification succeeds |
customer.verification_failed | Customer verification failed | Invalid BVN/NIN/CAC or verification error |
Webhook Payload Structure
| Field | Type | Description |
|---|---|---|
id | string | Unique webhook event ID |
type | string | Event type |
data | object | Customer details |
data.customer_id | string | Customer ID |
data.status | string | New customer status |
data.verification_method | string | Method used (bvn, nin, or cac) |
data.verified_at | string | Verification timestamp |
data.failure_reason | string | Reason for failure (only in failed events) |
Verified Event Example
{
"id": "evt_customer_001",
"type": "customer.verified",
"data": {
"customer_id": "cus_123",
"type": "individual",
"first_name": "John",
"last_name": "Doe",
"email": "[email protected]",
"status": "verified",
"verification_method": "bvn",
"verified_name": "JOHN VICTOR DOE",
"verified_at": "2025-10-17T12:30:00Z"
}
}Failed Verification Event Example
{
"id": "evt_customer_002",
"type": "customer.verification_failed",
"data": {
"customer_id": "cus_456",
"type": "individual",
"email": "[email protected]",
"status": "rejected",
"verification_method": "bvn",
"failure_reason": "BVN does not match provided name",
"failed_at": "2025-10-17T13:00:00Z"
}
}β οΈ Error Responses
Common Errors
| Error Code | Message | Cause | Solution |
|---|---|---|---|
400 | Invalid email format | Email is malformed | Use valid email format |
400 | Invalid phone format | Phone not in international format | Use format: +2348012345678 |
400 | Invalid BVN format | BVN is not 11 digits | Ensure BVN is exactly 11 digits |
400 | Invalid CAC number | CAC format is wrong | Use format: RC123456 |
400 | Missing required field | Required field not provided | Check all required fields are included |
401 | Unauthorized | Invalid or missing API key | Check Authorization header with Secret Key |
404 | Customer not found | Customer ID doesn't exist | Verify customer ID is correct |
409 | Duplicate customer | Email or phone already exists | Use existing customer or different email/phone |
422 | Validation error | Data validation failed | Check error details for specific field issues |
Error Response Structure
{
"success": false,
"status": "error",
"status_code": 400,
"message": "Invalid phone format",
"errors": {
"phone": [
"Phone number must be in international format (e.g., +2348012345678)"
]
}
}π‘ Use Cases
1. User Onboarding with KYC
Create customer records during user registration and verify their identity.
Workflow:
- User signs up on your platform
- Collect personal information and BVN
- Create customer via API
- Initiate BVN verification via Identity API
- Listen for webhook to confirm verification
- Grant full platform access when verified
Example:
{
"type": "individual",
"first_name": "Sarah",
"last_name": "Johnson",
"email": "[email protected]",
"phone": "+2348011112222",
"bvn": "12345678901",
"date_of_birth": "1995-03-20",
"metadata": {
"user_id": "app_user_789",
"registration_source": "mobile_app",
"referral_code": "REF123"
}
}Then verify immediately:
curl -X POST "https://sandbox.tsara.ng/v1/identity/verify/bvn" \
-H "Authorization: Bearer YOUR_SECRET_KEY" \
-d '{
"customer_id": "cus_123",
"bvn": "12345678901"
}'2. Reserved Account Creation
Create verified customers to enable reserved virtual accounts.
Workflow:
- Create customer with personal details
- Verify identity (required for reserved accounts)
- Create reserved virtual account linked to customer
- Customer receives permanent account number
- Track all payments to that account
Example:
{
"type": "individual",
"first_name": "Michael",
"last_name": "Chen",
"email": "[email protected]",
"phone": "+2348033334444",
"bvn": "98765432109",
"metadata": {
"account_purpose": "savings_wallet",
"user_tier": "premium"
}
}After verification, create reserved account:
curl -X POST "https://sandbox.tsara.ng/v1/fiat/virtual-accounts/reserved" \
-H "Authorization: Bearer YOUR_SECRET_KEY" \
-d '{
"identity_id": "cus_123",
"account_type": "personal"
}'3. Marketplace Seller Verification
Verify business sellers on your marketplace platform.
Workflow:
- Seller submits business registration
- Create business customer with CAC details
- Verify CAC via Identity API
- Approve seller account when verified
- Link payouts to verified business
Example:
{
"type": "business",
"business_name": "Fashion Boutique Ltd",
"email": "[email protected]",
"phone": "+2348055556666",
"cac_number": "RC987654",
"tax_id": "TIN987654",
"industry": "retail",
"contact_person": {
"first_name": "Amina",
"last_name": "Mohammed",
"email": "[email protected]",
"phone": "+2348077778888"
},
"metadata": {
"seller_id": "seller_456",
"store_category": "fashion",
"commission_rate": "5.0"
}
}4. Wallet System with Customer Profiles
Link customer profiles to USDC wallets for complete user management.
Workflow:
- Create customer during wallet creation
- Link wallet ID to customer metadata
- Track all wallet transactions under customer profile
- Generate customer-specific reports
- Implement tiered limits based on verification
Example:
{
"type": "individual",
"first_name": "David",
"last_name": "Okonkwo",
"email": "[email protected]",
"phone": "+2348099990000",
"nin": "12345678901",
"metadata": {
"wallet_id": "wallet_xyz123",
"daily_limit": "100000000",
"monthly_volume": "0"
}
}Track transactions:
async function recordTransaction(customerId, amount, type) {
const customer = await getCustomer(customerId);
const currentVolume = customer.metadata.monthly_volume;
await updateCustomer(customerId, {
metadata: {
...customer.metadata,
monthly_volume: (parseInt(currentVolume) + amount).toString()
}
});
}5. Compliance and Reporting
Use customer data for regulatory compliance and financial reporting.
Workflow:
- Create customers with complete KYC data
- Verify all identities before transactions
- Track transaction volumes per customer
- Generate compliance reports
- Monitor for suspicious activity
Example:
async function getComplianceReport(startDate, endDate) {
const customers = await listCustomers({
status: 'verified',
created_after: startDate,
created_before: endDate
});
return customers.map(customer => ({
customer_id: customer.id,
name: `${customer.first_name} ${customer.last_name}`,
verification_method: customer.verification_details.method,
verified_at: customer.verification_details.verified_at,
total_transactions: customer.transaction_count,
total_volume: customer.total_volume
}));
}π§ Best Practices
1. Use Metadata for Internal References
Store your internal user IDs and other business data in the metadata field.
Why?
- Link Tsara customers to your database records
- Track registration source and campaigns
- Store business-specific attributes
- Easy filtering and reporting
Example:
{
"type": "individual",
"first_name": "John",
"last_name": "Doe",
"email": "[email protected]",
"phone": "+2348012345678",
"metadata": {
"internal_user_id": "user_12345",
"registration_source": "mobile_app",
"referral_code": "FRIEND123",
"tier": "basic",
"onboarded_by": "agent_007",
"campaign": "summer_2024"
}
}Query by metadata:
const customer = await listCustomers({
metadata: { internal_user_id: 'user_12345' }
});2. Prevent Duplicate Customers
Check for existing customers before creating new ones using email or phone.
Why?
- Avoid duplicate records
- Maintain data integrity
- Prevent verification conflicts
- Better user experience
Implementation:
async function getOrCreateCustomer(customerData) {
const existing = await listCustomers({
email: customerData.email
});
if (existing.data.length > 0) {
return existing.data[0];
}
const existingByPhone = await listCustomers({
phone: customerData.phone
});
if (existingByPhone.data.length > 0) {
return existingByPhone.data[0];
}
return await createCustomer(customerData);
}3. Verify Before Enabling Sensitive Operations
Only allow reserved accounts, large transfers, or withdrawals for verified customers.
Why?
- Regulatory compliance
- Fraud prevention
- Account security
- Risk management
Implementation:
async function createReservedAccount(customerId) {
const customer = await getCustomer(customerId);
if (customer.status !== 'verified') {
throw new Error(
'Customer must be verified before creating reserved account. ' +
'Please complete identity verification first.'
);
}
return await tsara.createReservedAccount({
identity_id: customerId,
account_type: 'personal'
});
}Tiered limits:
function getTransferLimit(customer) {
switch (customer.status) {
case 'verified':
return 500000000;
case 'pending_verification':
return 10000000;
default:
return 0;
}
}4. Collect BVN/NIN During Registration
Collect identity information upfront to enable smooth verification later.
Why?
- Streamlined onboarding
- Immediate verification possible
- Reduced drop-off
- Better user experience
UI Flow:
const onboardingSteps = [
{
title: 'Personal Information',
fields: ['first_name', 'last_name', 'email', 'phone']
},
{
title: 'Identity Verification',
fields: ['bvn', 'date_of_birth'],
description: 'Required for account security and compliance'
},
{
title: 'Address Details',
fields: ['address', 'city', 'state']
}
];
async function completeOnboarding(formData) {
const customer = await createCustomer(formData);
const verification = await verifyBVN({
customer_id: customer.id,
bvn: formData.bvn
});
return { customer, verification };
}5. Handle Verification Failures Gracefully
Provide clear guidance when verification fails and allow retry.
Why?
- Better user experience
- Higher verification success rate
- Reduced support tickets
- Clear error communication
Implementation:
async function handleVerificationWebhook(event) {
if (event.type === 'customer.verification_failed') {
const customer = event.data;
const reason = event.data.failure_reason;
const userMessage = {
'BVN does not match provided name':
'The name on your BVN doesn\'t match the name you provided. Please check your spelling and try again.',
'Invalid BVN':
'The BVN you provided is invalid. Please verify your BVN and try again.',
'BVN verification service unavailable':
'We couldn\'t verify your BVN at this time. Please try again in a few minutes.'
}[reason] || 'Verification failed. Please contact support.';
await notifyUser(customer.customer_id, {
title: 'Verification Failed',
message: userMessage,
action: 'retry_verification'
});
await updateCustomerStatus(customer.customer_id, 'retry_required');
}
}6. Update Customer Information When Changed
Keep customer records synchronized with your platform's user data.
Why?
- Accurate communication
- Compliance requirements
- Audit trail
- Data consistency
Implementation:
async function handleUserProfileUpdate(userId, updates) {
const customer = await getCustomerByUserId(userId);
const tsaraUpdates = {};
if (updates.email) tsaraUpdates.email = updates.email;
if (updates.phone) tsaraUpdates.phone = updates.phone;
if (updates.address) tsaraUpdates.address = updates.address;
if (Object.keys(tsaraUpdates).length > 0) {
await updateCustomer(customer.id, tsaraUpdates);
}
await updateLocalDatabase(userId, updates);
}7. Store Customer ID in Your Database
Always save the Tsara customer_id in your database for quick lookups.
Why?
- Fast customer retrieval
- Link to wallets and accounts
- Transaction tracking
- Reporting and analytics
Database Schema:
CREATE TABLE users (
id SERIAL PRIMARY KEY,
email VARCHAR(255) UNIQUE NOT NULL,
tsara_customer_id VARCHAR(100) UNIQUE,
customer_status VARCHAR(50),
verified_at TIMESTAMP,
created_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX idx_tsara_customer_id ON users(tsara_customer_id);Usage:
async function createUser(userData) {
const tsaraCustomer = await tsara.createCustomer({
type: 'individual',
first_name: userData.firstName,
last_name: userData.lastName,
email: userData.email,
phone: userData.phone,
bvn: userData.bvn
});
const user = await db.users.create({
email: userData.email,
tsara_customer_id: tsaraCustomer.data.id,
customer_status: tsaraCustomer.data.status
});
return user;
}8. Implement Webhook Handlers for Status Updates
Listen to customer verification webhooks to update status in real-time.
Why?
- Immediate status updates
- No polling required
- Better user experience
- Automatic workflow triggers
Implementation:
app.post('/webhooks/tsara', async (req, res) => {
const event = req.body;
res.status(200).send('OK');
if (event.type === 'customer.verified') {
const customerId = event.data.customer_id;
await db.users.update(
{ tsara_customer_id: customerId },
{
customer_status: 'verified',
verified_at: event.data.verified_at
}
);
await sendNotification(customerId, {
title: 'Account Verified!',
message: 'Your identity has been verified. You now have full access to all features.'
});
await enablePremiumFeatures(customerId);
}
if (event.type === 'customer.verification_failed') {
const customerId = event.data.customer_id;
await db.users.update(
{ tsara_customer_id: customerId },
{ customer_status: 'verification_failed' }
);
await sendNotification(customerId, {
title: 'Verification Failed',
message: event.data.failure_reason
});
}
});π§ Troubleshooting
1. "Duplicate Customer" Error
Symptoms:
- Create customer fails with 409 error
- Error message: "Customer with this email already exists"
Causes:
- Email or phone number already registered
- Previous registration attempt succeeded
- User trying to create multiple accounts
Solutions:
- Search for existing customer first:
curl -X GET "https://sandbox.tsara.ng/v1/[email protected]" \
-H "Authorization: Bearer YOUR_SECRET_KEY"- Implement duplicate check:
async function createCustomerSafely(customerData) {
try {
const customer = await tsara.createCustomer(customerData);
return customer;
} catch (error) {
if (error.status_code === 409) {
const existing = await tsara.listCustomers({
email: customerData.email
});
return existing.data[0];
}
throw error;
}
}- Show user-friendly message:
if (error.message.includes('already exists')) {
showMessage(
'An account with this email already exists. ' +
'Please log in or use a different email address.'
);
}2. "Invalid Phone Format" Error
Symptoms:
- Customer creation fails with 400 error
- Error message: "Phone number must be in international format"
Causes:
- Missing country code
- Wrong format (not starting with +)
- Spaces or special characters
Solutions:
- Validate phone format before sending:
function validatePhone(phone) {
const phoneRegex = /^\+234\d{10}$/;
if (!phoneRegex.test(phone)) {
throw new Error(
'Phone number must be in format: +2348012345678'
);
}
return phone;
}- Auto-format user input:
function formatNigerianPhone(input) {
let cleaned = input.replace(/\D/g, '');
if (cleaned.startsWith('0')) {
cleaned = '234' + cleaned.slice(1);
}
if (!cleaned.startsWith('234')) {
cleaned = '234' + cleaned;
}
return '+' + cleaned;
}
const formattedPhone = formatNigerianPhone('08012345678');- Provide clear UI guidance:
<input
type="tel"
placeholder="+2348012345678"
pattern="^\+234\d{10}$"
title="Phone number must start with +234 followed by 10 digits"
/>3. Verification Status Stuck at "Pending"
Symptoms:
- Customer status remains "pending_verification"
- No verification webhook received
- User can't access features requiring verification
Causes:
- Verification not initiated yet
- BVN/NIN/CAC not provided during creation
- Verification API not called
- Webhook endpoint not configured
Solutions:
- Check if verification was initiated:
curl -X GET "https://sandbox.tsara.ng/v1/customers?id=cus_123" \
-H "Authorization: Bearer YOUR_SECRET_KEY"- Initiate verification manually:
curl -X POST "https://sandbox.tsara.ng/v1/identity/verify/bvn" \
-H "Authorization: Bearer YOUR_SECRET_KEY" \
-d '{
"customer_id": "cus_123",
"bvn": "12345678901"
}'- Implement automatic verification after creation:
async function createAndVerifyCustomer(customerData) {
const customer = await tsara.createCustomer(customerData);
if (customerData.bvn) {
await tsara.verifyBVN({
customer_id: customer.data.id,
bvn: customerData.bvn
});
} else if (customerData.nin) {
await tsara.verifyNIN({
customer_id: customer.data.id,
nin: customerData.nin
});
}
return customer;
}4. "Customer Not Found" When Creating Reserved Account
Symptoms:
- Reserved account creation fails
- Error message: "Invalid identity_id" or "Customer not found"
Causes:
- Wrong customer ID used
- Customer not verified yet
- Customer is business type (reserved accounts require individual)
Solutions:
- Verify customer exists and is verified:
async function validateCustomerForReservedAccount(customerId) {
const customer = await tsara.getCustomer(customerId);
if (customer.type !== 'individual') {
throw new Error('Reserved accounts are only available for individual customers');
}
if (customer.status !== 'verified') {
throw new Error('Customer must be verified before creating reserved account');
}
return customer;
}- Show clear error messages:
try {
await validateCustomerForReservedAccount(customerId);
await createReservedAccount(customerId);
} catch (error) {
if (error.message.includes('must be verified')) {
showMessage(
'Please complete identity verification before creating your account. ' +
'Tap here to verify now.'
);
}
}5. BVN Name Mismatch During Verification
Symptoms:
- Verification fails immediately
- Error: "BVN does not match provided name"
- Webhook shows verification_failed
Causes:
- Name format differences (JOHN DOE vs John Doe)
- Missing middle names
- Typos in first_name or last_name
- BVN registered with different name
Solutions:
- Show BVN name to user before creation:
async function preVerifyBVN(bvn) {
const result = await tsara.lookupBVN(bvn);
return {
bvn_name: result.name,
dob: result.date_of_birth,
message: `We found this name on your BVN: ${result.name}. ` +
`Please ensure your registration details match exactly.`
};
}- Normalize names before comparison:
function normalizeNameForBVN(name) {
return name
.toUpperCase()
.replace(/\s+/g, ' ')
.trim();
}
const customerData = {
first_name: normalizeNameForBVN(formData.firstName),
last_name: normalizeNameForBVN(formData.lastName),
bvn: formData.bvn
};- Allow name correction after failed verification:
async function handleNameMismatch(customerId, bvnName) {
const [firstName, ...lastNameParts] = bvnName.split(' ');
const lastName = lastNameParts.join(' ');
await updateCustomer(customerId, {
first_name: firstName,
last_name: lastName
});
await retryVerification(customerId);
}6. Missing Metadata After Customer Creation
Symptoms:
- Customer created but metadata is null or empty
- Can't find customer by internal user ID
Causes:
- Metadata not included in create request
- Metadata object empty
- Metadata exceeded size limits
Solutions:
- Always include metadata in creation:
const requiredMetadata = {
internal_user_id: userId,
created_via: 'api',
timestamp: Date.now()
};
const customer = await tsara.createCustomer({
...customerData,
metadata: {
...requiredMetadata,
...optionalMetadata
}
});- Validate metadata before sending:
function validateMetadata(metadata) {
const keys = Object.keys(metadata);
if (keys.length > 10) {
throw new Error('Metadata cannot exceed 10 keys');
}
if (!metadata.internal_user_id) {
throw new Error('internal_user_id is required in metadata');
}
return metadata;
}- Update metadata if missing:
async function ensureMetadata(customerId, userId) {
const customer = await tsara.getCustomer(customerId);
if (!customer.metadata || !customer.metadata.internal_user_id) {
await tsara.updateCustomer(customerId, {
metadata: {
...customer.metadata,
internal_user_id: userId
}
});
}
}π Security Considerations
1. Protect Personally Identifiable Information (PII)
Handle customer data with appropriate security measures.
Implementation:
const SENSITIVE_FIELDS = ['bvn', 'nin', 'cac_number', 'tax_id'];
function maskSensitiveData(customer) {
const masked = { ...customer };
SENSITIVE_FIELDS.forEach(field => {
if (masked[field]) {
const value = masked[field];
masked[field] = value.substring(0, 3) + '*'.repeat(value.length - 3);
}
});
return masked;
}
app.get('/api/customers/:id', async (req, res) => {
const customer = await getCustomer(req.params.id);
const maskedCustomer = maskSensitiveData(customer);
res.json(maskedCustomer);
});2. Implement Role-Based Access Control
Restrict customer data access to authorized personnel only.
Implementation:
const PERMISSIONS = {
'customer_support': ['read'],
'admin': ['read', 'write', 'update'],
'developer': ['read']
};
function checkPermission(userRole, action) {
const allowed = PERMISSIONS[userRole] || [];
if (!allowed.includes(action)) {
throw new Error('Insufficient permissions');
}
}
app.post('/api/customers', authenticate, async (req, res) => {
checkPermission(req.user.role, 'write');
const customer = await createCustomer(req.body);
res.json(customer);
});3. Encrypt Customer Data at Rest
Store sensitive customer information encrypted in your database.
Implementation:
const crypto = require('crypto');
const ENCRYPTION_KEY = process.env.ENCRYPTION_KEY;
function encrypt(text) {
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv('aes-256-cbc', ENCRYPTION_KEY, iv);
let encrypted = cipher.update(text, 'utf8', 'hex');
encrypted += cipher.final('hex');
return iv.toString('hex') + ':' + encrypted;
}
function decrypt(text) {
const parts = text.split(':');
const iv = Buffer.from(parts[0], 'hex');
const encrypted = parts[1];
const decipher = crypto.createDecipheriv('aes-256-cbc', ENCRYPTION_KEY, iv);
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}
async function storeCustomerData(customer) {
await db.customers.create({
tsara_customer_id: customer.id,
bvn_encrypted: encrypt(customer.bvn),
email: customer.email
});
}4. Audit Log All Customer Operations
Track all customer creation, updates, and verification events.
Implementation:
async function auditLog(action, customerId, userId, changes) {
await db.audit_logs.create({
action: action,
customer_id: customerId,
performed_by: userId,
changes: JSON.stringify(changes),
ip_address: req.ip,
user_agent: req.headers['user-agent'],
timestamp: new Date()
});
}
async function createCustomer(customerData, userId) {
const customer = await tsara.createCustomer(customerData);
await auditLog('customer_created', customer.data.id, userId, {
type: customerData.type,
email: customerData.email
});
return customer;
}5. Implement Data Retention Policies
Automatically delete or anonymize customer data after specified periods.
Implementation:
async function anonymizeInactiveCustomers() {
const cutoffDate = new Date();
cutoffDate.setFullYear(cutoffDate.getFullYear() - 2);
const inactiveCustomers = await db.customers.findAll({
where: {
last_active: { lt: cutoffDate },
status: 'inactive'
}
});
for (const customer of inactiveCustomers) {
await db.customers.update(customer.id, {
first_name: 'ANONYMIZED',
last_name: 'USER',
email: `deleted_${customer.id}@anonymized.com`,
phone: null,
bvn_encrypted: null,
anonymized_at: new Date()
});
await auditLog('customer_anonymized', customer.id, 'system', {
reason: 'inactive_for_2_years'
});
}
}π Related Pages
- Identity Verification β Verify BVN, NIN, or CAC details
- Virtual Accounts β Create reserved accounts for verified customers
- Wallets β Link USDC wallets to customer profiles
- Bank Transfers β Send payouts to verified customers
- Webhooks Security β Secure your webhook endpoints for customer events
Next documentation page: Identity Verification - https://usetsara.readme.io/reference/identity-verification