McTaba Labs logo
Software EngineeringThe WhiteboardProjectsEventsMasterclassesWebinarsLearn
Book Call

500+ developers already in the community

Get answers, find accountability partners, and stay ahead

Join Discord →
McTaba Labs logo

Educating 1 million students in tech by 2030.

Programs

  • HighSchool Graduates Launchpad
  • AI & Business Automation
  • Corporate Training
  • McTaba Story & Mission
  • Full-Stack Curriculum
  • Course Pricing & Plans
  • Frequently Asked Questions

Resources

  • Join Discord Community
  • Tech Events in Nairobi
  • Masterclasses
  • Free Resources
  • Learn
  • Campus
  • Africa Tech Talent Report
  • Partner With Us
  • Start a Project
  • Our Work

Contact Us

  • 124 Manyani East Road, Lavington, Nairobi
  • +254727795603
  • Book a Discovery Call

Office Hours:

Mon-Fri: 9am - 2pm EAT

Sat: 10am - 12pm EAT

© 2027 McTaba Labs - A product of Edtech Industries. All rights reserved.

Privacy PolicyTerms of ServiceRefund Policy
  1. Learn
  2. Paystack for African Developers
  3. Charging a Saved Card with Paystack Authorization Codes
Bonaventure OgetoBy Bonaventure Ogeto|Updated July 20, 2026

Charging a Saved Card with Paystack Authorization Codes

To charge a saved card on Paystack, call POST /transaction/charge_authorization with the customer's email, the amount in the smallest currency unit, and the authorization_code you stored from their first payment. Paystack processes the charge and returns a response immediately. Always generate a unique reference for each charge, verify the response status, and handle failures with a retry strategy.

In this article

  1. 1. What charge_authorization Does
  2. 2. The Basic Charge Flow
  3. 3. Generating Unique References
  4. 4. Handling the Response
  5. 5. Verifying After the Charge
  6. 6. A Production-Ready Charge Function
  7. 7. Common Mistakes and How to Avoid Them
  8. 8. Using Metadata for Charge Tracking

What charge_authorization Does

The charge_authorization endpoint is one of the most important endpoints in the Paystack API. It lets you charge a customer's card without them being present, without a checkout page, and without any user interaction. You provide an authorization code (a token representing their saved card), an amount, and an email. Paystack does the rest.

This is how manual recurring billing works on Paystack. The Subscriptions API uses this same mechanism internally, but when you call charge_authorization directly, you control the timing, the amount, and the retry logic.

Use cases that depend on this endpoint:

  • Usage-based billing where the amount changes each cycle
  • Instalment plans with irregular schedules (school fees, loan repayments)
  • Overage charges on top of a base subscription
  • One-click reorders or top-ups where the customer has saved a card
  • Any billing pattern where the Subscriptions API intervals are too rigid

This article is part of the subscriptions and recurring billing guide. For background on what authorization codes are and how they are created, see authorization codes explained.

The Basic Charge Flow

Charging a saved card is a single API call. Here is the minimal version:

var axios = require('axios');

async function chargeCard(email, amount, authorizationCode, reference) {
  var response = await axios.post(
    'https://api.paystack.co/transaction/charge_authorization',
    {
      email: email,
      amount: amount,
      authorization_code: authorizationCode,
      reference: reference,
      currency: 'NGN',
    },
    {
      headers: {
        Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY,
        'Content-Type': 'application/json',
      },
    }
  );

  return response.data.data;
}

The parameters:

  • email: The customer's email address. Must match the email associated with the authorization on Paystack.
  • amount: The charge amount in the smallest currency unit. 500000 means 5,000 NGN in kobo. See amount handling for currency details.
  • authorization_code: The token from the customer's first payment. Must be a reusable authorization.
  • reference: Your unique identifier for this charge. If you do not provide one, Paystack generates one. Always provide your own for traceability.
  • currency: The currency code. Must match the currency the authorization was created in.

The response comes back immediately for card charges. Unlike the Charge API (which can return send_otp or pending), charge_authorization returns either success or failed right away. The customer has already authenticated during their first payment, so no OTP or PIN is needed.

Get weekly developer tips

Join 25,000+ developers. Practical guides, job tips, and new content — straight to your inbox.

No spam. Unsubscribe anytime.

Generating Unique References

The reference field is your defense against double charges. If a network timeout causes your code to retry the API call, Paystack checks the reference. If that reference has already been charged, Paystack returns the original transaction instead of charging the customer again.

Build references that are unique, meaningful, and traceable:

var crypto = require('crypto');

function generateChargeReference(userId, purpose) {
  var timestamp = Date.now().toString();
  var random = crypto.randomBytes(4).toString('hex');
  return 'CHG_' + userId + '_' + purpose + '_' + timestamp + '_' + random;
}

// Examples:
// CHG_user123_monthly_1721500000000_a1b2c3d4
// CHG_user456_overage_1721500000000_e5f6g7h8
// CHG_user789_instalment3_1721500000000_i9j0k1l2

Store the reference in your database before making the API call. This way, even if your application crashes between sending the request and receiving the response, you have a record of the attempt. You can later reconcile by calling GET /transaction/verify/:reference.

async function chargeWithAuditTrail(userId, email, amount, authorizationCode) {
  var reference = generateChargeReference(userId, 'monthly');

  // Step 1: Record the attempt before calling Paystack
  await db.query(
    'INSERT INTO charge_attempts (user_id, reference, amount, status, created_at) VALUES ($1, $2, $3, $4, NOW())',
    [userId, reference, amount, 'pending']
  );

  try {
    var result = await chargeCard(email, amount, authorizationCode, reference);

    // Step 2: Update with the result
    await db.query(
      'UPDATE charge_attempts SET status = $1, gateway_response = $2, paystack_id = $3, updated_at = NOW() WHERE reference = $4',
      [result.status, result.gateway_response, result.id, reference]
    );

    return result;
  } catch (error) {
    // Step 3: Record the error
    await db.query(
      'UPDATE charge_attempts SET status = $1, error_message = $2, updated_at = NOW() WHERE reference = $3',
      ['error', error.message, reference]
    );
    throw error;
  }
}

Handling the Response

The charge_authorization response has a predictable structure. Here is what to look for:

async function processChargeResponse(result) {
  if (result.status === 'success') {
    // Charge went through
    console.log('Charged successfully');
    console.log('Amount:', result.amount);
    console.log('Transaction ID:', result.id);
    console.log('Reference:', result.reference);

    // Grant access, extend subscription, mark invoice as paid
    await grantAccess(result.customer.email, result.reference);
    return { success: true, reference: result.reference };
  }

  if (result.status === 'failed') {
    console.log('Charge failed');
    console.log('Reason:', result.gateway_response);

    // Different failure reasons need different handling
    var reason = result.gateway_response;

    if (reason === 'Insufficient Funds') {
      // Temporary. Retry later.
      await scheduleRetry(result.reference, 'insufficient_funds');
    } else if (reason === 'Card Expired') {
      // Permanent. Ask customer to update card.
      await requestCardUpdate(result.customer.email);
    } else if (reason === 'Do Not Honor') {
      // Could be temporary or permanent. Retry once, then ask customer.
      await scheduleRetry(result.reference, 'do_not_honor');
    } else {
      // Unknown reason. Log and investigate.
      console.log('Unknown decline reason:', reason);
      await flagForReview(result.reference, reason);
    }

    return { success: false, reason: reason };
  }
}

Common gateway_response values and what they mean:

Response Meaning Action
Approved Charge succeeded Grant access
Insufficient Funds Card valid but account balance too low Retry in a few hours or days
Card Expired Card is past its expiry date Ask customer to add new card
Do Not Honor Bank declined without specific reason Retry once, then contact customer
Invalid Transaction The transaction parameters are wrong Check your request data
Transaction Not Permitted Card restrictions prevent this charge Ask customer to contact their bank

For a complete guide to handling each failure type and building a dunning flow around them, see handling failed recurring charges and dunning.

Verifying After the Charge

The charge_authorization response tells you the result. But as a best practice, verify the transaction independently, especially if you are acting on the result (granting access, shipping goods, extending a subscription).

async function verifyCharge(reference) {
  var response = await axios.get(
    'https://api.paystack.co/transaction/verify/' + reference,
    {
      headers: {
        Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY,
      },
    }
  );

  var data = response.data.data;

  if (data.status === 'success') {
    // Double-check the amount matches what you intended to charge
    if (data.amount !== expectedAmount) {
      console.log('Amount mismatch. Expected: ' + expectedAmount + ', Got: ' + data.amount);
      // Flag for review
    }
    return true;
  }

  return false;
}

Why verify when you already have the response? Two reasons. First, network issues can corrupt responses. You might get a garbled response that you parse incorrectly. Second, if your application crashes between receiving the response and processing it, the verification call lets you recover the state on restart.

A recovery function that runs on application startup:

async function reconcilePendingCharges() {
  var pending = await db.query(
    'SELECT reference FROM charge_attempts WHERE status = $1',
    ['pending']
  );

  for (var i = 0; i < pending.rows.length; i++) {
    var ref = pending.rows[i].reference;
    try {
      var response = await axios.get(
        'https://api.paystack.co/transaction/verify/' + ref,
        {
          headers: {
            Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY,
          },
        }
      );

      var data = response.data.data;
      await db.query(
        'UPDATE charge_attempts SET status = $1, updated_at = NOW() WHERE reference = $2',
        [data.status, ref]
      );

      if (data.status === 'success') {
        await grantAccess(data.customer.email, ref);
      }
    } catch (error) {
      console.log('Could not verify ' + ref + ': ' + error.message);
    }
  }
}

For the full verification pattern, see verify transaction.

A Production-Ready Charge Function

Putting it all together: a charge function that handles references, audit trails, error handling, and verification in one flow.

var axios = require('axios');
var crypto = require('crypto');

var PAYSTACK_BASE = 'https://api.paystack.co';
var headers = {
  Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY,
  'Content-Type': 'application/json',
};

async function chargeCustomer(options) {
  var userId = options.userId;
  var email = options.email;
  var amount = options.amount;
  var authorizationCode = options.authorizationCode;
  var currency = options.currency || 'NGN';
  var purpose = options.purpose || 'charge';
  var metadata = options.metadata || {};

  // 1. Validate inputs
  if (!authorizationCode || !email || !amount) {
    throw new Error('Missing required charge parameters');
  }

  if (amount < 100) {
    throw new Error('Amount must be at least 100 (1 NGN in kobo)');
  }

  // 2. Generate reference and record attempt
  var reference = 'CHG_' + userId + '_' + purpose + '_' + Date.now() + '_' + crypto.randomBytes(4).toString('hex');

  await db.query(
    'INSERT INTO charge_attempts (user_id, reference, amount, currency, purpose, status, created_at) VALUES ($1, $2, $3, $4, $5, $6, NOW())',
    [userId, reference, amount, currency, purpose, 'pending']
  );

  // 3. Make the charge
  try {
    var response = await axios.post(
      PAYSTACK_BASE + '/transaction/charge_authorization',
      {
        email: email,
        amount: amount,
        authorization_code: authorizationCode,
        reference: reference,
        currency: currency,
        metadata: metadata,
      },
      { headers: headers }
    );

    var result = response.data.data;

    // 4. Record the outcome
    await db.query(
      'UPDATE charge_attempts SET status = $1, gateway_response = $2, paystack_id = $3, updated_at = NOW() WHERE reference = $4',
      [result.status, result.gateway_response, result.id, reference]
    );

    return {
      success: result.status === 'success',
      reference: reference,
      status: result.status,
      gatewayResponse: result.gateway_response,
      amount: result.amount,
      paystackId: result.id,
    };

  } catch (error) {
    var errorMessage = error.response
      ? error.response.data.message
      : error.message;

    await db.query(
      'UPDATE charge_attempts SET status = $1, error_message = $2, updated_at = NOW() WHERE reference = $3',
      ['error', errorMessage, reference]
    );

    return {
      success: false,
      reference: reference,
      status: 'error',
      gatewayResponse: errorMessage,
      amount: amount,
    };
  }
}

This function never throws. It returns a result object that your calling code can inspect. Failed charges are recorded, not lost. Network errors are caught and logged. The reference is generated before the API call, so even crashes leave an auditable record.

Common Mistakes and How to Avoid Them

These mistakes show up repeatedly in production Paystack integrations.

1. Using a non-reusable authorization

If the customer's first payment was via bank transfer or USSD, the authorization has reusable: false. Calling charge_authorization with it will fail. Always check the reusable field before storing an authorization for recurring use. See reusable vs non-reusable authorizations.

2. Not generating your own reference

If you let Paystack generate the reference, you lose idempotency protection. A network timeout followed by a retry will result in two separate charges. Always generate and store your reference before calling the API.

3. Retrying too aggressively

When a charge fails, some developers retry immediately in a loop. This is dangerous. If the failure is "Insufficient Funds," retrying 10 times in 10 seconds will not help. Space retries over hours or days. See building a dunning email sequence for timing guidance.

4. Ignoring the currency mismatch

An authorization created in NGN cannot be charged in KES. The currency must match. If you serve multiple countries, track which currency each customer's authorization was created in.

5. Not handling the email mismatch

The email you pass to charge_authorization must match the email Paystack has for that customer. If the customer changed their email in your system but not on Paystack, the charge can fail. Keep customer emails synchronized.

6. Charging without user consent

Just because you have an authorization code does not mean you can charge any amount at any time. Make sure your terms of service clearly state what amounts and frequencies the customer is agreeing to. Unexpected charges lead to chargebacks and disputes.

Using Metadata for Charge Tracking

The charge_authorization endpoint accepts a metadata field. Use it to attach business context to each charge so you can trace it later in the Paystack Dashboard, in webhook events, and in your own reports.

var metadata = {
  custom_fields: [
    {
      display_name: 'Invoice Number',
      variable_name: 'invoice_number',
      value: 'INV-2026-0042',
    },
    {
      display_name: 'Billing Period',
      variable_name: 'billing_period',
      value: 'July 2026',
    },
    {
      display_name: 'Plan',
      variable_name: 'plan',
      value: 'Pro Monthly',
    },
  ],
};

// Pass metadata in the charge call
var result = await chargeCustomer({
  userId: 'user123',
  email: 'customer@example.com',
  amount: 500000,
  authorizationCode: 'AUTH_xxxxxxxxxxxxx',
  metadata: metadata,
});

The custom_fields array shows up in the Paystack Dashboard under the transaction details. This saves your support team from cross-referencing your internal database every time they need to look up a charge.

Metadata also appears in webhook payloads. When you process a charge.success event, the metadata is right there in the event data, letting your webhook handler route the charge to the correct business logic without additional API calls.

Key Takeaways

  • ✓The charge_authorization endpoint lets you charge a customer's saved card without them re-entering card details. You need their email, the amount, and their authorization_code.
  • ✓Always generate a unique reference for each charge. This prevents double charges if your request is retried due to network issues.
  • ✓The charge response is synchronous for card payments. You get success or failed immediately, unlike the Charge API which can return pending.
  • ✓Check the gateway_response field on failures to understand why a charge was declined. Different decline reasons need different handling.
  • ✓Store every charge attempt in your database before calling the API. This gives you an audit trail and makes reconciliation possible.
  • ✓Do not retry charges indefinitely. Three to five attempts over a week is reasonable. Excessive retries can get your Paystack account flagged.
  • ✓The authorization_code must have reusable: true. Non-reusable authorizations from bank transfers or USSD will fail when you try to charge them.

Frequently Asked Questions

Can I charge a different amount each time with charge_authorization?
Yes. Unlike the Subscriptions API where the plan fixes the amount, charge_authorization lets you specify any amount on each call. This is what makes it suitable for usage-based billing, instalment plans, and other variable-amount scenarios.
Does the customer get a notification when I charge their saved card?
Paystack sends a payment receipt email to the customer by default when a charge succeeds. You can include metadata to make the receipt more informative. If you want to control all customer communication yourself, coordinate with Paystack support about notification settings.
What happens if I charge the same reference twice?
Paystack treats the reference as an idempotency key. If you send a second charge request with the same reference, Paystack returns the result of the original charge instead of creating a new one. This prevents accidental double charges.
Can I charge an authorization code from a different Paystack account?
No. Authorization codes are scoped to the merchant account (integration) that created them. You cannot use an authorization code generated on another merchant's Paystack account.
Is there a minimum or maximum amount for charge_authorization?
Paystack enforces minimum transaction amounts that vary by currency. For NGN, the minimum is typically 100 kobo (1 Naira). Maximum amounts depend on your Paystack account limits and the card's own transaction limits. Check the Paystack documentation for current limits in your currency.

Learn Paystack Integration

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

Not ready? Create Free Account

Keep reading

Paystack Subscriptions and Recurring Billing: Complete Guide

Everything you need to build recurring billing on Paystack. Covers the subscriptions API, plans, authorization codes for manual charges, dunning, lifecycle webhooks, cancellation, and real-world patterns for SaaS, memberships, school fees, and loan collection across Africa.

Read more

Authorization Codes: How Paystack Recurring Billing Really Works

Authorization codes are the engine behind every recurring charge on Paystack. This article explains what happens during card tokenization, how Paystack stores and returns authorization codes, what makes them reusable, and how both the Subscriptions API and manual charge_authorization calls depend on them.

Read more

Storing Paystack Authorization Codes Securely

Authorization codes are payment credentials. They can charge real money from a customer's card. This guide covers how to store them safely in your database, what metadata to keep alongside them, encryption at rest, access control patterns, and how authorization codes fit into PCI compliance requirements.

Read more

Handling Failed Recurring Charges and Dunning on Paystack

Recurring charges fail. Cards expire, accounts run dry, banks decline for opaque reasons. This guide covers every failure scenario in Paystack recurring billing, shows you how to read gateway responses, build a retry strategy, implement a dunning state machine, and handle the higher failure rates typical in African card markets.

Read more

Building Your Own Billing Engine on Paystack Authorizations

When the Subscriptions API is too rigid for your billing model, you build your own engine on top of Paystack authorization codes. This guide walks through the database schema, charge scheduler, billing state machine, retry queue, invoice generation, and reconciliation process you need to run reliable recurring billing.

Read more

Explore related topics

M-Pesa & African Stack

M-Pesa Daraja API tutorials, USSD development, and WhatsApp Business API guides. The African Stack that global bootcamps do not teach.

Learn to Code in Kenya

Everything a Kenyan needs to know before writing their first line of code. Career paths, real costs, hardware requirements, and honest answers about what it takes.

Learning to Code

Roadmaps, language guides, and practical advice for learning to code and launching a programming career.

Breaking Into Tech

Practical, no-nonsense guides for anyone looking to start or change their career in tech. From choosing your first language to acing interviews.

The African Stack: Skills for This Market

The skills that make Kenyan developers hard to replace: M-Pesa, USSD, WhatsApp, fintech, and building for the African market.

Bonaventure Ogeto

Bonaventure Ogeto

Founder, McTaba Labs

Bonaventure is the founder of McTaba Labs, a coding bootcamp in Nairobi, Kenya. He has spent years building products for the African market and mentoring the next generation of developers across the continent.