Paystack Card Preauthorization for Holds and Deposits
Paystack supports preauthorization through a two-step flow: first charge a small amount (like 50 NGN) to validate the card and get an authorization code, then charge the real amount later using that authorization code. You can also use partial debit to attempt a charge and capture only what the card allows. Full hold-and-capture (like Stripe auth and capture) is not natively supported in the same way, so you work with authorization codes.
How Preauthorization Works on Paystack
Traditional payment processors offer a two-phase card charge: authorize (hold the funds) and capture (take the funds). Paystack works differently. There is no explicit "hold" endpoint that freezes funds on a customer's card without moving money.
Instead, Paystack gives you authorization codes. When a customer pays with a card, the verify response includes an authorization object with an authorization_code. You can use this code to charge the same card again in the future without the customer re-entering details.
The preauthorization pattern on Paystack is:
- Validate: Charge a small amount (the minimum allowed, like 50 NGN) to verify the card is real and active
- Store: Save the authorization code from the successful charge
- Refund: Optionally refund the validation charge
- Charge later: When you know the final amount, charge the card using the authorization code
This is not a true hold (the customer's available balance is not reduced until you charge), but it verifies the card and gives you the ability to charge later. It is how ride-hailing apps, hotel booking platforms, and equipment rental services implement "save card for later" flows on Paystack.
The Validation Charge Flow
Step 1: Initialize a transaction for a small validation amount.
// Server: initialize a validation charge
app.post('/api/validate-card', async function(req, res) {
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: req.body.email,
amount: 5000, // 50 NGN - validation amount
currency: 'NGN',
channels: ['card'], // Card only for preauth
metadata: {
type: 'card_validation',
user_id: req.body.userId,
},
}),
});
var data = await response.json();
res.json({
access_code: data.data.access_code,
reference: data.data.reference,
});
});
Step 2: After the customer pays, verify and extract the authorization code.
// Webhook handler: capture the authorization code
app.post('/webhooks/paystack', async function(req, res) {
res.sendStatus(200);
var event = req.body;
if (event.event === 'charge.success') {
var metadata = event.data.metadata || {};
if (metadata.type === 'card_validation') {
// Save the authorization code for later charges
var authCode = event.data.authorization.authorization_code;
var cardBin = event.data.authorization.bin;
var cardLast4 = event.data.authorization.last4;
var cardBank = event.data.authorization.bank;
await db.savedCards.create({
user_id: metadata.user_id,
authorization_code: authCode,
card_display: cardBin + '****' + cardLast4,
bank: cardBank,
});
// Refund the validation charge
await fetch('https://api.paystack.co/refund', {
method: 'POST',
headers: {
Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
transaction: event.data.reference,
}),
});
}
}
});
Step 3: When you know the final amount, charge the saved card.
// Charge the saved card later
async function chargeCustomer(userId, amountInKobo) {
var savedCard = await db.savedCards.findByUserId(userId);
var response = await fetch('https://api.paystack.co/transaction/charge_authorization', {
method: 'POST',
headers: {
Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
authorization_code: savedCard.authorization_code,
email: savedCard.email,
amount: amountInKobo,
currency: 'NGN',
}),
});
var data = await response.json();
if (data.data.status === 'success') {
return { success: true, reference: data.data.reference };
} else {
return { success: false, message: data.data.gateway_response };
}
}
Deposit and Hold Patterns
Pattern 1: Refundable deposit
Charge the full deposit amount, save the authorization code, and refund the deposit when the customer returns the item or completes the service. This works for equipment rentals, key deposits, or event catering deposits.
// Charge deposit
var depositResult = await initializeAndRedirect({
email: customer.email,
amount: 1000000, // 10,000 NGN deposit
metadata: {
type: 'rental_deposit',
rental_id: rental.id,
},
});
// Later, when rental is returned undamaged
await fetch('https://api.paystack.co/refund', {
method: 'POST',
headers: {
Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
transaction: rental.deposit_reference,
}),
});
Pattern 2: Validate now, charge later
Charge a token amount (50 NGN), refund it immediately, then charge the real amount when the service is complete. This is the ride-hailing pattern: the app validates the card when the customer books, then charges the final fare after the trip.
Pattern 3: Charge the estimate, refund the difference
When the final amount is lower than the initial charge (the estimate was higher), you can issue a partial refund for the difference. When the final amount is higher, you charge the difference using the saved authorization code.
Security Considerations
Authorization codes give you the power to charge a customer's card without their direct interaction. This comes with significant responsibility.
- Store auth codes securely: Treat authorization codes like passwords. Store them encrypted in your database. Do not log them, do not include them in error messages, and do not send them to the frontend.
- Limit access: Only your payment processing code should be able to read auth codes. Not your admin dashboard, not your analytics pipeline, not your support tools.
- Communicate charges clearly: Before charging a saved card, notify the customer (via email, SMS, or in-app notification). Unexpected charges lead to chargebacks and lost customers.
- Let customers remove saved cards: Provide a way for customers to delete their saved payment methods. When they do, delete the auth code from your database.
- Audit charges: Log every charge attempt (without the auth code itself) so you can trace what happened if a customer disputes a charge.
For disputes and chargebacks, see the developer playbook for disputes.
Designing the Customer Experience
The validation charge pattern can confuse customers if not communicated well. Here is what good communication looks like:
Before the validation charge: Tell the customer exactly what will happen. "We will charge NGN 50 to verify your card. This charge will be refunded within 24 hours."
After the validation charge: Confirm the card was saved. "Your card ending in 4081 has been saved. The NGN 50 validation charge will be refunded."
Before a deferred charge: Notify the customer. "Your trip has ended. We are charging NGN 2,500 to your card ending in 4081."
After a charge failure: Tell the customer their card needs updating. "We could not charge your saved card. Please update your payment method to continue."
The deposit pattern is clearer because the customer expects to pay the deposit. Still, confirm when the deposit will be refunded and the conditions for keeping it.
Alternatives to Preauthorization
If the preauthorization pattern feels heavy for your use case, consider these alternatives:
- Charge the full amount upfront: If you know the amount, just charge it. This is simpler than validation-then-charge-later.
- Use Paystack subscriptions: If you are charging on a recurring schedule, Paystack's subscription API handles card saving and recurring charges for you.
- Manual card saving in Paystack dashboard: For simple "save card for later" without preauth, you can use the customer API to associate cards with customers after their first payment.
For wallet and top-up flows that use saved cards, see building a wallet top-up flow. For the complete guide to the Charge API that powers deferred charges, see Charge API for custom checkout.
Key Takeaways
- ✓Paystack does not have a traditional hold-and-capture like Stripe. Instead, you validate a card with a small charge, get an authorization code, and charge the full amount later.
- ✓The authorization code (auth_code) is returned after a successful card payment. You can use it to charge the same card again without the customer re-entering their details.
- ✓For deposits and holds, charge a small validation amount (like 50 or 100 NGN), refund it, then charge the real amount when the service is complete.
- ✓Authorization codes can expire or become invalid if the customer replaces their card. Always handle charge failures gracefully and have a fallback to re-collect card details.
- ✓Partial debit lets you attempt to charge an amount and capture whatever the card allows if the full amount is not available. This is useful for installment or wallet top-up flows.
- ✓Always communicate clearly with the customer about the validation charge, the refund, and the final charge. Unexpected charges erode trust fast.
Frequently Asked Questions
- Does Paystack support true hold-and-capture like Stripe?
- Not in the same way. Stripe lets you authorize without capturing and then capture within 7 days. Paystack charges immediately when you call the charge or initialize endpoints. The workaround is to charge a small validation amount, save the auth code, and charge the real amount later.
- How long does an authorization code last?
- Authorization codes remain valid as long as the customer card is active. They can become invalid when the card expires, is replaced, or is cancelled by the cardholder. There is no published expiration period from Paystack, so always handle charge failures gracefully.
- Can I charge any amount with a saved authorization code?
- You can charge any amount up to the limits of the card and your Paystack account. There is no link between the original validation amount and what you can charge later. However, charging significantly more than the customer expects will likely result in a chargeback.
- Will the customer get an OTP prompt when I charge their saved card?
- It depends on the card issuer and the charge amount. Some banks require OTP for every transaction, making background charges fail. Others allow merchant-initiated charges without OTP. Paystack handles the response. If an OTP is required, the charge will return a status that indicates further action is needed, and you will need to involve the customer.
- Is the validation charge visible on the customer bank statement?
- Yes. The validation charge appears on the customer statement like any other transaction. If you refund it, the refund also appears. Some banks show both immediately; others may show the charge and the refund at different times.
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