Bonaventure OgetoBy Bonaventure Ogeto|

Initialize Transaction vs Charge API: Which Paystack Endpoint to Use

Use Initialize Transaction (/transaction/initialize) for most integrations. It gives you a hosted checkout page, handles PCI compliance, and works with all payment channels. Use the Charge API (/charge) only when you need a fully custom checkout UI, are charging saved authorization codes, or need to handle mobile money without redirecting the customer.

The Two Endpoints at a Glance

Paystack's payment API has two main entry points. Every payment you accept goes through one of them.

POST /transaction/initialize

You send the amount, customer email, and optional parameters. Paystack returns an authorization URL and an access code. You send the customer to that URL (full redirect) or open a popup using the access code (Paystack Inline JS). The customer picks a payment method and completes the transaction on Paystack's hosted page. When they finish, Paystack redirects them back to your callback URL and fires a webhook.

POST /charge

You send the amount, customer email, and the payment details (card object, bank account, mobile money number, or USSD details). Paystack processes the charge directly. There is no redirect to a Paystack-hosted page. Depending on the bank's requirements, Paystack may respond with a status that requires follow-up actions: submitting an OTP, a PIN, a phone number, or redirecting for 3DS verification.

There is also a third endpoint worth mentioning here: POST /transaction/charge_authorization. This one charges a saved authorization code (a card token from a previous successful payment). It is technically part of the Initialize Transaction family but works without a checkout page.

Initialize Transaction in Detail

Here is a complete Initialize Transaction flow with all the common parameters.

const express = require('express');
const app = express();
app.use(express.json());

app.post('/api/pay', async (req, res) => {
  var email = req.body.email;
  var amountInNaira = req.body.amount;
  var orderId = req.body.orderId;

  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: email,
      amount: Math.round(amountInNaira * 100),
      reference: 'order_' + orderId + '_' + Date.now(),
      callback_url: 'https://yoursite.com/payment/callback',
      channels: ['card', 'bank_transfer'],
      currency: 'NGN',
      metadata: {
        order_id: orderId,
        custom_fields: [
          {
            display_name: 'Order ID',
            variable_name: 'order_id',
            value: orderId,
          },
        ],
      },
    }),
  });

  var data = await response.json();

  if (data.status) {
    res.json({
      authorization_url: data.data.authorization_url,
      access_code: data.data.access_code,
      reference: data.data.reference,
    });
  } else {
    res.status(400).json({ error: data.message });
  }
});

What you get back:

  • authorization_url: Full URL to Paystack's checkout page. Redirect the customer here, or use it as a link.
  • access_code: Short code to pass to Paystack Inline JS for a popup checkout experience.
  • reference: The transaction reference. If you provided one, Paystack echoes it back. If not, Paystack generates one.

What you do not need to handle:

  • Card number collection (Paystack's checkout page does this)
  • OTP or PIN prompts (Paystack handles them inside their checkout)
  • 3DS redirects (handled transparently within the checkout flow)
  • Bank transfer instructions (Paystack shows the generated account number)
  • Mobile money prompts (Paystack handles the provider-specific flow)

This is why Initialize Transaction is the recommended starting point. You write one POST request, and Paystack handles every payment channel and every bank-specific flow inside their checkout.

The Charge API in Detail

The Charge API puts you in control of the checkout experience. You collect payment details in your own UI and send them to Paystack directly. Here is a mobile money example.

app.post('/api/charge/mobile-money', async (req, res) => {
  var email = req.body.email;
  var phone = req.body.phone;
  var amount = req.body.amount;

  var response = await fetch('https://api.paystack.co/charge', {
    method: 'POST',
    headers: {
      Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      email: email,
      amount: Math.round(amount * 100),
      currency: 'GHS',
      mobile_money: {
        phone: phone,
        provider: 'MTN',
      },
    }),
  });

  var data = await response.json();

  // The Charge API returns a status that tells you what to do next
  var status = data.data.status;
  var chargeReference = data.data.reference;

  if (status === 'success') {
    // Rare for first attempt, but possible
    res.json({ status: 'complete', reference: chargeReference });
  } else if (status === 'send_otp') {
    // Customer received an OTP on their phone
    // You need to collect it and submit it
    res.json({ status: 'otp_required', reference: chargeReference });
  } else if (status === 'pay_offline') {
    // Customer needs to approve on their device
    res.json({
      status: 'pending_approval',
      reference: chargeReference,
      display_text: data.data.display_text,
    });
  } else {
    res.json({ status: status, reference: chargeReference });
  }
});

// Handle OTP submission
app.post('/api/charge/submit-otp', async (req, res) => {
  var otp = req.body.otp;
  var reference = req.body.reference;

  var response = await fetch('https://api.paystack.co/charge/submit_otp', {
    method: 'POST',
    headers: {
      Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      otp: otp,
      reference: reference,
    }),
  });

  var data = await response.json();

  if (data.data.status === 'success') {
    res.json({ status: 'complete', reference: reference });
  } else {
    res.json({ status: data.data.status });
  }
});

The multi-step nature of the Charge API

The Charge API is not a single request-response cycle. After your initial POST to /charge, the response status field tells you what happens next:

  • success: Charge completed. Verify and fulfill.
  • send_otp: Collect the OTP from the customer and POST to /charge/submit_otp.
  • send_pin: Collect the card PIN and POST to /charge/submit_pin.
  • send_phone: Collect the phone number linked to the card and POST to /charge/submit_phone.
  • send_birthday: Collect the cardholder's birthday and POST to /charge/submit_birthday.
  • open_url: Redirect the customer to data.data.url for 3DS verification.
  • pay_offline: The customer needs to complete the payment on their device (common for mobile money).
  • pending: Wait for a webhook or poll /charge/{reference}.

Each follow-up request may return yet another status. A card charge might go: send_pin then send_otp then open_url then success. Your code must handle the full state machine. For a complete implementation, see building a fully custom checkout.

Charging Saved Cards: charge_authorization

There is a third endpoint that falls between Initialize and Charge. When a customer has paid before and you have stored their authorization_code (with reusable: true), you can charge their card without any checkout UI.

app.post('/api/charge/saved-card', async (req, res) => {
  var customerId = req.body.customerId;

  // Look up the saved authorization code from your database
  var customer = await db.customers.findById(customerId);
  var authCode = customer.paystackAuthorizationCode;

  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: authCode,
        email: customer.email,
        amount: 500000, // 5,000 NGN
        reference: 'renewal_' + customerId + '_2026_07',
      }),
    }
  );

  var data = await response.json();

  if (data.data.status === 'success') {
    // Payment completed, no UI needed
    res.json({ status: 'complete', reference: data.data.reference });
  } else {
    // Authorization failed (card expired, blocked, etc.)
    // Prompt customer to re-enter card details
    res.json({ status: 'failed', message: data.data.gateway_response });
  }
});

This endpoint is simpler than /charge because it does not have the multi-step flow. The customer already authenticated their card during the initial payment. The response is either success or failed. There is no OTP, PIN, or 3DS step (the bank already trusts the authorization code).

Use charge_authorization for:

  • Subscription renewals
  • Wallet auto-top-ups
  • Recurring billing
  • "Pay with saved card" buttons in your UI

Always check authorization.reusable === true before storing an authorization code. Not all cards support reusable authorizations.

Decision Matrix: Which Endpoint for Which Use Case

Here is a straightforward decision guide. Find your use case and use the recommended endpoint.

Use Case Endpoint Why
First-time payment on a web app Initialize Transaction Fastest setup, PCI compliant, all channels supported
E-commerce checkout Initialize Transaction Customers expect a standard checkout flow
Subscription first payment Initialize Transaction Capture the authorization code from the verify response for future renewals
Subscription renewal charge_authorization No UI needed, charge the saved card directly
Saved card payment ("Pay with Visa ending 4081") charge_authorization Customer already authenticated, just charge the token
Custom mobile app checkout Charge API Mobile apps often need full control over the payment UI
Mobile money without redirect (Ghana) Charge API Collect phone number in your UI and charge directly
Background/automated charges charge_authorization No human interaction, server-to-server
Kiosk or POS-style payment Charge API Custom hardware UI, no browser available
Payment page (no code) Neither (use Paystack Dashboard) Paystack Payment Pages are a dashboard feature, no API needed

The general rule: if the customer is present and it is their first payment, use Initialize Transaction. If the customer is not present or has paid before, use charge_authorization. If you need a custom checkout UI, use the Charge API.

Technical Comparison

Here are the practical differences that affect your code.

PCI compliance

Initialize Transaction: Paystack handles all card data. Your server never sees card numbers. You are PCI compliant by default.

Charge API: If you are collecting card details in your UI and passing them to /charge, card data passes through your system. You take on PCI compliance responsibilities. Most developers using the Charge API use it for non-card channels (mobile money, bank) or for saved authorization codes, which avoids this issue.

Payment channels

Initialize Transaction: Supports all channels through the hosted checkout. The customer picks their preferred method. You do not need separate code for each channel.

Charge API: Each payment method has a different request format. Card charges send a card object. Mobile money charges send a mobile_money object. Bank charges send a bank object. You write different code paths for each channel you want to support.

Error handling

Initialize Transaction: Errors at the checkout level (wrong card number, declined transaction) are handled by Paystack's UI. The customer sees an error message on the checkout page and can retry. Your code only sees the final result (success or failure) when you verify.

Charge API: You handle every error state yourself. If the bank sends send_otp, you need a UI to collect the OTP. If it sends open_url, you need to redirect. If the charge fails, you need to show the error message. You build the entire error-handling flow.

Integration time

Initialize Transaction: A working integration takes 30 to 60 minutes. One POST request on the server, one redirect or popup on the frontend, one verify call on the callback.

Charge API: A complete custom checkout (handling all status codes, all channels, all error states) can take days. Each payment method has its own flow, and you need to test each one.

Using Both Endpoints Together

The two endpoints are not mutually exclusive. Many production applications use both. Here is a common pattern.

First payment: Initialize Transaction

When a customer pays for the first time (signs up for a subscription, makes their first purchase), use Initialize Transaction. The customer goes through Paystack's hosted checkout, enters their card details, and completes 3DS verification. You verify the transaction and extract the authorization code from the response.

Subsequent payments: charge_authorization

For subscription renewals, repeat purchases, or "pay with saved card," charge the stored authorization code. No checkout UI needed.

// First payment: use Initialize Transaction
app.post('/api/subscribe', async (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: 500000,
      reference: 'sub_first_' + req.body.userId + '_' + Date.now(),
      metadata: { user_id: req.body.userId, plan: 'monthly' },
    }),
  });

  var data = await response.json();
  res.json({ authorization_url: data.data.authorization_url });
});

// After verification, store the authorization code
// Then for renewals:
async function renewSubscription(customer) {
  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: customer.authCode,
        email: customer.email,
        amount: 500000,
        reference: 'sub_renew_' + customer.id + '_' + new Date().toISOString().slice(0, 7),
      }),
    }
  );

  var data = await response.json();

  if (data.data.status !== 'success') {
    // Card failed. Email the customer to update their payment method.
    // Next attempt should use Initialize Transaction again.
    await notifyCardFailed(customer);
  }

  return data;
}

This pattern gives you the best of both worlds: a smooth first payment experience with Paystack's hosted checkout, and frictionless renewals with saved cards. The two endpoints complement each other rather than compete.

For the complete guide to building on each endpoint, see the accept payments guide and Charge API for custom checkouts.

Stay Up to Date

Paystack evolves their API and adds new endpoints regularly. We keep these guides updated when things change.

Join the McTaba newsletter to get notified when we publish new Paystack integration guides or spot breaking changes you should know about.

Sign up for the McTaba newsletter

Key Takeaways

  • Initialize Transaction (POST /transaction/initialize) creates a hosted checkout session. You get an authorization URL and an access code. The customer completes payment on Paystack's page. This is the fastest path to accepting payments.
  • The Charge API (POST /charge) lets you send payment details directly. You build the checkout UI. You handle OTP prompts, PIN collection, and 3DS redirects. More control, more responsibility.
  • Initialize Transaction is PCI-compliant by default because card details never touch your server. The Charge API requires you to handle sensitive data carefully if you are collecting card details.
  • For charging saved cards (authorization codes), use POST /transaction/charge_authorization. This is a separate endpoint from /charge and is simpler for recurring payments.
  • The Charge API supports multi-step flows: send_otp, send_pin, send_phone, send_birthday, and open_url. You must handle all of these status codes if you go the custom checkout route.
  • Start with Initialize Transaction. Add Charge API flows later only for specific use cases that require it. The two endpoints are not mutually exclusive.

Frequently Asked Questions

Can I use Initialize Transaction for mobile apps?
Yes. You initialize the transaction on your server and open the authorization URL in a WebView or in-app browser on the mobile device. The customer completes payment on Paystack's checkout page inside the WebView. When they are redirected to your callback URL, you intercept the redirect in the WebView and verify the transaction. This is simpler than building a full custom checkout with the Charge API.
Is the Charge API faster than Initialize Transaction?
Not in terms of processing speed. Both endpoints go through the same banking infrastructure. The Charge API can feel faster for returning customers on saved cards because there is no redirect. But for first-time payments, the Charge API adds complexity (handling OTP, PIN, 3DS) that Initialize Transaction handles automatically.
What happens if I use the wrong endpoint?
You will not break anything. Using Initialize Transaction when you could use the Charge API just means the customer goes through the hosted checkout (which is fine). Using the Charge API when Initialize Transaction would suffice means you write more code to handle edge cases. The payment still works either way. Pick the one that matches your UX requirements.
Does the Charge API support all payment channels?
The Charge API supports card, bank, mobile_money, ussd, and bank_transfer. However, each channel requires a different request format with channel-specific fields. Initialize Transaction supports all channels through a single request format because Paystack's checkout handles channel selection and input collection.
Can I switch from Initialize Transaction to the Charge API later?
Yes. The two endpoints are independent. You can start with Initialize Transaction today and add Charge API flows later for specific use cases. Your existing transactions and authorization codes work the same regardless of which endpoint created them. Many production apps use Initialize Transaction for web checkout and the Charge API for mobile or background charges.

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