Transfer OTP: Enabling, Disabling and Automating
Paystack OTP for transfers is a security measure that sends a one-time code to the business owner for confirmation before a transfer processes. To disable it for automation, call POST /transfer/disable_otp, receive a final OTP, then call POST /transfer/disable_otp_finalize with that code. After that, all transfers process without manual confirmation. Re-enable anytime with POST /transfer/enable_otp.
How Transfer OTP Works
When OTP is enabled on your Paystack account and you initiate a transfer, Paystack does not process it immediately. Instead, the transfer enters an otp status, and Paystack sends a one-time password to the phone number or email registered on your Paystack business account. You must submit that OTP to finalize the transfer.
// Step 1: Initiate transfer (OTP enabled)
var transferResponse = await fetch('https://api.paystack.co/transfer', {
method: 'POST',
headers: {
Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
source: 'balance',
amount: 500000,
recipient: 'RCP_abc123',
reason: 'Vendor payout',
reference: 'payout_001',
}),
});
var data = await transferResponse.json();
// data.data.status => "otp"
// data.data.transfer_code => "TRF_xyz789"
// Step 2: Submit OTP (received via SMS or email)
var finalizeResponse = await fetch('https://api.paystack.co/transfer/finalize_transfer', {
method: 'POST',
headers: {
Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
transfer_code: data.data.transfer_code,
otp: '928374', // The code from SMS/email
}),
});
This is a deliberate friction point. Paystack wants to make sure that the person initiating the transfer is authorized to move money out of the account. Even if someone gets your API secret key, they still need access to the registered phone or email to complete a transfer.
The OTP is valid for a limited time. If you do not submit it, the transfer eventually expires and the reserved funds return to your available balance.
When OTP Makes Sense and When It Does Not
OTP works well for:
- Manual transfers from an admin dashboard where a human is sitting at the screen
- Low-volume payouts where someone reviews each one individually
- High-value, infrequent transfers where the extra confirmation step is worth the delay
- Early-stage products where you have not built your own approval controls yet
OTP does not work for:
- Automated payout systems that run on a schedule (cron jobs, queue workers)
- Real-time payouts triggered by webhooks (customer pays, vendor gets settled immediately)
- Bulk payouts where submitting OTP for each transfer is impractical
- Any system where no human is present to receive and enter the code
If you are building anything that needs to send money without a human typing in a code, you need to disable OTP. But before you do that, understand what you are giving up and what you need to build in its place.
Disabling OTP via the API
Disabling OTP is a two-step process. Paystack sends you one final OTP to confirm you actually want to disable the feature. This prevents someone who stole your API key from silently disabling OTP and then draining your account.
// Step 1: Request OTP disabling
var disableResponse = await fetch('https://api.paystack.co/transfer/disable_otp', {
method: 'POST',
headers: {
Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY,
'Content-Type': 'application/json',
},
});
var disableData = await disableResponse.json();
console.log(disableData.message);
// "OTP has been sent to your phone/email. Call disable_otp_finalize to complete."
// Step 2: Finalize with the OTP you received
var finalizeResponse = await fetch('https://api.paystack.co/transfer/disable_otp_finalize', {
method: 'POST',
headers: {
Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
otp: '482916', // The code from SMS/email
}),
});
var finalizeData = await finalizeResponse.json();
console.log(finalizeData.message);
// "OTP requirement for transfers has been disabled"
After this, all future transfers skip the OTP step. When you initiate a transfer, the status goes straight to pending instead of otp. Processing starts immediately.
You can also disable OTP through the Paystack dashboard under Settings > Transfers. The dashboard flow is identical: you click disable, receive an OTP, enter it, and OTP is off.
Re-Enabling OTP
If you need to turn OTP back on (security incident, change in process, testing), one API call does it:
var enableResponse = await fetch('https://api.paystack.co/transfer/enable_otp', {
method: 'POST',
headers: {
Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY,
'Content-Type': 'application/json',
},
});
var enableData = await enableResponse.json();
console.log(enableData.message);
// "OTP requirement for transfers has been enabled"
This takes effect immediately. The next transfer you initiate will require OTP. Any transfers already in the pending state continue processing normally. Only new transfers are affected.
Consider building a "panic button" in your admin dashboard that re-enables OTP with one click. If you detect suspicious activity or a potential key compromise, you want to be able to lock down transfers in seconds, not minutes.
Security Trade-Offs of Disabling OTP
With OTP enabled, stealing your API key is not enough to steal your money. The attacker also needs access to your phone or email. With OTP disabled, the API key is the only thing standing between an attacker and your balance. That is the trade-off in plain terms.
What you lose:
- A second factor of authentication on every transfer
- Protection against API key theft
- A forced human review point for every outgoing transfer
What you must implement to compensate:
- Secret key security: Store the key in environment variables, never in code. Use a secrets manager (AWS Secrets Manager, Google Secret Manager, or Vercel environment variables) in production. Rotate the key if anyone who had access leaves the team.
- IP allowlisting: If Paystack supports IP restrictions for your account, configure them so API calls can only come from your server's IP addresses.
- Application-level approval: Build maker-checker workflows, approval dashboards, or batch review steps in your own code. See the next section.
- Transfer limits: Implement per-transfer and daily aggregate limits in your application code. If a single transfer exceeds the limit, block it and alert your team.
- Monitoring and alerting: Set up alerts for unusual transfer patterns: transfers at odd hours, transfers to new recipients, transfers above your normal range, or a sudden spike in transfer volume.
Building Application-Level Approval Controls
The goal is to replace OTP (a Paystack-level control) with your own controls that are better suited to automated systems. The most common pattern is maker-checker.
// Maker-checker payout approval flow
// Step 1: Maker creates payout requests
async function createPayoutRequest(makerId, recipientCode, amount, reason) {
var payout = await db.payouts.create({
makerId: makerId,
recipientCode: recipientCode,
amount: amount,
reason: reason,
status: 'pending_approval',
createdAt: new Date(),
});
return payout;
}
// Step 2: Checker approves (must be a different person)
async function approvePayout(payoutId, checkerId) {
var payout = await db.payouts.findById(payoutId);
if (payout.makerId === checkerId) {
throw new Error('Checker cannot be the same person as maker');
}
if (payout.status !== 'pending_approval') {
throw new Error('Payout is not awaiting approval');
}
// Check daily limits
var todayTotal = await db.payouts.sumApprovedToday(checkerId);
if (todayTotal + payout.amount > 5000000) { // 50,000 NGN daily limit
throw new Error('Daily approval limit exceeded');
}
await db.payouts.update(payoutId, {
status: 'approved',
checkerId: checkerId,
approvedAt: new Date(),
});
}
// Step 3: Automated system picks up approved payouts and sends them
async function processApprovedPayouts() {
var approved = await db.payouts.findByStatus('approved');
for (var i = 0; i < approved.length; i++) {
await processPayout(approved[i].id);
}
}
This gives you more control than OTP ever did. You can enforce that the person who creates the payout is different from the person who approves it. You can set daily limits per approver. You can require multiple approvers for high-value transfers. And the entire approval trail is in your database for auditing.
For a complete approval workflow design, see payout approval workflows and maker-checker controls.
Testing OTP Changes Safely
Test OTP enable/disable behavior in Paystack's test mode before touching your live account. In test mode, transfers do not move real money, so you can experiment without risk.
// Use your test secret key for OTP experiments
// PAYSTACK_SECRET_KEY=sk_test_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
// 1. Check current OTP status by initiating a test transfer
var testTransfer = await fetch('https://api.paystack.co/transfer', {
method: 'POST',
headers: {
Authorization: 'Bearer ' + process.env.PAYSTACK_TEST_SECRET_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
source: 'balance',
amount: 10000,
recipient: 'RCP_test_recipient',
reference: 'otp_test_' + Date.now(),
}),
});
var result = await testTransfer.json();
console.log('Transfer status: ' + result.data.status);
// "otp" means OTP is enabled
// "pending" means OTP is disabled
After verifying the behavior in test mode, follow the same steps on your live account. Keep your live OTP changes to a planned window. Communicate with your team before disabling OTP on production, and make sure your application-level controls are deployed and tested before you flip the switch.
Key Takeaways
- ✓OTP is enabled by default on all Paystack accounts. Every transfer requires a one-time code sent to the registered phone or email before it processes.
- ✓Disabling OTP is a two-step API process: request disabling with POST /transfer/disable_otp, then confirm with the final OTP via POST /transfer/disable_otp_finalize.
- ✓With OTP disabled, any valid API call with your secret key can move money out of your account. This is the core security trade-off.
- ✓Re-enable OTP anytime with POST /transfer/enable_otp. It takes effect immediately for all subsequent transfers.
- ✓Replace OTP with application-level controls: maker-checker workflows, IP allowlisting, transfer amount limits, and approval dashboards.
- ✓Never disable OTP without first implementing strong secret key management. If your key leaks, there is no human checkpoint to stop unauthorized transfers.
Frequently Asked Questions
- Does disabling OTP affect all transfers or just bulk transfers?
- Disabling OTP affects all transfers on your Paystack account: single transfers, bulk transfers, and any transfers initiated through the dashboard. There is no way to disable OTP selectively for some transfers while keeping it for others.
- Can I disable OTP through the Paystack dashboard instead of the API?
- Yes. Go to your Paystack dashboard under Settings > Transfers, and you will find the option to enable or disable OTP. The dashboard flow sends you a confirmation OTP just like the API flow.
- What happens to in-progress transfers when I enable or disable OTP?
- Changing the OTP setting only affects new transfers. Transfers already in the otp state (waiting for confirmation) continue to require OTP submission. Transfers already in the pending state continue processing. The change applies to the next transfer you initiate.
- Is there a way to require OTP only for transfers above a certain amount?
- Paystack does not support conditional OTP based on transfer amount. It is all-or-nothing. To implement amount-based controls, disable OTP on Paystack and build your own approval logic that requires human confirmation for transfers above your threshold.
- If my API key is compromised and OTP is disabled, what should I do?
- Immediately re-enable OTP via the dashboard (do not use the potentially compromised key). Then rotate your API key from the Paystack dashboard. Check your recent transfer list for any unauthorized transfers. Contact Paystack support if you find suspicious activity.
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