Direct debit (Open banking)
Get your customer’s permission once, then charge their bank account whenever a payment is due, up to a limit you set. No cards needed. Built for subscriptions, loan repayments and other recurring billing.
| Environment | Collect API | Hosted set-up page |
|---|---|---|
| Sandbox | https://beta-collect.paga.com | https://beta-checkout.paga.com |
| Live | https://collect.paga.com | https://checkout.paga.com |
Authenticate with HTTP Basic and a hash header, as in Authentication, from a whitelisted IP address. Every step is asynchronous, so rely on callbacks rather than polling.
How it works
Set up a mandate
Use the hosted set-up page or the API. Give the mandate your own accountReference; it becomes the token you charge later.
Your customer authorises it
They send a small one-time transfer from the bank account they want debited.
Paga confirms, then the bank approves
You get a VERIFIED callback once the transfer arrives, then an APPROVED callback when the bank approves the mandate. Bank approval usually takes under 5 minutes, but can occasionally take longer.
Charge whenever a payment is due
Charge the accountReference for any amount up to the limit, and get a callback for each charge.
The mandate moves from PENDING to VERIFIED to APPROVED, or to REJECTED if the bank declines it or you disable it.
1. Set up the mandate
Choose how your customer authorises the mandate. Everything after this step is the same for both.
Best for: getting started quickly. Paga hosts the screens that verify your customer, collect their bank account and show them how to authorise. You create a session and redirect your customer to its checkoutUrl.
POST https://checkout.paga.com/checkout/charge-process/start
Authorization: Basic base64(publicKey:secretKey)
Content-Type: application/jsonAuthenticate with HTTP Basic. This endpoint doesn’t use a hash header.
{
"referenceNumber": "DD-2026-0042",
"accountReference": "CUST-000184-MANDATE",
"amountLimit": 40000.0,
"paymentMethods": ["BANK_ACCOUNT_DIRECT_DEBIT"],
"callbackUrl": "https://yourstore.com/webhooks/paga",
"redirectUrl": "https://yourstore.com/mandate/complete",
"sessionExpirySeconds": "50000",
"paymentExpirySeconds": "157680000",
"additionalProperties": {
"payerPhoneNumber": "08064343187",
"payerEmail": "[email protected]",
"authorizationAmount": 40000.0
}
}Request parameters (11)
| Parameter | Required | Description |
|---|---|---|
referenceNumber | Yes | Your unique reference for this set-up |
accountReference | Yes | Your ID for the mandate. You charge the customer with it once approved |
amountLimit | Yes | The most you can charge in a single debit |
paymentMethods | Yes | ["BANK_ACCOUNT_DIRECT_DEBIT"] |
callbackUrl | Yes | Where Paga sends mandate and charge callbacks |
redirectUrl | No | Where your customer returns after the set-up screens |
sessionExpirySeconds | No | Lifetime of the set-up session, in seconds from when you create it. After this, the checkoutUrl stops working |
paymentExpirySeconds | No | Lifetime of the mandate, in seconds. For example, 157680000 is 5 years |
additionalProperties.payerPhoneNumber | No | Your customer’s phone number. Filled in on the set-up screens for your customer to confirm |
additionalProperties.payerEmail | No | Your customer’s email address. Filled in on the set-up screens for your customer to confirm |
additionalProperties.authorizationAmount | No | Take a first payment during set-up. Your customer’s authorisation transfer is this amount instead of ₦50 to ₦100. Hosted set-up page only |
{
"statusCode": "0",
"statusMessage": "Session created successfully",
"data": {
"referenceNumber": "DD-2026-0042",
"sessionKey": "60dd76d8-6fc2-4a6c-832f-990d2a8f0c29",
"status": "INITIALIZED",
"checkoutUrl": "https://checkout.paga.com/session/60dd76d8-6fc2-4a6c-832f-990d2a8f0c29",
"redirectUrl": "https://yourstore.com/mandate/complete",
"callbackUrl": "https://yourstore.com/webhooks/paga",
"enabledPaymentMethods": ["BANK_ACCOUNT_DIRECT_DEBIT"],
"initiatedAt": "2026-11-02T19:11:47.659Z",
"sessionExpiresAt": "2026-11-02T21:11:47.659Z",
"paymentExpiresAt": "2031-11-01T19:11:47Z",
"metadata": {
"amountLimit": 40000.0,
"authorizationAmount": 40000.0,
"currency": "NGN",
"accountReference": "CUST-000184-MANDATE",
"allowPartialPayment": true,
"allowMultipleUse": true
}
}
}Response fields you’ll use most (8)
| Field | What it’s for |
|---|---|
data.checkoutUrl | Redirect your customer here to set up the mandate |
data.sessionKey | Paga’s ID for the set-up session |
data.status | INITIALIZED until your customer starts |
data.sessionExpiresAt | When the set-up link stops working, in UTC, set by sessionExpirySeconds |
data.paymentExpiresAt | When the mandate expires, in UTC, set by paymentExpirySeconds |
data.metadata.amountLimit | The most you can charge in a single debit |
data.metadata.authorizationAmount | The first payment taken during set-up, if you set one. null otherwise |
data.metadata.accountReference | The mandate you’ll charge once it’s approved |
Take the first payment during set-up. For subscriptions, set additionalProperties.authorizationAmount to the first payment, such as ₦40,000. Your customer’s authorisation transfer then pays it, instead of a separate ₦50 to ₦100. The VERIFIED callback confirms it in receivedAuthorizationAmount; there’s no separate charge callback. Charge the mandate from the second payment onward. Available on the hosted set-up page only.
What your customer sees
Start
Your customer sees how Direct Debit works and selects Get started.
1. Confirm it’s you
They confirm their phone, bank and account, then enter a one-time code.
2. Transfer to authorize
They send the amount shown from the account being set up.
Best for: keeping customers in your own app. You collect your customer’s details and bank account, create a payment request, and show them the authorisation transfer instructions yourself.
POST /paymentRequest
Authorization: Basic base64(publicKey:secretKey)
hash: sha512(referenceNumber + amount + currency + payer.phoneNumber + payer.email + hashKey)
Content-Type: application/json{
"referenceNumber": "DD-2026-0043",
"amount": 5000.0,
"currency": "NGN",
"accountReference": "CUST-000185-MANDATE",
"payer": {
"name": "Bello Ramon",
"phoneNumber": "08064343187",
"email": "[email protected]",
"address": "176 Herbert Macaulay Way, Yaba",
"bankId": "824d4b53-2752-49bb-b84a-20b69bb897ef",
"bankAccountNumber": "0697122703"
},
"payee": { "name": "Your business name" },
"isSingleUse": false,
"expiryDateTimeUTC": "2030-11-25T00:00:00",
"payerCollectionFeeShare": 0.0,
"payeeCollectionFeeShare": 1.0,
"isAllowPartialPayments": true,
"callBackUrl": "https://yourstore.com/webhooks/paga",
"paymentMethods": ["DIRECT_DEBIT"]
}Request parameters (18)
| Parameter | Required | Description |
|---|---|---|
referenceNumber | Yes | Your unique reference for this set-up. Hash value 1 |
amount | Yes | The most you can charge in a single debit. Hash value 2 |
currency | Yes | NGN. Hash value 3 |
accountReference | Yes | Your ID for the mandate. You charge the customer with it once approved |
payer.name | Yes | Customer’s name |
payer.phoneNumber | Yes | Customer’s phone number. Hash value 4 |
payer.email | Yes | Customer’s email address. Hash value 5 |
payer.address | Yes | Address registered with their bank |
payer.bankId | Yes | Customer’s bank, from Get Banks (POST /banks) |
payer.bankAccountNumber | Yes | The 10-digit account to debit |
payee.name | Yes | Your business name, shown to the customer |
isSingleUse | No | Set to false if you’ll charge for more than 30 days. Default true |
expiryDateTimeUTC | Yes | When the mandate expires, in UTC. Up to 5 years ahead |
isAllowPartialPayments | Yes | true lets you charge less than the limit. Default false |
payerCollectionFeeShare | Yes | Share of the fee your customer pays, from 0.0 to 1.0 |
payeeCollectionFeeShare | Yes | Share of the fee you pay. Both shares must add up to 1.0 |
callBackUrl | Yes | Where Paga sends mandate and charge callbacks |
paymentMethods | Yes | ["DIRECT_DEBIT"] |
{
"referenceNumber": "DD-2026-0043",
"statusCode": "0",
"statusMessage": "success",
"requestAmount": 5000.0,
"totalPaymentAmount": 5000.0,
"paymentMethods": [
{
"name": "DIRECT_DEBIT",
"properties": {
"activationAmount": "N50.00",
"accountReference": "CUST-000185-MANDATE",
"activationAccountNumber": "9880218357",
"activationBankName": "Paystack-Titan"
}
}
],
"expiryDateTimeUTC": "2030-11-25T00:00:00",
"isPayerPagaAccountHolder": true,
"bankName": "Paga"
}Response fields you’ll use most (4)
| Field | What it’s for |
|---|---|
paymentMethods[].properties.activationAmount | The amount your customer transfers to authorise, between ₦50 and ₦100 |
paymentMethods[].properties.activationAccountNumber | The account they transfer it to |
paymentMethods[].properties.activationBankName | The bank to show with that account |
paymentMethods[].properties.accountReference | The mandate you’ll charge once it’s approved |
Show your customer the activation amount, account number and bank, and tell them to send it from the account in payer.bankAccountNumber.
Authorise from an approved channelThe authorisation transfer must come from the customer’s mobile banking app, internet banking, or a bank branch. Transfers by POS, USSD or ATM don’t count, and the mandate stays pending with no callback.
Show these channels in your app, and remind customers to send from the account they’re setting up. Some banks don’t allow transfers under ₦100, so suggest ₦100 if a smaller transfer fails.
2. Track the mandate
Paga sends two callbacks to your callback URL: VERIFIED when the authorisation transfer arrives, and APPROVED when the bank approves the mandate. Both set-up methods send the same callbacks. Only charge once it’s approved.
Your customer’s transfer arrived. The mandate now waits for bank approval, which usually takes under 5 minutes but can occasionally take longer.
{
"event": "Tokenization",
"notificationId": "9f1c2e44-0b7a-4d6e-9c1f-2b8e7a6d5c40",
"statusCode": "004",
"statusMessage": "VERIFIED",
"mandateStatus": "VERIFIED",
"referenceNumber": "DD-2026-0042",
"accountReference": "CUST-000184-MANDATE",
"customerEmail": "[email protected]",
"customerPhoneNumber": "+2348064343187",
"bankName": "Opay",
"accountNumber": "8872493187",
"currency": "NGN",
"receivedAuthorizationAmount": 40000.0,
"timeStamp": "2026-11-04T07:31:00.885302596Z",
"hash": "f2a126a87622ab66ac44570e738c62d7b58a53472bcc7aed0fb9fec193d31a84bbc3cb606ee45ab911033e5f0c122801f3b5ff27df6dabccb47331ead4eecafc"
}The bank approved the mandate. You can now charge it.
{
"event": "Tokenization",
"notificationId": "9f1c2e44-0b7a-4d6e-9c1f-2b8e7a6d5c40",
"statusCode": "0",
"statusMessage": "APPROVED",
"mandateStatus": "APPROVED",
"referenceNumber": "DD-2026-0042",
"accountReference": "CUST-000184-MANDATE",
"customerEmail": "[email protected]",
"customerPhoneNumber": "+2348064343187",
"bankName": "Opay",
"accountNumber": "8872493187",
"currency": "NGN",
"receivedAuthorizationAmount": 40000.0,
"timeStamp": "2026-11-04T07:31:00.885302596Z",
"hash": "f2a126a87622ab66ac44570e738c62d7b58a53472bcc7aed0fb9fec193d31a84bbc3cb606ee45ab911033e5f0c122801f3b5ff27df6dabccb47331ead4eecafc"
}Mandate callback fields (15)
| Field | Meaning |
|---|---|
event | Tokenization |
notificationId | Unique ID for this notification. De-duplicate on it |
statusCode | 004 when verified, 0 when approved. See the status codes below |
statusMessage | VERIFIED or APPROVED |
mandateStatus | The mandate’s status: VERIFIED or APPROVED |
referenceNumber | Your set-up reference |
accountReference | The mandate. Charge it with this once approved |
customerEmail | Customer’s email address |
customerPhoneNumber | Customer’s phone number |
bankName | The customer’s bank |
accountNumber | The customer’s account that will be debited |
currency | NGN |
receivedAuthorizationAmount | The first payment received during set-up, if you set authorizationAmount. Otherwise null. This is the only confirmation of that payment |
timeStamp | When Paga sent the notification, in UTC |
hash | Signature to check before you trust the callback. Built the same way for verified and approved |
Check the callback hash
Join statusCode + timeStamp + accountNumber + bankName + referenceNumber + mandateStatus + hashKey, hash the result with SHA-512, and compare it with hash.
The same applies to VERIFIED and APPROVED callbacks. Use each value exactly as received, including the full timeStamp. For the VERIFIED example, the string to hash is 0042026-11-04T07:31:00.885302596Z8872493187OpayDD-2026-0042VERIFIED followed by your hash key. Respond with HTTP 200.
Mandate status codes (6)
| statusCode | Meaning |
|---|---|
003 | Pending. Created, but not yet authorised |
004 | Verified. The customer authorised it; waiting for the bank |
0 | Approved. Ready to charge |
005 | Rejected. The authorisation was declined |
006 | Unknown. The processor can’t confirm the status |
007 | Not found. No mandate matches the details |
Check mandate status
POST /status
Authorization: Basic base64(publicKey:secretKey)
hash: sha512(referenceNumber + hashKey)
Content-Type: application/json{
"referenceNumber": "DD-2026-0042"
}{
"referenceNumber": "DD-2026-0042",
"statusCode": "0",
"statusMessage": "success",
"data": {
"referenceNumber": "DD-2026-0042",
"statusCode": "1",
"statusMessage": "pending",
"requestAmount": 50000,
"totalPaymentAmount": 50050,
"paymentMethods": [
{
"name": "DIRECT_DEBIT",
"properties": {
"activationAmount": "50",
"accountReference": "CUST-000184-MANDATE",
"activationAccountNumber": "0569304340",
"activationBankName": "Paga"
}
}
],
"expiryDateTimeUTC": "2031-11-01T19:11:47",
"isPayerPagaAccountHolder": false,
"additionalData": {
"mandate": [
{
"ProcessStatusId": "APPROVED",
"AccountReference": "CUST-000184-MANDATE",
"StatusMessage": null,
"CreatedDateTimeUTC": 1793790649533
}
]
}
}
}
Use ProcessStatusId, not data.statusCode
data.statusCodeanddata.statusMessagestay1andpendingwhatever happens to the mandate, including after it’s approved or disabled, so they can’t tell you whether to charge. Readdata.additionalData.mandate[].ProcessStatusIdinstead, and charge only when it’sAPPROVED.
| Field | Meaning |
|---|---|
statusCode | 0 means your status request worked |
data.referenceNumber | Your set-up reference |
data.statusCode | Always 1, even after the mandate is approved or disabled. Don’t use it to decide whether to charge |
data.statusMessage | Always pending, for the same reason. Ignore it too |
data.requestAmount | The most you can charge in a single debit |
data.totalPaymentAmount | The limit including fees |
data.paymentMethods[].properties | The authorisation transfer details: amount, account number and bank |
data.expiryDateTimeUTC | When the mandate expires, in UTC |
data.additionalData.mandate[].ProcessStatusId | The field to check. The mandate’s status: PENDING, VERIFIED, APPROVED or REJECTED. REJECTED means the bank declined it or you disabled it |
data.additionalData.mandate[].AccountReference | The mandate you charge |
data.additionalData.mandate[].CreatedDateTimeUTC | When the mandate was created, in milliseconds since 1 January 1970 (UTC) |
3. Charge the mandate
Once the mandate is approved, charge it for any amount up to the limit. Give each charge its own referenceNumber.
POST /chargeDebitMandate
Authorization: Basic base64(publicKey:secretKey)
hash: sha512(referenceNumber + amount + accountReference + hashKey)
Content-Type: application/json{
"referenceNumber": "CHG-2026-0101",
"amount": 600.0,
"accountReference": "CUST-000184-MANDATE"
}{
"referenceNumber": "CHG-2026-0101",
"statusCode": "0",
"statusMessage": "Successful"
}Request parameters (3)
| Parameter | Required | Description |
|---|---|---|
referenceNumber | Yes | Your unique reference for this charge. Hash value 1 |
amount | Yes | Amount to charge, up to the mandate’s limit. Hash value 2 |
accountReference | Yes | The approved mandate to charge. Hash value 3 |
A successful response means the charge was accepted. The debit itself completes later. Fulfil only when the charge callback confirms it.
{
"event": "Charge_Complete",
"notificationId": "CHG-2026-0101",
"statusCode": "0",
"statusMessage": "SUCCESSFUL",
"processStatusId": "SUCCESSFUL",
"referenceNumber": "CHG-2026-0101",
"accountReference": "CUST-000184-MANDATE",
"amount": "600.00",
"currency": "NGN",
"senderName": "BELLO RAMON",
"sourceBankName": "Opay",
"sourceBankAccountNumber": "8872493187",
"hash": "c95f24663e0bbce55cf9970dbfb57d6eb5a31725ed4420d79aed46f645cb3ed24336e1061fdd18531dc8087556677007a97eab1328664c993254c18170cff53f"
}Charge callback fields (13)
| Field | Meaning |
|---|---|
event | Charge_Complete |
notificationId | Unique ID for this notification. De-duplicate on it |
statusCode | 0 means the charge succeeded. See the charge status codes below |
statusMessage | Readable status, such as SUCCESSFUL |
processStatusId | The charge’s status, such as SUCCESSFUL |
referenceNumber | Your charge reference |
accountReference | The mandate that was charged |
amount | Amount charged, as a string with two decimal places and no commas, such as 50000.00 |
currency | NGN |
senderName | Name on the debited account |
sourceBankName | The customer’s bank |
sourceBankAccountNumber | The customer’s debited account |
hash | Signature to check before you trust the callback |
Check the callback hash
Join statusCode + referenceNumber + accountReference + amount + hashKey, hash the result with SHA-512, and compare it with hash.
Use amount exactly as it appears in the callback: two decimal places and no commas, even for large amounts, such as 600.00 or 50000.00. For the example above, the string to hash is 0CHG-2026-0101CUST-000184-MANDATE600.00 followed by your hash key. Respond with HTTP 200, then mark the payment as collected.
Check charge status
POST /getChargeMandateStatus
Authorization: Basic base64(publicKey:secretKey)
hash: sha512(referenceNumber + hashKey)
Content-Type: application/json{
"referenceNumber": "CHG-2026-0101"
}{
"statusCode": "0",
"statusMessage": "SUCCESS",
"data": {
"statusCode": "0",
"ReferenceNumber": "CHG-2026-0101",
"Amount": 600,
"ProcessStatusId": "SUCCESSFUL"
}
}Charged successfully.
Pending. Wait for the callback.
Invalid: wrong details, expired mandate or limit exceeded.
Unknown. Check again before retrying.
4. Disable a mandate
Stop all future charges, for example when a customer cancels their subscription. Once disabled, the mandate’s ProcessStatusId in Check mandate status becomes REJECTED, and it can’t be charged again.
POST /disableMandate
Authorization: Basic base64(publicKey:secretKey)
hash: sha512(referenceNumber + accountReference + hashKey)
Content-Type: application/json{
"referenceNumber": "DIS-2026-0007",
"accountReference": "CUST-000184-MANDATE"
}{
"statusCode": "0",
"statusMessage": "success",
"referenceNumber": "DIS-2026-0007",
"data": null
}Good to know
Your accountReference is the token
Map it carefully to your customer. Every charge and the disable request use it.
Set the limit to your biggest charge
The set-up amount is the most you can take in one debit. Allow partial payments to charge less.
Long-running mandates
In the API, set isSingleUse to false for mandates you’ll charge beyond 30 days. They can last up to 5 years.
Wait for the APPROVED callback
Bank approval usually takes under 5 minutes, but can occasionally take longer. Don’t schedule the first charge until the APPROVED callback arrives.
Need help? We’re here 24/7
Talk to our support team any time, or ask questions in the developer community.
Updated 4 days ago

