skills/rexedge/paystack/paystack-charges

paystack-charges

SKILL.md

Paystack Charges

The Charge API gives you direct control over which payment channel to use, bypassing the standard Paystack checkout. It supports card, bank, USSD, mobile money, QR, EFT, Capitec Pay, and bank transfers.

Depends on: paystack-setup for the paystackRequest helper and environment config.

Charge Flow

The Charge API follows a multi-step flow. After creating a charge, Paystack may request additional authentication:

Create Charge → Check status → If "send_pin"    → Submit PIN    → Check status
                              → If "send_otp"    → Submit OTP    → Check status
                              → If "send_phone"  → Submit Phone  → Check status
                              → If "send_birthday"→ Submit Birthday→ Check status
                              → If "send_address"→ Submit Address → Check status
                              → If "pending"     → Wait 10s      → Check Pending
                              → If "success"     → Done ✓

Endpoints

Method Endpoint Description
POST /charge Create a charge
POST /charge/submit_pin Submit PIN for verification
POST /charge/submit_otp Submit OTP for verification
POST /charge/submit_phone Submit phone number
POST /charge/submit_birthday Submit birthday
POST /charge/submit_address Submit address (AVS)
GET /charge/:reference Check pending charge status

Create Charge

POST /charge

Core Parameters (Body)

Param Type Required Description
email string Yes Customer's email address
amount string Yes Amount in subunits
reference string No Unique transaction reference
metadata object No Custom data for post-payment processing

Channel-Specific Parameters

Send ONE of these objects to select the payment channel:

Bank Account (Nigeria)

const result = await paystackRequest("/charge", {
  method: "POST",
  body: JSON.stringify({
    email: "customer@email.com",
    amount: "10000",
    bank: {
      code: "057",           // Bank code (get from List Banks endpoint)
      account_number: "0000000000",
    },
    birthday: "1995-12-23",  // Required for some banks
  }),
});

Bank Account (Kuda Bank)

{
  email: "customer@email.com",
  amount: "10000",
  bank: {
    code: "50211",
    account_number: "0000000000",
    phone: "08012345678",    // Required for Kuda
    token: "123456",         // 6-digit code from Kuda app
  }
}

USSD (Nigeria only)

{
  email: "customer@email.com",
  amount: "10000",
  ussd: { type: "737" }     // 737 (GTBank) — currently only supported type
}

Mobile Money (Ghana, Kenya, Côte d'Ivoire)

// MTN Ghana
{
  email: "customer@email.com",
  amount: "10000",
  mobile_money: {
    phone: "0551234987",
    provider: "mtn"          // mtn | atl | vod (Ghana), mpesa (Kenya), orange | wave (CIV)
  }
}

// M-Pesa Kenya (till number)
{
  email: "customer@email.com",
  amount: "10000",
  mobile_money: {
    account: "TILL_NUMBER",  // Use account instead of phone for M-Pesa till
    provider: "mptill"       // or "mpesa" for phone-based, "mpesa_offline"
  }
}

QR Code (South Africa)

{
  email: "customer@email.com",
  amount: "10000",
  qr: { provider: "scan-to-pay" }
}

EFT (South Africa)

{
  email: "customer@email.com",
  amount: "10000",
  eft: { provider: "ozow" }
}

Capitec Pay (South Africa)

{
  email: "customer@email.com",
  amount: "10000",
  capitec_pay: {
    identifier_key: "CELLPHONE",    // CELLPHONE | IDNUMBER | ACCOUNTNUMBER
    identifier_value: "0712345678"
  }
}

Bank Transfer (Pay with Transfer)

{
  email: "customer@email.com",
  amount: "10000",
  bank_transfer: {
    account_expires_at: "2024-12-31T23:59:59.000Z"  // Expiry for generated account
  }
}

Card (with authorization code)

{
  email: "customer@email.com",
  amount: "10000",
  authorization_code: "AUTH_72btv547",  // From a previous successful charge
  pin: "1234"                           // Only for non-reusable auth codes
}

Split Payment Parameters

Param Type Description
split_code string Split code e.g. SPL_98WF13Eb3w
subaccount string Subaccount code e.g. ACCT_8f4s1eq7ml6rlzj
transaction_charge integer Override split amount (subunits)
bearer string "account" or "subaccount" — who pays fees

Submit PIN

POST /charge/submit_pin

When data.status === "send_pin" in the charge response:

const result = await paystackRequest("/charge/submit_pin", {
  method: "POST",
  body: JSON.stringify({
    pin: "1234",
    reference: "5bwib5v6anhe9xa",
  }),
});

Submit OTP

POST /charge/submit_otp

When data.status === "send_otp":

const result = await paystackRequest("/charge/submit_otp", {
  method: "POST",
  body: JSON.stringify({
    otp: "123456",
    reference: "5bwib5v6anhe9xa",
  }),
});

Submit Phone

POST /charge/submit_phone

When data.status === "send_phone":

const result = await paystackRequest("/charge/submit_phone", {
  method: "POST",
  body: JSON.stringify({
    phone: "08012345678",
    reference: "5bwib5v6anhe9xa",
  }),
});

Submit Birthday

POST /charge/submit_birthday

When data.status === "send_birthday":

const result = await paystackRequest("/charge/submit_birthday", {
  method: "POST",
  body: JSON.stringify({
    birthday: "1995-12-23",
    reference: "5bwib5v6anhe9xa",
  }),
});

Submit Address

POST /charge/submit_address

When data.status === "send_address" (AVS — Address Verification System):

const result = await paystackRequest("/charge/submit_address", {
  method: "POST",
  body: JSON.stringify({
    reference: "7c7rpkqpc0tijs8",
    address: "140 N 2ND ST",
    city: "Stroudsburg",
    state: "PA",
    zipcode: "18360",
  }),
});

Check Pending Charge

GET /charge/:reference

When status is "pending", wait at least 10 seconds before checking:

const result = await paystackRequest<{
  status: string;
  reference: string;
  amount: number;
}>(`/charge/${encodeURIComponent(reference)}`);

// result.data.status: "success" | "failed" | "pending"

Full Charge Flow Example

async function processDirectCharge(chargeData: any) {
  let result = await paystackRequest<any>("/charge", {
    method: "POST",
    body: JSON.stringify(chargeData),
  });

  // Handle multi-step authentication
  while (result.data.status !== "success" && result.data.status !== "failed") {
    switch (result.data.status) {
      case "send_pin":
        const pin = await promptUserForPIN();
        result = await paystackRequest("/charge/submit_pin", {
          method: "POST",
          body: JSON.stringify({ pin, reference: result.data.reference }),
        });
        break;
      case "send_otp":
        const otp = await promptUserForOTP();
        result = await paystackRequest("/charge/submit_otp", {
          method: "POST",
          body: JSON.stringify({ otp, reference: result.data.reference }),
        });
        break;
      case "send_phone":
        const phone = await promptUserForPhone();
        result = await paystackRequest("/charge/submit_phone", {
          method: "POST",
          body: JSON.stringify({ phone, reference: result.data.reference }),
        });
        break;
      case "send_birthday":
        const birthday = await promptUserForBirthday();
        result = await paystackRequest("/charge/submit_birthday", {
          method: "POST",
          body: JSON.stringify({ birthday, reference: result.data.reference }),
        });
        break;
      case "send_address":
        const address = await promptUserForAddress();
        result = await paystackRequest("/charge/submit_address", {
          method: "POST",
          body: JSON.stringify({ ...address, reference: result.data.reference }),
        });
        break;
      case "pending":
        await new Promise((r) => setTimeout(r, 10000)); // Wait 10 seconds
        result = await paystackRequest(`/charge/${result.data.reference}`);
        break;
      default:
        throw new Error(`Unknown charge status: ${result.data.status}`);
    }
  }

  return result.data;
}

Important Notes

  • The Charge API is more complex than the standard checkout — only use it when you need direct control over payment channels
  • Some bank charges require birthday as part of the initial request
  • For pending status, wait at least 10 seconds before polling to avoid rate limiting
  • Mobile money providers vary by country: mtn, atl, vod for Ghana; mpesa for Kenya; orange, wave for Côte d'Ivoire
  • USSD currently only supports 737 (GTBank) in Nigeria
  • PIN and OTP submissions must happen server-side — never collect or transmit these on the client
Weekly Installs
9
GitHub Stars
1
First Seen
8 days ago
Installed on
github-copilot9
cursor9
opencode9
cline8
codex8
kimi-cli8