Using Paystack Custom Fields to Collect Extra Checkout Information
To collect extra information at checkout with Paystack, add a custom_fields array inside the metadata object when initializing a transaction. Each field needs a display_name (what the customer sees), a variable_name (the machine-readable key), and a value. These fields appear on the Paystack checkout page and come back in webhook payloads under data.metadata.custom_fields.
What Custom Fields Are and Why They Exist
When you initialize a Paystack transaction, the API accepts a metadata object where you can attach any JSON data you want. Paystack stores that metadata and returns it in verify responses and webhook payloads. Custom fields are a specific structure within metadata that Paystack treats differently from everything else: it actually displays them.
Regular metadata is invisible to customers. If you attach {"order_id": "ORD-142"} to a transaction, that data exists in the API response but never shows up on a checkout page or in a dashboard-friendly format. Custom fields solve this. When you structure data as a custom_fields array inside metadata, Paystack renders each field on the checkout page, includes them in the dashboard transaction view, and makes them available on email receipts.
This matters for three audiences:
- Customers see what they are paying for. A field like "Delivery Address: 14 Moi Avenue, Nairobi" on the checkout page confirms the order details before they enter their card.
- Support teams can open a transaction in the Paystack dashboard and immediately see context. No need to cross-reference your internal admin panel.
- Your backend can extract structured data from webhooks using the
variable_namekey, which is more reliable than parsing free-form metadata.
Display Name vs Variable Name
Every custom field has three required properties, and understanding each one prevents confusion later.
display_name
This is what humans see. It appears as the label on the checkout page, on email receipts, and in the Paystack dashboard. Use natural language with proper capitalization: "Delivery Address", "Phone Number", "Order Notes". This value is purely for display and has no functional purpose in your code.
variable_name
This is what your code uses. It is the machine-readable key you will search for when processing webhook data. Use snake_case with no spaces, no special characters, and no uppercase letters: delivery_address, phone_number, order_notes. Be consistent across your application. If you call it phone_number in one place and customer_phone in another, your webhook handler needs to check for both.
value
The actual data. This is always a string. If you need to pass a number, convert it to a string first. If you need structured data (like multiple items in a cart), consider using regular metadata for that and custom fields only for the human-readable summary.
// Good: clear, consistent naming
const customFields = [
{
display_name: 'Phone Number',
variable_name: 'phone_number',
value: '+254712345678',
},
{
display_name: 'Delivery Address',
variable_name: 'delivery_address',
value: '14 Moi Avenue, Nairobi',
},
];
// Bad: inconsistent, unclear
const badFields = [
{
display_name: 'Phone',
variable_name: 'Phone Number', // spaces, capitalization
value: 254712345678, // number instead of string
},
];
Adding Custom Fields When Initializing a Transaction
Custom fields go inside the metadata object you pass when calling /transaction/initialize. The structure is an array under the key custom_fields.
const https = require('https');
function initializeWithCustomFields(email, amount, orderDetails) {
const params = JSON.stringify({
email: email,
amount: amount, // in kobo
reference: 'order_' + orderDetails.orderId + '_' + Date.now(),
metadata: {
// Regular metadata for your backend
order_id: orderDetails.orderId,
user_id: orderDetails.userId,
// Custom fields for display
custom_fields: [
{
display_name: 'Order Number',
variable_name: 'order_number',
value: orderDetails.orderId,
},
{
display_name: 'Phone Number',
variable_name: 'phone_number',
value: orderDetails.phone,
},
{
display_name: 'Delivery Address',
variable_name: 'delivery_address',
value: orderDetails.address,
},
{
display_name: 'Special Instructions',
variable_name: 'special_instructions',
value: orderDetails.notes || 'None',
},
],
},
});
const options = {
hostname: 'api.paystack.co',
port: 443,
path: '/transaction/initialize',
method: 'POST',
headers: {
Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY,
'Content-Type': 'application/json',
},
};
return new Promise(function(resolve, reject) {
const req = https.request(options, function(res) {
let data = '';
res.on('data', function(chunk) { data += chunk; });
res.on('end', function() { resolve(JSON.parse(data)); });
});
req.on('error', reject);
req.write(params);
req.end();
});
}
// Usage
initializeWithCustomFields(
'customer@example.com',
250000, // 2,500 NGN
{
orderId: 'ORD-2026-00387',
userId: 'usr_9k2m',
phone: '+234812345678',
address: '22 Herbert Macaulay Way, Yaba, Lagos',
notes: 'Please leave at the gate',
}
).then(function(result) {
console.log(result.data.authorization_url);
});
Notice how the metadata object contains both regular properties (order_id, user_id) and the custom_fields array. The regular properties are for your backend logic. The custom fields are what shows up on the checkout page.
Common Custom Field Patterns
Here are the custom fields that come up most often in real integrations, with the variable names we recommend standardizing on.
E-commerce delivery
var deliveryFields = [
{ display_name: 'Order Number', variable_name: 'order_number', value: 'ORD-2026-00387' },
{ display_name: 'Delivery Address', variable_name: 'delivery_address', value: '22 Herbert Macaulay Way, Yaba' },
{ display_name: 'Phone Number', variable_name: 'phone_number', value: '+234812345678' },
{ display_name: 'Delivery Notes', variable_name: 'delivery_notes', value: 'Call when arriving' },
];
Event tickets
var ticketFields = [
{ display_name: 'Event', variable_name: 'event_name', value: 'Lagos Tech Summit 2026' },
{ display_name: 'Ticket Type', variable_name: 'ticket_type', value: 'VIP' },
{ display_name: 'Attendee Name', variable_name: 'attendee_name', value: 'Amina Bello' },
{ display_name: 'Number of Tickets', variable_name: 'ticket_count', value: '2' },
];
Service bookings
var bookingFields = [
{ display_name: 'Service', variable_name: 'service_name', value: 'Home Cleaning - 3 Bedrooms' },
{ display_name: 'Date', variable_name: 'booking_date', value: '2026-07-25' },
{ display_name: 'Time Slot', variable_name: 'time_slot', value: '9:00 AM - 12:00 PM' },
{ display_name: 'Contact Phone', variable_name: 'contact_phone', value: '+254700123456' },
];
Donations and fundraising
var donationFields = [
{ display_name: 'Campaign', variable_name: 'campaign_name', value: 'School Building Fund' },
{ display_name: 'Donor Name', variable_name: 'donor_name', value: 'Anonymous' },
{ display_name: 'Dedication', variable_name: 'dedication', value: 'In memory of Dr. Osei' },
];
Notice that all values are strings. Even "Number of Tickets" is passed as the string "2", not the number 2. This keeps the structure predictable and prevents serialization issues.
Extracting Custom Fields from Webhook Data
When Paystack sends a charge.success webhook to your server, the custom fields arrive inside data.metadata.custom_fields as an array. The structure looks exactly like what you sent in, so you can loop through and match on variable_name.
// Express webhook handler
var express = require('express');
var crypto = require('crypto');
var app = express();
app.post('/webhooks/paystack', express.json(), function(req, res) {
// Verify webhook signature first
var hash = crypto
.createHmac('sha512', process.env.PAYSTACK_SECRET_KEY)
.update(JSON.stringify(req.body))
.digest('hex');
if (hash !== req.headers['x-paystack-signature']) {
return res.status(401).send('Invalid signature');
}
var event = req.body;
if (event.event === 'charge.success') {
var data = event.data;
var metadata = data.metadata || {};
var customFields = metadata.custom_fields || [];
// Extract specific fields by variable_name
var phone = getCustomFieldValue(customFields, 'phone_number');
var address = getCustomFieldValue(customFields, 'delivery_address');
var notes = getCustomFieldValue(customFields, 'special_instructions');
console.log('Phone:', phone);
console.log('Address:', address);
console.log('Notes:', notes);
// Fulfill the order with this information
fulfillOrder(data.reference, {
phone: phone,
address: address,
notes: notes,
amount: data.amount,
email: data.customer.email,
});
}
res.sendStatus(200);
});
function getCustomFieldValue(fields, variableName) {
for (var i = 0; i < fields.length; i++) {
if (fields[i].variable_name === variableName) {
return fields[i].value;
}
}
return null;
}
The getCustomFieldValue helper function is worth keeping in a shared utility. You will use it every time you process a webhook that includes custom fields. A few things to watch for:
- Always check that
metadataandcustom_fieldsexist before trying to iterate. Some transactions might not have metadata at all. - If a field is missing (the customer was charged through a flow that did not include it),
getCustomFieldValuereturnsnull. Handle this gracefully instead of letting your fulfillment logic crash. - The same custom field data is available in the verify response at
data.data.metadata.custom_fields. If your callback handler also needs it, you can extract it there too.
What the Webhook Payload Looks Like
Seeing the actual structure helps more than any explanation. Here is a trimmed charge.success webhook payload for a transaction that included custom fields:
{
"event": "charge.success",
"data": {
"id": 3456789012,
"reference": "order_ORD-2026-00387_1721472000000",
"amount": 250000,
"currency": "NGN",
"status": "success",
"customer": {
"email": "customer@example.com",
"customer_code": "CUS_abc123def456"
},
"metadata": {
"order_id": "ORD-2026-00387",
"user_id": "usr_9k2m",
"custom_fields": [
{
"display_name": "Order Number",
"variable_name": "order_number",
"value": "ORD-2026-00387"
},
{
"display_name": "Phone Number",
"variable_name": "phone_number",
"value": "+234812345678"
},
{
"display_name": "Delivery Address",
"variable_name": "delivery_address",
"value": "22 Herbert Macaulay Way, Yaba, Lagos"
},
{
"display_name": "Special Instructions",
"variable_name": "special_instructions",
"value": "Please leave at the gate"
}
]
}
}
}
Notice the structure: data.metadata contains both the regular properties (order_id, user_id) and the custom_fields array. Everything you sent in comes back exactly as you structured it. The regular metadata properties are flat key-value pairs at the top level of metadata. The custom fields are nested in the custom_fields array with their three-property structure intact.
Custom Fields vs Regular Metadata: When to Use Each
Both custom fields and regular metadata attach data to a transaction. The difference is visibility.
| Feature | Regular Metadata | Custom Fields |
|---|---|---|
| Shows on checkout page | No | Yes |
| Shows in dashboard | As raw JSON | As formatted labels |
| Shows on email receipt | No | Yes |
| Available in webhooks | Yes | Yes |
| Available in verify response | Yes | Yes |
| Structure | Any JSON | Array of {display_name, variable_name, value} |
Use regular metadata when:
- The data is only for your backend (internal user IDs, cart hashes, feature flags)
- You need nested objects or arrays
- The information is sensitive and should not appear on the checkout page (like internal pricing tiers or margin calculations)
Use custom fields when:
- Customers should see the data before they pay (order summary, delivery address, booking time)
- Your support team needs quick context in the dashboard without opening your admin panel
- You want the data on email receipts
Use both together when:
- You need backend-only data AND customer-facing display. Put internal IDs in regular metadata and display-worthy data in custom fields.
For a deeper look at metadata patterns beyond custom fields, see passing custom data through a transaction.
Gotchas and Practical Limits
Custom fields are straightforward, but a few things catch people.
Values must be strings
If you pass a number as the value, Paystack may accept it, but the behavior in the dashboard and on receipts can be inconsistent. Always convert to a string: use String(quantity) or amount.toString() before passing.
Keep the list short
There is no documented maximum number of custom fields, but the checkout page has limited space. Four or five fields is a reasonable ceiling. Beyond that, the checkout page looks cluttered and the customer starts scrolling past walls of text instead of paying. If you have a lot of data, put the essentials in custom fields and the rest in regular metadata.
Do not use custom fields for secret data
Custom fields show up on the checkout page. If you are passing an internal reference like a database primary key or a pricing tier identifier that you do not want the customer to see, use regular metadata instead.
Special characters in values
Values can contain most characters, including commas, periods, and spaces. However, extremely long values (multi-paragraph text) can break the layout on the checkout page. Keep values concise. If a customer needs to enter lengthy special instructions, consider collecting that in your own form and only displaying a summary in the custom field.
The cancel_action metadata key
Paystack recognizes one special metadata key outside of custom fields: cancel_action. Setting metadata.cancel_action to a URL tells Paystack where to redirect the customer if they close the checkout without paying. This is not a custom field. It is a separate metadata property.
Key Takeaways
- ✓Custom fields live inside the metadata object as a custom_fields array. Each entry has three required properties: display_name, variable_name, and value.
- ✓Paystack displays custom fields on the checkout page below the payment form, so customers can see order details like delivery address or booking reference before they pay.
- ✓The variable_name should use snake_case with no spaces or special characters. This is the key you will use to look up the value in your webhook handler.
- ✓Custom fields also appear in the Paystack dashboard transaction detail view, making them useful for customer support teams who need context without opening your internal tools.
- ✓In webhook payloads, custom fields arrive at data.metadata.custom_fields as an array. Loop through the array and match on variable_name to extract specific values.
- ✓You can combine custom fields with regular metadata properties in the same request. Custom fields are for display; regular metadata is for backend logic that does not need to be shown to anyone.
- ✓There is no hard limit on the number of custom fields, but keep the list short. Too many fields clutter the checkout page and slow down the payment experience.
Frequently Asked Questions
- Can I make custom fields editable by the customer on the checkout page?
- No. Custom fields in Paystack are display-only. The values you set when initializing the transaction are what the customer sees. If you need the customer to enter information like their phone number or address, collect it in your own form before initializing the transaction, then pass the collected data as custom field values.
- Do custom fields count toward any payload size limit?
- Paystack does not publish a specific size limit for the metadata object. In practice, keeping total metadata under a few kilobytes has never caused issues. If you are attaching large amounts of data, store it in your own database and pass only a reference ID in the metadata.
- Are custom fields available in the Paystack mobile SDK?
- Yes. When you initialize a transaction server-side with custom fields in the metadata, the fields are attached to the transaction regardless of which SDK or checkout method the customer uses. The server-side initialization is what matters. The mobile SDK, inline JS, and redirect checkout all carry the same metadata.
- Can I update custom fields after a transaction is initialized?
- No. Once a transaction is initialized, the metadata (including custom fields) is locked. If you need to change the information, you would need to initialize a new transaction with the updated custom fields and a new reference.
- Do custom fields show up in the Paystack CSV export?
- Paystack includes metadata in their transaction export, but the exact format depends on the export type. Custom fields may appear as part of a JSON string in the metadata column. For clean reporting, extract custom field values in your webhook handler and store them in your own database columns where you can query and export them in any format you need.
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