Pass a metadata object when initializing a Paystack transaction. It accepts any JSON key-value pairs. Paystack stores this data and returns it in verify responses and webhook payloads at data.metadata. Use it to link transactions to your internal records (order IDs, user IDs, product names) without needing a separate mapping table.
When a Paystack webhook arrives at your server with a charge.success event, you need to know what to do with the payment. Which order does it belong to? Which user paid? What product should be delivered? You can answer these questions two ways:
Both approaches work. Metadata makes the second approach possible and can serve as a useful backup or supplement to reference-based lookups.
// Initialize with metadata
var response = await fetch('https://api.paystack.co/transaction/initialize', {
method: 'POST',
headers: {
Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
email: 'customer@example.com',
amount: 350000,
currency: 'NGN',
reference: 'order_2026_00142',
metadata: {
order_id: 'ORD-2026-00142',
user_id: 'usr_8f3k2n',
product_type: 'subscription',
plan: 'annual_pro',
},
}),
});
After payment, when you verify the transaction or receive a webhook, the metadata comes back in data.metadata:
// In your webhook handler
var metadata = event.data.metadata;
console.log(metadata.order_id); // 'ORD-2026-00142'
console.log(metadata.user_id); // 'usr_8f3k2n'
console.log(metadata.product_type); // 'subscription'
console.log(metadata.plan); // 'annual_pro'
The metadata field is a JSON object. You can nest objects and arrays within it, though flat structures are easier to work with.
// Flat structure (recommended)
metadata: {
order_id: 'ORD-2026-00142',
user_id: 'usr_8f3k2n',
items_count: 3,
source: 'web_checkout',
}
// Nested structure (works but harder to query)
metadata: {
order: {
id: 'ORD-2026-00142',
items_count: 3,
},
user: {
id: 'usr_8f3k2n',
tier: 'premium',
},
}
Flat structures are easier to read in the Paystack dashboard and simpler to extract in webhook handlers. Use flat keys unless nesting truly adds clarity.
Data types
Metadata values can be strings, numbers, booleans, arrays, or nested objects. However, when metadata comes back from Paystack (in verify responses or webhooks), all values are serialized as JSON. Be prepared for numbers to arrive as strings in some cases and always validate types in your handler.
// Be defensive when reading metadata values
var orderId = String(metadata.order_id);
var itemsCount = Number(metadata.items_count);
Get weekly developer tips
Join 25,000+ developers. Practical guides, job tips, and new content — straight to your inbox.
No spam. Unsubscribe anytime.
The custom_fields key is a special metadata structure. It is an array of objects, each with display_name, variable_name, and value. Paystack renders these on the checkout page and in the transaction details on your dashboard.
metadata: {
order_id: 'ORD-2026-00142',
user_id: 'usr_8f3k2n',
custom_fields: [
{
display_name: 'Order Number',
variable_name: 'order_number',
value: 'ORD-2026-00142',
},
{
display_name: 'Customer Name',
variable_name: 'customer_name',
value: 'Amina Okafor',
},
{
display_name: 'Delivery Address',
variable_name: 'delivery_address',
value: '15 Adeola Odeku, VI, Lagos',
},
],
}
Custom fields serve two audiences:
You can have both regular metadata keys (for your backend logic) and custom_fields (for display) in the same metadata object. They do not interfere with each other. For advanced custom field patterns, see collecting extra checkout information.
Paystack recognizes a special metadata key called cancel_action. If you set it to a URL, Paystack displays a "Cancel" button on the checkout page that redirects the customer to that URL when clicked. Without it, the cancel button closes the popup or navigates back.
metadata: {
cancel_action: 'https://yoursite.com/cart',
order_id: 'ORD-2026-00142',
}
This is useful when you want to redirect the customer to a specific page (like their cart or the product page) instead of just closing the checkout. It only works with redirect checkout; for inline/popup checkout, the onCancel callback handles the cancellation.
E-commerce order tracking
metadata: {
order_id: 'ORD-2026-00142',
cart_id: 'cart_9x8k2',
items_count: 3,
shipping_method: 'express',
coupon_code: 'SAVE20',
}
SaaS subscription activation
metadata: {
user_id: 'usr_8f3k2n',
plan: 'pro_annual',
workspace_id: 'ws_4m2n',
trial_conversion: true,
}
Event ticket purchase
metadata: {
event_id: 'evt_devfest_2026',
ticket_type: 'vip',
quantity: 2,
attendee_email: 'customer@example.com',
custom_fields: [
{
display_name: 'Event',
variable_name: 'event_name',
value: 'DevFest Lagos 2026',
},
{
display_name: 'Ticket Type',
variable_name: 'ticket_type',
value: 'VIP (x2)',
},
],
}
Marketplace or multi-vendor payment
metadata: {
vendor_id: 'vendor_abc',
platform_fee_percent: 10,
buyer_id: 'buyer_xyz',
listing_id: 'listing_789',
}
In each case, the metadata travels with the transaction. When the webhook arrives, you read the metadata to determine what to do: create tickets, activate a subscription, update an order status, or split funds between vendors.
Paystack does not publish an exact size limit for metadata, but the practical limit is a few kilobytes. Here are the guidelines:
A reasonable upper bound: keep the serialized metadata JSON under 5KB. Most practical use cases are well under 1KB.
// BAD: too much data in metadata
metadata: {
full_cart: [
{ name: 'Product 1', price: 5000, description: '500 words of text...' },
{ name: 'Product 2', price: 3000, description: '500 words of text...' },
// ... 20 more items
],
}
// GOOD: just the IDs, look up the rest from your database
metadata: {
cart_id: 'cart_9x8k2',
items_count: 22,
}
The webhook payload includes your metadata at data.metadata. Here is how to use it in your handler:
app.post('/webhooks/paystack', function(req, res) {
// Return 200 immediately
res.sendStatus(200);
var event = req.body;
if (event.event === 'charge.success') {
var metadata = event.data.metadata || {};
// Use metadata to determine what to do
if (metadata.product_type === 'subscription') {
activateSubscription(metadata.user_id, metadata.plan);
} else if (metadata.product_type === 'order') {
fulfillOrder(metadata.order_id);
} else {
// Fallback: look up by reference
processGenericPayment(event.data.reference);
}
}
});
Always have a fallback. If metadata is missing or malformed (which can happen in edge cases), fall back to looking up the transaction by its reference. Your system should never break because metadata is empty.
For the full webhook handling guide, see the complete webhook engineering guide.
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