Customers

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

TypeDescriptionRequired InfoVerification MethodUse Case
IndividualPersonal customersName, email, phone, BVN/NINBVN or NIN verificationUser wallets, reserved accounts, personal transfers
BusinessCorporate entitiesBusiness name, email, CAC, TINCAC verificationBusiness 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

StatusDescriptionNext Actions
pending_verificationCustomer created, identity not verified yetInitiate verification via Identity API
verifiedIdentity successfully verifiedCan create reserved accounts, full platform access
rejectedVerification failed or documents invalidReview 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

ParameterTypeRequiredDescription
typestringRequiredMust be individual for personal customers
first_namestringRequiredCustomer's first name (2-50 characters)
last_namestringRequiredCustomer's last name (2-50 characters)
emailstringRequiredValid email address (used for notifications)
phonestringRequiredPhone number in international format (e.g., +2348012345678)
bvnstringOptional11-digit Bank Verification Number (required for BVN verification)
ninstringOptional11-digit National Identification Number (alternative to BVN)
date_of_birthstringOptionalDate of birth in YYYY-MM-DD format
addressstringOptionalResidential address
citystringOptionalCity of residence
statestringOptionalState of residence
countrystringOptionalCountry code (e.g., NG for Nigeria)
metadataobjectOptionalCustom 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

FieldTypeDescription
successbooleanWhether request was successful
dataobjectCustomer details
data.idstringUnique customer ID (e.g., cus_123) - use this for all future operations
data.typestringCustomer type (individual)
data.first_namestringCustomer's first name
data.last_namestringCustomer's last name
data.emailstringCustomer's email address
data.phonestringCustomer's phone number
data.bvnstringBVN (masked for security: 223344****7)
data.date_of_birthstringDate of birth
data.addressstringResidential address
data.statusstringVerification status: pending_verification, verified, or rejected
data.metadataobjectCustom metadata you provided
data.created_atstringISO 8601 timestamp
data.updated_atstringISO 8601 timestamp
request_idstringUnique 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

ParameterTypeRequiredDescription
typestringRequiredMust be business for corporate customers
business_namestringRequiredRegistered business name (5-100 characters)
emailstringRequiredBusiness email address
phonestringRequiredBusiness phone number in international format
cac_numberstringRequiredCorporate Affairs Commission registration number (e.g., RC123456)
tax_idstringOptionalTax Identification Number (TIN)
industrystringOptionalBusiness industry (e.g., technology, retail, finance)
addressstringOptionalBusiness address
citystringOptionalCity of operation
statestringOptionalState of operation
countrystringOptionalCountry code (default: NG)
contact_personobjectOptionalPrimary contact person details
contact_person.first_namestringOptionalContact person's first name
contact_person.last_namestringOptionalContact person's last name
contact_person.emailstringOptionalContact person's email
contact_person.phonestringOptionalContact person's phone
metadataobjectOptionalCustom 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

FieldTypeDescription
successbooleanWhether request was successful
dataobjectCustomer details
data.idstringUnique customer ID (e.g., cus_789)
data.typestringCustomer type (business)
data.business_namestringRegistered business name
data.emailstringBusiness email
data.phonestringBusiness phone
data.cac_numberstringCAC registration number
data.tax_idstringTax identification number
data.industrystringBusiness industry
data.addressstringBusiness address
data.contact_personobjectContact person details
data.statusstringVerification status
data.metadataobjectCustom metadata
data.created_atstringCreation timestamp
data.updated_atstringLast update timestamp
request_idstringRequest 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

ParameterTypeRequiredDescription
idstringRequiredCustomer 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

FieldTypeDescription
successbooleanWhether request was successful
dataobjectCustomer details (same structure as create response)
data.idstringCustomer ID
data.typestringCustomer type
data.statusstringCurrent verification status
data.verification_detailsobjectVerification results (if verified)
data.walletsarrayAssociated wallet IDs
data.reserved_accountsarrayAssociated 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

ParameterTypeRequiredDescription
pageintegerOptionalPage number (default: 1)
limitintegerOptionalResults per page (default: 20, max: 100)
typestringOptionalFilter by type: individual or business
statusstringOptionalFilter by status: pending_verification, verified, or rejected
emailstringOptionalFilter by email address
phonestringOptionalFilter 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

FieldTypeDescription
successbooleanWhether request was successful
dataarrayArray of customer objects
data[].idstringCustomer ID
data[].typestringCustomer type
data[].emailstringCustomer email
data[].statusstringVerification status
data[].created_atstringCreation timestamp
paginationobjectPagination details
pagination.pageintegerCurrent page
pagination.limitintegerResults per page
pagination.totalintegerTotal customers
pagination.total_pagesintegerTotal 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

ParameterTypeRequiredDescription
idstringRequiredCustomer ID to update

Request Parameters

ParameterTypeDescription
emailstringNew email address
phonestringNew phone number
addressstringUpdated address
citystringUpdated city
statestringUpdated state
metadataobjectUpdated 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 TypeDescriptionWhen Triggered
customer.verifiedCustomer identity verified successfullyBVN/NIN/CAC verification succeeds
customer.verification_failedCustomer verification failedInvalid BVN/NIN/CAC or verification error

Webhook Payload Structure

FieldTypeDescription
idstringUnique webhook event ID
typestringEvent type
dataobjectCustomer details
data.customer_idstringCustomer ID
data.statusstringNew customer status
data.verification_methodstringMethod used (bvn, nin, or cac)
data.verified_atstringVerification timestamp
data.failure_reasonstringReason 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 CodeMessageCauseSolution
400Invalid email formatEmail is malformedUse valid email format
400Invalid phone formatPhone not in international formatUse format: +2348012345678
400Invalid BVN formatBVN is not 11 digitsEnsure BVN is exactly 11 digits
400Invalid CAC numberCAC format is wrongUse format: RC123456
400Missing required fieldRequired field not providedCheck all required fields are included
401UnauthorizedInvalid or missing API keyCheck Authorization header with Secret Key
404Customer not foundCustomer ID doesn't existVerify customer ID is correct
409Duplicate customerEmail or phone already existsUse existing customer or different email/phone
422Validation errorData validation failedCheck 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:

  1. User signs up on your platform
  2. Collect personal information and BVN
  3. Create customer via API
  4. Initiate BVN verification via Identity API
  5. Listen for webhook to confirm verification
  6. 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:

  1. Create customer with personal details
  2. Verify identity (required for reserved accounts)
  3. Create reserved virtual account linked to customer
  4. Customer receives permanent account number
  5. 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:

  1. Seller submits business registration
  2. Create business customer with CAC details
  3. Verify CAC via Identity API
  4. Approve seller account when verified
  5. 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:

  1. Create customer during wallet creation
  2. Link wallet ID to customer metadata
  3. Track all wallet transactions under customer profile
  4. Generate customer-specific reports
  5. 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:

  1. Create customers with complete KYC data
  2. Verify all identities before transactions
  3. Track transaction volumes per customer
  4. Generate compliance reports
  5. 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:

  1. Search for existing customer first:
curl -X GET "https://sandbox.tsara.ng/v1/[email protected]" \
  -H "Authorization: Bearer YOUR_SECRET_KEY"
  1. 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;
  }
}
  1. 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:

  1. 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;
}
  1. 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');
  1. 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:

  1. Check if verification was initiated:
curl -X GET "https://sandbox.tsara.ng/v1/customers?id=cus_123" \
  -H "Authorization: Bearer YOUR_SECRET_KEY"
  1. 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"
  }'
  1. 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:

  1. 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;
}
  1. 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:

  1. 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.`
  };
}
  1. 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
};
  1. 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:

  1. 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
  }
});
  1. 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;
}
  1. 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


Next documentation page: Identity Verification - https://usetsara.readme.io/reference/identity-verification