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/initializeHeaders
Authorization: Bearer YOUR_SECRET_KEY
Content-Type: application/jsonRequest Body Parameters
| Parameter | Type | Required | Validation Rules |
|---|---|---|---|
email | string | Required | Valid email address, minimum 6 characters |
amount | numeric | Required | Minimum value: 1 |
reference | string | Optional | Min 16 characters, max 33 characters |
first_name | string | Optional | Maximum 100 characters |
last_name | string | Optional | Maximum 100 characters |
callbackUrl | string | Optional | Must be a valid URL |
type | string | Optional | Payment 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_code | string | Conditional | Required when type is bankUssd or bankAccount. CBN bank code (e.g. 033). |
account_number | string | Conditional | Required when type is bankAccount. Customer’s bank account number. |
bvn | string | Conditional | Required when type is bankAccount. Customer’s 11-digit BVN. |
dob_day | string | Conditional | Required when type is bankAccount. Customer’s day of birth (e.g. 12). |
dob_month | string | Conditional | Required when type is bankAccount. Customer’s month of birth (e.g. 12). |
dob_year | string | Conditional | Required 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
| Field | Type | Description |
|---|---|---|
status | boolean | Indicates whether initialization succeeded. |
message | string | Response message. |
data.payment_url | string | OPay checkout URL where the customer completes payment. |
data.reference | string | Unique transaction reference. |
data.status | string/null | Current 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
| Field | Type | Description |
|---|---|---|
status | boolean | Indicates whether initialization succeeded. |
message | string | Response message. |
data.reference | string | Unique transaction reference. Use this to verify the transaction status. |
data.status | string/null | Current transaction status returned during initialization (e.g. pending). |
data.order_no | string | OPay’s internal order number for the transaction. |
data.next_action.actionType | string | Always SCAN_QR_CODE for this flow. |
data.qr_code | string | The 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
| Field | Type | Description |
|---|---|---|
data.next_action.actionType | string | Always TRANSFER_ACCOUNT for this flow. |
data.transfer_account_number | string | The one-time account number the customer transfers to. |
data.transfer_bank_name | string | The bank holding the one-time account. |
data.expired_timestamp | integer | Unix 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
| Field | Type | Description |
|---|---|---|
data.next_action.actionType | string | Always SHOW_USSD for this flow. |
data.ussd | string | The 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:
actionType | What to do |
|---|---|
INPUT_OTP | Prompt the customer for the OTP sent to their phone, then call Submit OTP. |
INPUT_PIN | Prompt 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| Parameter | Type | Required | Description |
|---|---|---|---|
reference | string | Required | The transaction reference from initialization. |
otp | string | Required | The 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| Parameter | Type | Required | Description |
|---|---|---|---|
reference | string | Required | The transaction reference from initialization. |
pin | string | Required | The 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| Parameter | Type | Required | Description |
|---|---|---|---|
reference | string | Required | The 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": "..."
}
}| Field | Type | Description |
|---|---|---|
data.gateway_status | string | One of PENDING, SUCCESS, FAIL, CLOSE. |
data.next_action.actionType | string | Present while gateway_status is PENDING — the next input to collect (INPUT_OTP or INPUT_PIN). |
data.failure_code | string | Present 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.