Direct debit (Open banking)

Recurring payments

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.

EnvironmentCollect APIHosted set-up page
Sandboxhttps://beta-collect.paga.comhttps://beta-checkout.paga.com
Livehttps://collect.paga.comhttps://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/json

Authenticate 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)
ParameterRequiredDescription
referenceNumberYesYour unique reference for this set-up
accountReferenceYesYour ID for the mandate. You charge the customer with it once approved
amountLimitYesThe most you can charge in a single debit
paymentMethodsYes["BANK_ACCOUNT_DIRECT_DEBIT"]
callbackUrlYesWhere Paga sends mandate and charge callbacks
redirectUrlNoWhere your customer returns after the set-up screens
sessionExpirySecondsNoLifetime of the set-up session, in seconds from when you create it. After this, the checkoutUrl stops working
paymentExpirySecondsNoLifetime of the mandate, in seconds. For example, 157680000 is 5 years
additionalProperties.payerPhoneNumberNoYour customer’s phone number. Filled in on the set-up screens for your customer to confirm
additionalProperties.payerEmailNoYour customer’s email address. Filled in on the set-up screens for your customer to confirm
additionalProperties.authorizationAmountNoTake 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)
FieldWhat it’s for
data.checkoutUrlRedirect your customer here to set up the mandate
data.sessionKeyPaga’s ID for the set-up session
data.statusINITIALIZED until your customer starts
data.sessionExpiresAtWhen the set-up link stops working, in UTC, set by sessionExpirySeconds
data.paymentExpiresAtWhen the mandate expires, in UTC, set by paymentExpirySeconds
data.metadata.amountLimitThe most you can charge in a single debit
data.metadata.authorizationAmountThe first payment taken during set-up, if you set one. null otherwise
data.metadata.accountReferenceThe 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

Direct Debit set-up start screen explaining how it works, with a Get started button

Your customer sees how Direct Debit works and selects Get started.

1. Confirm it’s you

Confirm it’s you screen asking for a 6-digit code sent by email

They confirm their phone, bank and account, then enter a one-time code.

2. Transfer to authorize

Transfer to authorize screen showing ₦50 to ₦100, the account number, NIBSS Mandate Activation and Paga

They send the amount shown from the account being set up.

🚧

Authorise from an approved channel

The 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"
}
Mandate callback fields (15)
FieldMeaning
eventTokenization
notificationIdUnique ID for this notification. De-duplicate on it
statusCode004 when verified, 0 when approved. See the status codes below
statusMessageVERIFIED or APPROVED
mandateStatusThe mandate’s status: VERIFIED or APPROVED
referenceNumberYour set-up reference
accountReferenceThe mandate. Charge it with this once approved
customerEmailCustomer’s email address
customerPhoneNumberCustomer’s phone number
bankNameThe customer’s bank
accountNumberThe customer’s account that will be debited
currencyNGN
receivedAuthorizationAmountThe first payment received during set-up, if you set authorizationAmount. Otherwise null. This is the only confirmation of that payment
timeStampWhen Paga sent the notification, in UTC
hashSignature 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)
statusCodeMeaning
003Pending. Created, but not yet authorised
004Verified. The customer authorised it; waiting for the bank
0Approved. Ready to charge
005Rejected. The authorisation was declined
006Unknown. The processor can’t confirm the status
007Not 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.statusCode and data.statusMessage stay 1 and pending whatever happens to the mandate, including after it’s approved or disabled, so they can’t tell you whether to charge. Read data.additionalData.mandate[].ProcessStatusId instead, and charge only when it’s APPROVED.

FieldMeaning
statusCode0 means your status request worked
data.referenceNumberYour set-up reference
data.statusCodeAlways 1, even after the mandate is approved or disabled. Don’t use it to decide whether to charge
data.statusMessageAlways pending, for the same reason. Ignore it too
data.requestAmountThe most you can charge in a single debit
data.totalPaymentAmountThe limit including fees
data.paymentMethods[].propertiesThe authorisation transfer details: amount, account number and bank
data.expiryDateTimeUTCWhen the mandate expires, in UTC
data.additionalData.mandate[].ProcessStatusIdThe 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[].AccountReferenceThe mandate you charge
data.additionalData.mandate[].CreatedDateTimeUTCWhen 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)
ParameterRequiredDescription
referenceNumberYesYour unique reference for this charge. Hash value 1
amountYesAmount to charge, up to the mandate’s limit. Hash value 2
accountReferenceYesThe 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)
FieldMeaning
eventCharge_Complete
notificationIdUnique ID for this notification. De-duplicate on it
statusCode0 means the charge succeeded. See the charge status codes below
statusMessageReadable status, such as SUCCESSFUL
processStatusIdThe charge’s status, such as SUCCESSFUL
referenceNumberYour charge reference
accountReferenceThe mandate that was charged
amountAmount charged, as a string with two decimal places and no commas, such as 50000.00
currencyNGN
senderNameName on the debited account
sourceBankNameThe customer’s bank
sourceBankAccountNumberThe customer’s debited account
hashSignature 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"
  }
}
0

Charged successfully.

1

Pending. Wait for the callback.

-1

Invalid: wrong details, expired mandate or limit exceeded.

-4

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.