Paystack Customer Objects: Create, Fetch, Update, Whitelist
Use POST /customer to create a customer explicitly, GET /customer/{email_or_code} to fetch one, PUT /customer/{code} to update details, and POST /customer/set_risk_action to whitelist or blacklist. Paystack auto-creates customers on first transaction, but explicit creation lets you attach metadata and validate identity before any payment happens.
What Is a Paystack Customer Object
Every person who pays through your Paystack integration gets a customer object. This object stores their email, name, phone number, metadata, transaction history, saved payment authorizations, and risk status. It is the central record Paystack keeps about someone interacting with your business.
Paystack creates this object automatically the first time someone pays. If a person with the email "ada@example.com" completes a checkout on your site, Paystack creates a customer with that email and assigns it a customer_code like CUS_abc123def. Every future transaction with that same email gets linked to the same customer object.
You can also create customer objects explicitly before any transaction happens. This is useful when you want to:
- Collect customer details during signup, before they pay
- Attach metadata (like your internal user ID) to the customer record
- Validate a customer's identity through BVN verification
- Whitelist a customer so future charges skip authorization prompts
The customer object is not the same as a user in your database. Think of it as Paystack's payment profile for someone. Your application's user table holds everything about a user. The Paystack customer object holds everything about that user's payment activity.
Creating a Customer
To create a customer explicitly, send a POST request to /customer with at least an email address. You can optionally include first_name, last_name, phone, and metadata.
// createCustomer.js
const https = require('https');
function createCustomer(customerData) {
const params = JSON.stringify({
email: customerData.email,
first_name: customerData.firstName,
last_name: customerData.lastName,
phone: customerData.phone || '',
metadata: {
internal_user_id: customerData.userId,
signup_source: customerData.source || 'web',
},
});
const options = {
hostname: 'api.paystack.co',
port: 443,
path: '/customer',
method: 'POST',
headers: {
Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY,
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(params),
},
};
return new Promise(function(resolve, reject) {
const req = https.request(options, function(res) {
var body = '';
res.on('data', function(chunk) { body += chunk; });
res.on('end', function() {
var parsed = JSON.parse(body);
if (parsed.status) {
// parsed.data.customer_code is your handle for this customer
resolve(parsed.data);
} else {
reject(new Error(parsed.message));
}
});
});
req.on('error', reject);
req.write(params);
req.end();
});
}
// Usage during user signup
async function onUserSignup(user) {
try {
var customer = await createCustomer({
email: user.email,
firstName: user.firstName,
lastName: user.lastName,
phone: user.phone,
userId: user.id,
});
// Store the customer_code in your database
await savePaystackCustomerCode(user.id, customer.customer_code);
console.log('Customer created: ' + customer.customer_code);
} catch (err) {
console.error('Failed to create Paystack customer:', err.message);
}
}
A few things to know about creation:
- Email uniqueness. If a customer with that email already exists on your Paystack integration, the API returns the existing customer object instead of creating a duplicate. This is safe to call repeatedly.
- Metadata is flexible. You can store any JSON-serializable data in the metadata field. Use it to link Paystack's customer to your internal user ID.
- Phone format. Include the country code. For Nigerian numbers, use the format +2348012345678. For Kenyan numbers, use +254712345678.
If you skip explicit creation and let Paystack auto-create customers during transactions, those customers will only have an email and a customer_code. No name, no phone, no metadata. You can update them later, but starting with explicit creation gives you cleaner data from day one.
Fetching Customer Details
To fetch a customer, send a GET request to /customer/{email_or_code}. You can use either the customer's email address or their customer_code. The customer_code is more reliable because emails can change.
// fetchCustomer.js
const https = require('https');
function fetchCustomer(emailOrCode) {
var options = {
hostname: 'api.paystack.co',
port: 443,
path: '/customer/' + encodeURIComponent(emailOrCode),
method: 'GET',
headers: {
Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY,
},
};
return new Promise(function(resolve, reject) {
var req = https.request(options, function(res) {
var body = '';
res.on('data', function(chunk) { body += chunk; });
res.on('end', function() {
var parsed = JSON.parse(body);
if (parsed.status) {
resolve(parsed.data);
} else {
reject(new Error(parsed.message));
}
});
});
req.on('error', reject);
req.end();
});
}
// Example: build a customer dashboard
async function getCustomerDashboard(customerCode) {
var customer = await fetchCustomer(customerCode);
return {
name: customer.first_name + ' ' + customer.last_name,
email: customer.email,
totalTransactions: customer.transactions.length,
savedCards: customer.authorizations.length,
subscriptions: customer.subscriptions.length,
riskAction: customer.risk_action,
createdAt: customer.createdAt,
};
}
The response includes several nested objects:
- transactions - an array of all transactions by this customer on your integration
- authorizations - saved payment methods (card tokens) the customer has used. Each authorization has a reusable flag showing whether you can charge it again without the customer being present.
- subscriptions - active and cancelled subscriptions for the customer
- metadata - whatever custom data you stored when creating or updating the customer
- risk_action - "default", "allow" (whitelisted), or "deny" (blacklisted)
You can also list all customers on your integration with GET /customer. This endpoint supports pagination with perPage and page query parameters. The default is 50 customers per page.
Updating a Customer
To update a customer, send a PUT request to /customer/{code}. You can update the first_name, last_name, phone, and metadata fields. You cannot change the email through the API.
// updateCustomer.js
const https = require('https');
function updateCustomer(customerCode, updates) {
var params = JSON.stringify({
first_name: updates.firstName,
last_name: updates.lastName,
phone: updates.phone,
metadata: updates.metadata,
});
var options = {
hostname: 'api.paystack.co',
port: 443,
path: '/customer/' + customerCode,
method: 'PUT',
headers: {
Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY,
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(params),
},
};
return new Promise(function(resolve, reject) {
var req = https.request(options, function(res) {
var body = '';
res.on('data', function(chunk) { body += chunk; });
res.on('end', function() {
var parsed = JSON.parse(body);
if (parsed.status) {
resolve(parsed.data);
} else {
reject(new Error(parsed.message));
}
});
});
req.on('error', reject);
req.write(params);
req.end();
});
}
// Sync your user profile changes to Paystack
async function syncUserToPaystack(user) {
if (!user.paystackCustomerCode) return;
await updateCustomer(user.paystackCustomerCode, {
firstName: user.firstName,
lastName: user.lastName,
phone: user.phone,
metadata: {
internal_user_id: user.id,
plan: user.subscriptionPlan,
last_synced: new Date().toISOString(),
},
});
}
The metadata field on update does a full replace, not a merge. If your existing metadata is { plan: "pro", source: "web" } and you update with { plan: "enterprise" }, the source field disappears. Always fetch the current metadata first and merge it yourself if you need to preserve existing fields.
// Safe metadata update that preserves existing fields
async function updateCustomerMetadata(customerCode, newFields) {
var customer = await fetchCustomer(customerCode);
var existingMeta = customer.metadata || {};
// Merge old and new
var mergedMeta = Object.assign({}, existingMeta, newFields);
await updateCustomer(customerCode, { metadata: mergedMeta });
}
Whitelisting and Blacklisting Customers
Paystack's risk action system lets you whitelist or blacklist customers. This affects how Paystack handles charges to that customer.
- Default - normal behavior. Customer goes through standard authorization for every charge.
- Allow (whitelist) - the customer is trusted. Charges using saved authorizations skip the standard checks. This is useful for recurring billing on trusted accounts.
- Deny (blacklist) - all charge attempts to this customer are blocked. Use this for fraud prevention.
// riskActions.js
const https = require('https');
function setRiskAction(customerCode, riskAction) {
// riskAction must be 'default', 'allow', or 'deny'
var params = JSON.stringify({
customer: customerCode,
risk_action: riskAction,
});
var options = {
hostname: 'api.paystack.co',
port: 443,
path: '/customer/set_risk_action',
method: 'POST',
headers: {
Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY,
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(params),
},
};
return new Promise(function(resolve, reject) {
var req = https.request(options, function(res) {
var body = '';
res.on('data', function(chunk) { body += chunk; });
res.on('end', function() {
var parsed = JSON.parse(body);
if (parsed.status) {
resolve(parsed.data);
} else {
reject(new Error(parsed.message));
}
});
});
req.on('error', reject);
req.write(params);
req.end();
});
}
// Whitelist a customer after identity verification
async function whitelistVerifiedCustomer(customerCode) {
await setRiskAction(customerCode, 'allow');
console.log('Customer ' + customerCode + ' whitelisted');
}
// Blacklist a customer after fraud detection
async function blacklistCustomer(customerCode, reason) {
await setRiskAction(customerCode, 'deny');
// Log the action in your system
await logSecurityEvent({
type: 'customer_blacklisted',
customerCode: customerCode,
reason: reason,
timestamp: new Date().toISOString(),
});
}
Whitelisting is not something to do casually. When you whitelist a customer, charges on their saved authorizations go through with less friction. This is powerful for subscription billing or wallet top-ups where you have already verified the customer's identity. But if a bad actor gains access to a whitelisted account, they can drain funds faster.
A sensible pattern: whitelist only after BVN validation or after a customer has completed several successful transactions without disputes. Never whitelist all customers by default.
Blacklisting is your emergency brake. If a customer files multiple fraudulent chargebacks, if their card is stolen and used on your platform, or if your fraud detection flags suspicious activity, blacklist first and investigate later. You can always set the action back to "default" once you clear the customer.
Customer Validation and Identity Verification
Paystack supports identity verification through the customer validation endpoint. In Nigeria, this means BVN (Bank Verification Number) validation. You send the customer's identity details, and Paystack verifies them against the bank's records.
// validateCustomer.js
const https = require('https');
function validateCustomer(customerCode, identityData) {
var params = JSON.stringify({
country: identityData.country, // e.g., 'NG'
type: identityData.type, // e.g., 'bvn'
value: identityData.value, // The BVN number
account_number: identityData.accountNumber,
bank_code: identityData.bankCode,
first_name: identityData.firstName,
last_name: identityData.lastName,
});
var options = {
hostname: 'api.paystack.co',
port: 443,
path: '/customer/' + customerCode + '/identification',
method: 'POST',
headers: {
Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY,
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(params),
},
};
return new Promise(function(resolve, reject) {
var req = https.request(options, function(res) {
var body = '';
res.on('data', function(chunk) { body += chunk; });
res.on('end', function() {
var parsed = JSON.parse(body);
resolve(parsed);
});
});
req.on('error', reject);
req.write(params);
req.end();
});
}
This is an asynchronous operation. The API returns immediately with a 200 status, but the actual verification happens in the background. Paystack sends the result to your webhook URL as either a customeridentification.success or customeridentification.failed event.
// Handle the webhook for identity verification result
function handleIdentityWebhook(event) {
if (event.event === 'customeridentification.success') {
var customerCode = event.data.customer_code;
console.log('Identity verified for ' + customerCode);
// Now safe to whitelist this customer
setRiskAction(customerCode, 'allow');
}
if (event.event === 'customeridentification.failed') {
var customerCode = event.data.customer_code;
console.log('Identity verification failed for ' + customerCode);
// Flag for manual review
flagForReview(customerCode, 'identity_verification_failed');
}
}
Customer validation is particularly important for Dedicated Virtual Accounts (DVAs). Paystack requires identity verification before it assigns a DVA to a customer in Nigeria. Without a validated identity, the DVA creation will fail.
Linking Paystack Customers to Your Database
The most important design decision with customer objects is how you link them to your own user records. There are two clean approaches:
Option 1: Store the customer_code in your user table
Add a paystack_customer_code column to your users table. When you create a Paystack customer (or when one is auto-created during a first transaction), store the code. This gives you a direct lookup path: user ID to customer_code and back.
Option 2: Store your user ID in Paystack's metadata
When creating or updating a customer, include your internal user ID in the metadata field. When you receive webhooks about transactions, the customer object in the webhook payload includes this metadata. You can look up your user without a separate API call.
The best approach is to do both. Store the customer_code in your database for outbound API calls (when you need to fetch or charge a customer). Store your user ID in Paystack metadata for inbound webhooks (when Paystack tells you something happened and you need to find the right user).
// Complete customer sync function
async function ensurePaystackCustomer(user) {
// If we already have a customer code, fetch and update
if (user.paystackCustomerCode) {
await updateCustomer(user.paystackCustomerCode, {
firstName: user.firstName,
lastName: user.lastName,
phone: user.phone,
metadata: { internal_user_id: user.id },
});
return user.paystackCustomerCode;
}
// Otherwise create a new customer
var customer = await createCustomer({
email: user.email,
firstName: user.firstName,
lastName: user.lastName,
phone: user.phone,
userId: user.id,
source: 'sync',
});
// Save the code to your database
await db.query(
'UPDATE users SET paystack_customer_code = $1 WHERE id = $2',
[customer.customer_code, user.id]
);
return customer.customer_code;
}
Common Patterns and Pitfalls
Duplicate customers. If two parts of your system create customers with the same email, Paystack returns the same customer object both times. This is safe. But if a user changes their email in your app and you create a new Paystack customer with the new email, you now have two separate customer objects. The old one still has the transaction history and saved cards. Plan for email changes by updating your stored customer_code when this happens.
Listing customers with pagination. GET /customer returns paginated results. When you need to process all customers (for a migration or audit), page through them properly:
async function getAllCustomers() {
var allCustomers = [];
var page = 1;
var hasMore = true;
while (hasMore) {
var response = await fetch(
'https://api.paystack.co/customer?perPage=100&page=' + page,
{
headers: {
Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY,
},
}
);
var data = await response.json();
allCustomers = allCustomers.concat(data.data);
// Check if there are more pages
if (data.data.length < 100) {
hasMore = false;
} else {
page++;
}
}
return allCustomers;
}
Rate limiting. Paystack applies rate limits to API calls. If you are syncing thousands of customers, add a small delay between requests. A safe approach is to wait 200ms between calls.
Metadata size. Paystack does not document an official metadata size limit, but keep it reasonable. Store identifiers and flags, not large blobs of data. If you need to associate large amounts of data with a customer, store it in your own database and use the customer_code as the linking key.
For the full picture of how customer objects fit into the payment flow, see the complete guide to accepting payments with Paystack.
Key Takeaways
- ✓Paystack auto-creates a customer object the first time an email appears in a transaction. You do not need to create customers manually before charging them.
- ✓Explicit customer creation via POST /customer lets you attach metadata, phone numbers, and names before any payment happens. This is useful for onboarding flows.
- ✓Every customer gets a unique customer_code (CUS_xxxxx). Use this code for updates and risk actions, not the email address, because emails can change.
- ✓The whitelist/blacklist system uses POST /customer/set_risk_action. Whitelisting skips authorization checks on future charges. Blacklisting blocks all charges to that customer.
- ✓Fetching a customer returns their full transaction history, subscriptions, and authorizations. This is your single source of truth for what a customer has done on your platform.
- ✓Customer validation via POST /customer/{code}/identification triggers BVN or other identity verification. You receive the result via the customeridentification.success or customeridentification.failed webhook.
Frequently Asked Questions
- Do I need to create a customer before initializing a transaction?
- No. Paystack auto-creates a customer object when you initialize a transaction with an email that does not exist yet. Explicit creation is only needed when you want to attach metadata, validate identity, or set risk actions before the first payment.
- Can I change a customer email on Paystack?
- No. The email field cannot be changed through the Customer Update API. If a user changes their email in your application, you will need to create a new customer with the new email and update your stored customer_code reference.
- What happens if I whitelist a customer?
- Whitelisted customers (risk_action set to "allow") have charges on saved authorizations processed with fewer friction checks. This does not bypass all security. It signals to Paystack that you trust this customer. Use it after BVN verification or after a clean transaction history.
- Is customer_code the same across test and live mode?
- No. Test mode and live mode are completely separate environments. A customer created in test mode has a different customer_code than the same email in live mode. Do not mix test and live customer codes.
- How do I find all transactions for a specific customer?
- Fetch the customer with GET /customer/{email_or_code}. The response includes a transactions array with all their transactions. You can also use GET /transaction with the customer query parameter set to the customer ID for paginated results with more filtering options.
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