paystack-charges
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
paystackRequesthelper 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
birthdayas part of the initial request - For
pendingstatus, wait at least 10 seconds before polling to avoid rate limiting - Mobile money providers vary by country:
mtn,atl,vodfor Ghana;mpesafor Kenya;orange,wavefor 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