OPay Payment Initialization

Use this endpoint to initialize an OPay payment transaction. On success, you receive a payment_url that you redirect your customer to in order to complete payment.

This default endpoint settles into the merchant’s default wallet. Do not send wallet_code to this endpoint. To collect into a specific NGN wallet, use POST /api/s2s/v1/transaction/opay/initialize/wallet.

Endpoint

POST https://backendapi.sayswitchgroup.com/api/s2s/v1/transaction/opay/initialize

Headers

Authorization: Bearer YOUR_SECRET_KEY
Content-Type: application/json

Request Body Parameters

ParameterTypeRequiredValidation Rules
emailstringRequiredValid email address, minimum 6 characters
amountnumericRequiredMinimum value: 1
referencestringOptionalMin 16 characters, max 33 characters
first_namestringOptionalMaximum 100 characters
last_namestringOptionalMaximum 100 characters
callbackUrlstringOptionalMust be a valid URL
typestringOptionalPayment method. One of wallet (default, hosted checkout), walletQR, bankTransfer, bankUssd, bankAccount. All values other than wallet use the server-to-server flow — see the corresponding sections below.
bank_codestringConditionalRequired when type is bankUssd or bankAccount. CBN bank code (e.g. 033).
account_numberstringConditionalRequired when type is bankAccount. Customer’s bank account number.
bvnstringConditionalRequired when type is bankAccount. Customer’s 11-digit BVN.
dob_daystringConditionalRequired when type is bankAccount. Customer’s day of birth (e.g. 12).
dob_monthstringConditionalRequired when type is bankAccount. Customer’s month of birth (e.g. 12).
dob_yearstringConditionalRequired when type is bankAccount. Customer’s year of birth (e.g. 2000).

Sample Request Body

{
    "email": "lekan126@gmail.com",
    "amount": 13,
    "callbackUrl": "https://www.yoursite.com"
}

Sample cURL

curl -X POST "https://backendapi.sayswitchgroup.com/api/s2s/v1/transaction/opay/initialize" \
  -H "Authorization: Bearer YOUR_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "lekan126@gmail.com",
    "amount": 13,
    "callbackUrl": "https://www.yoursite.com"
  }'

Validation Error Response

If validation fails, the API returns a 400 status with the following structure:

{
    "status": false,
    "message": "The email field is required.",
    "error": {
        "email": ["The email field is required."],
        "amount": ["The amount field is required."]
    }
}

Success Response

On success, redirect your customer to the payment_url returned in the response to complete payment on the OPay checkout page.

{
    "status": true,
    "message": "Proceed payment",
    "data": {
        "payment_url": "https://express.opaycheckout.com/apiCashier/redirect/payment/checkoutHome?orderToken=TOKEN.fd20af62...",
        "reference": "SSW_17793454451101895",
        "status": null
    }
}

Response Fields

FieldTypeDescription
statusbooleanIndicates whether initialization succeeded.
messagestringResponse message.
data.payment_urlstringOPay checkout URL where the customer completes payment.
data.referencestringUnique transaction reference.
data.statusstring/nullCurrent transaction status returned during initialization.

Generate a QR Code (server-to-server)

To collect payment via a scannable QR code rendered inside your own application, add the type parameter to the request body and set its value to walletQR. All other parameters (email, amount, callbackUrl, etc.) work exactly as described above.

"type": "walletQR"

Unlike the default checkout flow, walletQR does not return a payment_url and does not involve a redirect to an OPay-hosted page. Instead, the response contains the QR code content in the qr_code field. You encode this string into a QR code using any standard QR library and display it in your application. The customer scans it with the OPay app to complete payment.

Sample Request Body

{
    "email": "lekan126@gmail.com",
    "amount": 13,
    "callbackUrl": "https://www.geegpay.africa",
    "type": "walletQR"
}

Sample cURL

curl -X POST "https://backendapi.sayswitchgroup.com/api/s2s/v1/transaction/opay/initialize" \
  -H "Authorization: Bearer YOUR_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "lekan126@gmail.com",
    "amount": 13,
    "callbackUrl": "https://www.geegpay.africa",
    "type": "walletQR"
  }'

Sample Response

{
    "status": true,
    "message": "Proceed payment",
    "data": {
        "reference": "be84102f-44bd-4caa-914b-7b071994b2f2",
        "status": "pending",
        "order_no": "260715140300593305",
        "next_action": {
            "actionType": "SCAN_QR_CODE",
            "qrCode": "https://opay.ng/macquiring?qrContent=https://qrm.operapay.com//m0000260715140300593305728794_macquiring_macquiring"
        },
        "qr_code": "https://opay.ng/macquiring?qrContent=https://qrm.operapay.com//m0000260715140300593305728794_macquiring_macquiring"
    }
}

Response Fields

FieldTypeDescription
statusbooleanIndicates whether initialization succeeded.
messagestringResponse message.
data.referencestringUnique transaction reference. Use this to verify the transaction status.
data.statusstring/nullCurrent transaction status returned during initialization (e.g. pending).
data.order_nostringOPay’s internal order number for the transaction.
data.next_action.actionTypestringAlways SCAN_QR_CODE for this flow.
data.qr_codestringThe QR code content. Encode this string into a QR code and display it to the customer.

Displaying the QR Code

The qr_code value is the QR content, not an image. Render it with any standard QR library. For example, on the web:

import { QRCodeSVG } from 'qrcode.react';
 
<QRCodeSVG value={data.qr_code} size={220} level="M" />

Encode the string exactly as returned — do not trim, normalize, or re-encode it. The value must match byte-for-byte for the OPay app to recognize the payment.

On mobile devices, the same string also functions as a tap-to-pay deep link that opens the OPay app directly. You may present it as a “Pay with OPay” button in addition to (or instead of) the rendered QR code.

QR Expiry

Each QR code is bound to a single transaction and expires 30 minutes after initialization. If the customer does not complete payment before expiry, initialize a new transaction with a new reference — references are single-use.

Payment Confirmation

The initialize response confirms the payment request was created — not that funds were received. Final payment status is delivered via webhook to your configured webhook URL (or the callbackUrl supplied at initialization). While the QR is displayed, you may also poll the transaction verify endpoint using data.reference to update your UI in real time. Treat the webhook as the source of truth before delivering value.

Bank Transfer Payment (server-to-server)

Set type to bankTransfer to receive a dedicated one-time bank account. Display the account details to your customer; they complete payment by making a bank transfer to the account from any banking app.

Sample Request Body

{
    "email": "lekan126@gmail.com",
    "amount": 13,
    "callbackUrl": "https://www.geegpay.africa",
    "type": "bankTransfer"
}

Sample Response

{
    "status": true,
    "message": "Proceed payment",
    "data": {
        "reference": "be84102f-44bd-4caa-914b-7b071994b2f2",
        "status": "pending",
        "order_no": "220110144664537659",
        "next_action": {
            "actionType": "TRANSFER_ACCOUNT",
            "transferAccountNumber": "7827845341",
            "transferBankName": "WEMA BANK",
            "expiredTimestamp": 1641773850
        },
        "transfer_account_number": "7827845341",
        "transfer_bank_name": "WEMA BANK",
        "expired_timestamp": 1641773850
    }
}

Response Fields

FieldTypeDescription
data.next_action.actionTypestringAlways TRANSFER_ACCOUNT for this flow.
data.transfer_account_numberstringThe one-time account number the customer transfers to.
data.transfer_bank_namestringThe bank holding the one-time account.
data.expired_timestampintegerUnix timestamp when the account expires. Display a countdown to the customer.

The account is single-use and bound to this transaction’s exact amount. Payment confirmation arrives via webhook once the transfer settles.

Bank USSD Payment (server-to-server)

Set type to bankUssd and supply the customer’s bank_code to receive a USSD string. Display it to the customer; they dial it on the phone number linked to their bank account to authorize payment.

Sample Request Body

{
    "email": "lekan126@gmail.com",
    "amount": 13,
    "callbackUrl": "https://www.geegpay.africa",
    "type": "bankUssd",
    "bank_code": "033"
}

Sample Response

{
    "status": true,
    "message": "Proceed payment",
    "data": {
        "reference": "be84102f-44bd-4caa-914b-7b071994b2f2",
        "status": "pending",
        "order_no": "220110144664561849",
        "next_action": {
            "actionType": "SHOW_USSD",
            "ussd": "*919*000*5195#"
        },
        "ussd": "*919*000*5195#"
    }
}

Response Fields

FieldTypeDescription
data.next_action.actionTypestringAlways SHOW_USSD for this flow.
data.ussdstringThe USSD code the customer dials to complete payment. On mobile, you may render it as a tel: link for one-tap dialing.

Payment confirmation arrives via webhook once the customer completes the USSD prompts.

Bank Account Payment (server-to-server)

Set type to bankAccount to charge a customer’s bank account directly. This flow requires the customer’s account number, bank code, BVN, and date of birth, and involves additional authorization steps — the customer must confirm the charge with an OTP and/or their account PIN, which you collect in your UI and submit via the action endpoints below.

Sample Request Body

{
    "email": "lekan126@gmail.com",
    "amount": 13,
    "callbackUrl": "https://www.geegpay.africa",
    "type": "bankAccount",
    "bank_code": "033",
    "account_number": "2215381176",
    "bvn": "01234567899",
    "dob_day": "12",
    "dob_month": "12",
    "dob_year": "2000"
}

Sample Response

{
    "status": true,
    "message": "Proceed payment",
    "data": {
        "reference": "be84102f-44bd-4caa-914b-7b071994b2f2",
        "status": "pending",
        "order_no": "220110144664579546",
        "next_action": {
            "actionType": "INPUT_OTP"
        }
    }
}

Authorization Flow

After initialization, next_action.actionType tells you which input to collect from the customer next:

actionTypeWhat to do
INPUT_OTPPrompt the customer for the OTP sent to their phone, then call Submit OTP.
INPUT_PINPrompt the customer for their account PIN, then call Submit PIN.

Each submission returns a new next_action (or a final status). Repeat until gateway_status is SUCCESS or the payment fails. A single payment may require OTP followed by PIN, or vice versa — always drive your UI from the returned actionType rather than assuming a fixed order.

Authorization Action Endpoints

The following endpoints complete the Bank Account authorization flow. All use the same Authorization: Bearer YOUR_SECRET_KEY header as initialization.

Submit OTP

POST https://backendapi.sayswitchgroup.com/api/s2s/v1/transaction/opay/otp/submit
ParameterTypeRequiredDescription
referencestringRequiredThe transaction reference from initialization.
otpstringRequiredThe OTP entered by the customer.
{
    "reference": "be84102f-44bd-4caa-914b-7b071994b2f2",
    "otp": "873156"
}

Submit PIN

POST https://backendapi.sayswitchgroup.com/api/s2s/v1/transaction/opay/pin/submit
ParameterTypeRequiredDescription
referencestringRequiredThe transaction reference from initialization.
pinstringRequiredThe account PIN entered by the customer.
{
    "reference": "be84102f-44bd-4caa-914b-7b071994b2f2",
    "pin": "1109"
}

Resend OTP

If the customer did not receive the OTP, request a new one:

POST https://backendapi.sayswitchgroup.com/api/s2s/v1/transaction/opay/otp/resend
ParameterTypeRequiredDescription
referencestringRequiredThe transaction reference from initialization.
{
    "reference": "be84102f-44bd-4caa-914b-7b071994b2f2"
}

Action Endpoint Responses

All three action endpoints share the same response structure.

Continue — another authorization step is required:

{
    "status": true,
    "message": "Proceed payment",
    "data": {
        "reference": "be84102f-44bd-4caa-914b-7b071994b2f2",
        "gateway_status": "PENDING",
        "next_action": {
            "actionType": "INPUT_PIN"
        }
    }
}

Success — authorization complete:

{
    "status": true,
    "message": "Payment successful",
    "data": {
        "reference": "be84102f-44bd-4caa-914b-7b071994b2f2",
        "gateway_status": "SUCCESS"
    }
}

Failure — e.g. wrong OTP/PIN or declined charge:

{
    "status": false,
    "message": "Payment failed",
    "data": {
        "reference": "be84102f-44bd-4caa-914b-7b071994b2f2",
        "gateway_status": "FAIL",
        "failure_code": "..."
    }
}
FieldTypeDescription
data.gateway_statusstringOne of PENDING, SUCCESS, FAIL, CLOSE.
data.next_action.actionTypestringPresent while gateway_status is PENDING — the next input to collect (INPUT_OTP or INPUT_PIN).
data.failure_codestringPresent on failure — the gateway failure code for support enquiries.

Even when an action endpoint returns SUCCESS, the final settlement confirmation is delivered via webhook. Treat the webhook as the source of truth before delivering value.

Security Note (Bank Account Flow)

OTP, PIN, BVN, and account numbers are sensitive customer credentials. Transmit them only over HTTPS directly from your customer-facing form to this API — do not log them, store them, or echo them back in responses on your side.