Bonaventure OgetoBy Bonaventure Ogeto|

Paystack Transfer OTP Required Error

Paystack requires an OTP for every transfer by default. Your API call is not failing. It is working as designed. To automate transfers without manual OTP entry, call the POST /transfer/disable_otp endpoint, receive a confirmation OTP on your registered email or phone, then call POST /transfer/finalize_disable_otp with that OTP. After this, transfers will go through without requiring an OTP. This is a one-time setup per Paystack account.

What the Error Looks Like

You call the Paystack Transfer API to send money to a bank account. Instead of processing the transfer, Paystack returns this:

{
  "status": true,
  "message": "OTP is required to complete this transfer",
  "data": {
    "integration": 123456,
    "domain": "live",
    "transfer_code": "TRF_abc123def456",
    "status": "otp",
    "reference": "your-reference-123"
  }
}

Notice that status is true. This is not an HTTP error. Paystack accepted your transfer request but put it on hold until you provide an OTP. The transfer sits in an "otp" state and will not proceed until you either submit the OTP or disable OTP for your account entirely.

If you are building an automated system (payroll, vendor payments, wallet withdrawals), this blocks everything. You cannot manually enter an OTP for every transfer at 2 AM when your cron job runs.

Why Paystack Requires OTP by Default

OTP stands for one-time password. When OTP is enabled on your Paystack account, every transfer triggers a code sent to the account owner's registered phone number or email. You must submit that code to finalize the transfer.

Paystack enables OTP by default for a good reason: transfers move money out of your Paystack balance to external bank accounts. If someone gains access to your secret key, they could drain your entire balance by initiating transfers to their own accounts. OTP is the last line of defense against this.

Think of it this way:

  • Collecting payments (initializing transactions) puts money into your Paystack balance. Even if someone uses your key to collect payments, the money goes to you.
  • Sending transfers takes money out of your Paystack balance. If someone uses your key to initiate transfers, the money goes to them.

OTP protects the dangerous direction: money going out. That is why Paystack defaults to requiring it.

How to Disable OTP for Automated Transfers

Disabling OTP is a two-step process. First, you request to disable it. Paystack sends a confirmation OTP to your registered email or phone. Then you submit that OTP to finalize the disabling. After that, all future transfers process without OTP.

Step 1: Request to disable OTP

// Step 1: Call disable_otp
async function requestDisableOtp() {
  const response = await fetch('https://api.paystack.co/transfer/disable_otp', {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${process.env.PAYSTACK_SECRET_KEY}`,
      'Content-Type': 'application/json',
    },
  });

  const data = await response.json();
  console.log(data);
  // { status: true, message: "OTP has been sent to ... to confirm disabling of OTP ..." }
  return data;
}

After this call, check the email or phone number associated with your Paystack account. You will receive a code.

Step 2: Finalize with the OTP you received

// Step 2: Submit the OTP to finalize
async function finalizeDisableOtp(otp) {
  const response = await fetch('https://api.paystack.co/transfer/finalize_disable_otp', {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${process.env.PAYSTACK_SECRET_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ otp }),
  });

  const data = await response.json();
  console.log(data);
  // { status: true, message: "OTP requirement for transfers has been disabled" }
  return data;
}

Once Step 2 succeeds, all future transfers on this Paystack account will process without requiring an OTP. You do not need to repeat this process unless you re-enable OTP.

Complete Transfer Flow After Disabling OTP

With OTP disabled, your transfer code looks like this:

async function initiateTransfer(recipientCode, amount, reference, reason) {
  const response = 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: amount * 100, // Convert to kobo/pesewas
      recipient: recipientCode,
      reference,
      reason,
    }),
  });

  const data = await response.json();

  if (data.status && data.data.status === 'success') {
    console.log('Transfer completed:', data.data.transfer_code);
    return { success: true, data: data.data };
  }

  if (data.data?.status === 'otp') {
    // OTP is still required. The disable did not take effect.
    console.error('OTP still required. Run disable_otp and finalize_disable_otp again.');
    return { success: false, reason: 'otp_required' };
  }

  if (data.data?.status === 'pending') {
    // Transfer is queued and will be processed shortly
    console.log('Transfer pending:', data.data.transfer_code);
    return { success: true, pending: true, data: data.data };
  }

  return { success: false, message: data.message };
}

After disabling OTP, the transfer response will have a status of "success" (processed immediately) or "pending" (queued for processing). You will never see the "otp" status again unless you re-enable OTP.

Listen for transfer webhooks (transfer.success, transfer.failed, transfer.reversed) to know the final outcome of pending transfers. Do not assume a pending transfer will succeed.

Security Implications of Disabling OTP

When you disable OTP, you remove the last manual confirmation step between your code and your money leaving your Paystack account. This is a serious decision. Here is what you need to have in place before doing it:

  • Protect your secret key. Store it in environment variables, never in code, never in Git, never in frontend code. If someone has your secret key and OTP is disabled, they can empty your balance.
  • Restrict server access. The server that makes transfer calls should have limited access. Not every developer on your team needs access to the production server that handles payouts.
  • IP allowlisting. In your Paystack dashboard, go to Settings and enable IP allowlisting for your API. Only your production server's IP addresses should be allowed to make API calls. Even if your key leaks, requests from other IPs will be rejected.
  • Transfer limits. Set daily transfer limits on your Paystack dashboard. If your system normally sends KES 500,000 per day, set the limit to KES 1,000,000. This caps the damage if something goes wrong.
  • Monitoring and alerts. Set up alerts for transfers above a threshold. If a transfer of KES 1,000,000 goes out and your normal transfers are KES 10,000 to KES 50,000, you want to know immediately.
  • Application-level approval. Even without Paystack OTP, you can build your own approval flow. Transfers above a certain amount require approval from a team member in your admin dashboard before your code calls the Paystack API.

If any of these safeguards are not in place, think twice before disabling OTP. The convenience of automated transfers is not worth the risk of losing your entire balance to a leaked key.

Re-Enabling OTP

If your account is compromised, or you decide the security trade-off is not worth it, re-enable OTP immediately:

async function enableOtp() {
  const response = await fetch('https://api.paystack.co/transfer/enable_otp', {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${process.env.PAYSTACK_SECRET_KEY}`,
      'Content-Type': 'application/json',
    },
  });

  const data = await response.json();
  console.log(data);
  // { status: true, message: "OTP requirement for transfers has been enabled" }
  return data;
}

This takes effect immediately. All future transfers will require OTP again. Any transfers that are already in a "pending" state will still process without OTP, but new transfers will be blocked until the OTP is provided.

If you suspect your account has been compromised:

  1. Call POST /transfer/enable_otp immediately.
  2. Rotate your secret key in the Paystack dashboard (Settings > API Keys > Generate New Key).
  3. Update your server with the new key.
  4. Review recent transfers in the dashboard for any unauthorized activity.
  5. Contact Paystack support if you find unauthorized transfers.

Handling OTP in Test Mode

In test mode, the disable OTP flow works the same way. You call disable_otp, receive a simulated OTP, and call finalize_disable_otp. No real money moves and no real OTP is sent to your phone.

Test mode and live mode have separate OTP settings. Disabling OTP in test mode does not disable it in live mode. You need to run the process in both environments.

A common workflow:

  1. Build your transfer feature using test keys with OTP enabled. Verify your code handles the "otp" status gracefully.
  2. Disable OTP in test mode. Verify transfers process without OTP.
  3. Deploy to production with live keys. OTP is still enabled in live mode.
  4. Run the disable OTP flow once in live mode (this requires manual action since you need to enter the real OTP sent to your phone/email).
  5. Verify live transfers process without OTP.

Do not hardcode the disable OTP flow in your application startup. It is a one-time manual operation, not something your code should run on every deployment.

Troubleshooting Common Issues

OTP still required after calling disable_otp. You called disable_otp but never called finalize_disable_otp. The first call only sends the OTP to you. The second call actually disables it. Check your email or phone for the code and submit it.

Finalize returns "Invalid OTP." The OTP has expired (they are time-limited, usually 30 minutes). Call disable_otp again to get a fresh OTP, then call finalize_disable_otp with the new code.

OTP disabled in test mode but still required in live mode. Test mode and live mode are separate. You need to run the disable flow in both environments using the respective keys.

Transfers stuck in "otp" status. Transfers that were created while OTP was enabled remain in the "otp" status. You cannot retroactively process them by disabling OTP. You have two options: submit the OTP for each pending transfer via POST /transfer/finalize_transfer, or cancel them and create new transfers after OTP is disabled.

Bulk transfers also require OTP. The bulk transfer endpoint (POST /transfer/bulk) follows the same OTP setting as individual transfers. If OTP is enabled, the entire batch will require OTP confirmation. Disable OTP once, and it applies to both individual and bulk transfers.

Further Reading

For more on Paystack transfers and troubleshooting:

Building automated payout systems is a core skill for fintech developers. The McTaba Full-Stack Software and AI Engineering course teaches you to build production payment systems with proper security from day one.

Key Takeaways

  • The OTP required response is not an error. Paystack sends an OTP to the account owner for every transfer by default. This is a security feature, not a bug in your code.
  • To automate transfers, call POST /transfer/disable_otp once. Paystack sends a confirmation OTP to your registered email or phone. Submit that OTP via POST /transfer/finalize_disable_otp. After this, transfers process without OTP.
  • Disabling OTP is a one-time action per Paystack account. You do not need to do it again unless you re-enable OTP.
  • Only disable OTP if your backend is secured properly. Without OTP, anyone with your secret key can move money out of your Paystack balance. Protect your secret key, restrict API access, and set up IP allowlisting on your dashboard.
  • You can re-enable OTP at any time by calling POST /transfer/enable_otp. If your account is compromised, this is the first thing to do.
  • Test the disable/finalize flow in test mode first. Paystack test mode simulates the OTP flow without sending real OTPs or moving real money.

Frequently Asked Questions

Is the OTP required response an error?
No. It is normal behavior. Paystack accepts the transfer request and returns status: true with a transfer_code. The transfer is created but paused, waiting for OTP confirmation. This is a security feature, not a bug. The HTTP status code is 200.
Do I need to disable OTP every time I make a transfer?
No. Disabling OTP is a one-time action per Paystack account. Once you call disable_otp and confirm with finalize_disable_otp, all future transfers on that account process without OTP. You only need to do it again if you re-enable OTP.
Can I disable OTP for some transfers and keep it for others?
No. OTP is an account-level setting. It is either on for all transfers or off for all transfers. If you want approval for high-value transfers, build that logic in your own application. Check the amount before calling the Paystack API and route large transfers through an internal approval flow.
What happens to transfers created while OTP was enabled?
Transfers created with OTP enabled stay in the "otp" status. Disabling OTP after the fact does not process them. You need to either submit the OTP for each pending transfer using the finalize_transfer endpoint, or let them expire and create new transfers after disabling OTP.
Is disabling OTP safe?
It is safe if your infrastructure is secured. Protect your secret key in environment variables, enable IP allowlisting on your Paystack dashboard, set transfer limits, and monitor for unusual activity. If any of these safeguards are missing, keep OTP enabled and process transfers manually.

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