The 23 methods, by category
Direct Debit / Mandates (3 methods)
The OPay surface that most teams won’t have working end to end. The differentiator.
Lifecycle: create → user sends ₦50 confirmation transfer from their bank app → OPay fires
MandateApproved webhook → OPay fires MandateReady webhook (typically 2-5 min later) → debits become possible.
Banks supported: Access (044), Ecobank (050), Fidelity (070), First Bank (011), Kuda (672).
Payouts (2 methods)
Sync success path. If OPay’s response includes
response_description matching /approved|completed|success/i and a nip_transaction_reference, we mark the payout SUCCESS immediately. No need to wait for the webhook. This shaves seconds off the user-perceived latency.
Webhook fallback. If OPay returns pending, the payout sits in PENDING state until the payout_successful (or payout_failed) webhook arrives. Same signature verification, same reconcile guard.
Virtual Accounts (2 methods)
Static VAs accept any amount from any sender. Dynamic VAs are amount-locked and time-locked; the webhook arrives with
transaction_status: SUCCESS, EXPIRED, or MISMATCH depending on what the sender did.
Reconciliation (2 methods)
verifyTransaction is the critical one. It is the call that defends against signature-replay attacks even if our webhook secret leaks. We never credit anyone’s wallet without it.
Transactional SMS (1 method)
Hosted checkout & card linking (3 methods)
Direct payment fallbacks (5 methods)
When a user has no linked mandate or saved card, these are the alternative paths to fund a wallet or pay a bill.Bills / VAS (5 methods)
Webhook events we handle
Every event OPay fires lands at our webhook endpoint. Every event passes through the same middleware: signature verification → idempotency check → reconcile guard (for credit-bearing events) → routing to the right handler service.The full event taxonomy
Green tiles are credit-bearing — they cause money to move into or out of someone’s wallet. Each one passes through the reconcile guard before the wallet motion fires.Event handlers in detail
Idempotency on inbound webhooks
Every webhook delivery includes OPay’s event ID. We dedupe on it in ourwebhookEvents table. A duplicate delivery (OPay retrying on a 5xx from us) is a no-op — we return 200 without processing again.
A unique constraint on the event ID prevents double-handling at the database level.
The three signature schemes (OPay inbound)
Every comparison iscrypto.timingSafeEqual. The middleware never returns early on a string mismatch; comparison time is constant regardless of how many bytes differ.
The reconcile guard, in pseudocode
Status normalization (OPay’s spelling drift)
OPay’s API sometimes returns the same logical status with different spellings (ValidateOtp, validate_otp, VALIDATEOTP). The wrapper normalizes through a STATUS_LOOKUP table:
DirectChargeNextStep only. The OPay-side spelling drift never escapes the wrapper.
Error classification
The wrapper classifies every error from OPay into one of two kinds:
Callers read the error kind and decide. The wrapper never retries on its own; idempotency keys make caller-side retries safe.
Circuit breakers
Every OPay call routes through anopossum circuit breaker.
When the breaker is closed, calls flow normally. When open, calls fail fast with a
TRANSIENT error without ever reaching OPay. When half-open, a small number of probe calls test recovery; if they succeed, the breaker closes; if they fail, the breaker re-opens.
For read-only endpoints (getTransaction, lookupPayoutAccount), the breaker has a cached-response fallback. If the breaker is open and a recent successful response is in cache, we serve it. This is the demo-resilience play — even if OPay sandbox has a hiccup mid-demo, read paths stay functional.
Per-merchant reference suffix
OPay rejects payout references that don’t carry our merchant suffix. The wrapper appends_<merchantId> automatically; application code passes a clean reference and the wrapper handles the convention. This is invisible to consumers of the wrapper.
What we don’t use from OPay
For completeness:- Recurring billing UI flows. We handle recurring through our own scheduling on
chargeCardordebitMandate. - OPay’s hosted dispute UI. Disputes resolve through our own ops review process.
- OPay’s chatbot integrations. Not relevant to our channel architecture.
What to read next
OPay integration design
The architectural framing of why the wrapper looks the way it does.
Webhook security
The security-focused walkthrough of signature verification.
Resilience & scale
Circuit breakers, demo-resilience tooling, idempotency at scale.
Data flow
How OPay events propagate through the rest of the system.