Skip to main content
This is the page for judges and integration engineers who want the full inventory in one place. Twenty-three OPay surfaces. Every webhook event we handle. Every signature scheme we accept. No gaps. For the design rationale and the philosophical framing, read OPay integration first. This page is the reference.

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 our webhookEvents 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 is crypto.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

The defence is layered. Signature alone is not sufficient. We require OPay’s own authoritative endpoint to corroborate. An attacker would have to compromise both our webhook secret and OPay’s database to fake a credit. The second is implausible. So the credit is safe.

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:
Application code reads 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 an opossum 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 chargeCard or debitMandate.
  • OPay’s hosted dispute UI. Disputes resolve through our own ops review process.
  • OPay’s chatbot integrations. Not relevant to our channel architecture.

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.