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.
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:
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.
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:
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.
Get weekly developer tips
Join 25,000+ developers. Practical guides, job tips, and new content — straight to your inbox.
No spam. Unsubscribe anytime.
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:
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.
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 });
}
Paystack's risk action system lets you whitelist or blacklist customers. This affects how Paystack handles charges to that customer.
// 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.
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.
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;
}
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.
KES 2,999
The definitive guide to building payment systems with Paystack — from your first transaction to production-grade payment infrastructure across Africa. 9 modules, 47 lessons. Free preview available.
Start Paystack Course