OuiPay
Transactions

Transaction Lifecycle

How every money-moving attempt becomes a durable record, from request to settlement.

A transaction is the durable record of any money-moving attempt: a payment, transfer, exchange, or funding. Every attempt creates an entry before any work happens, so the record exists even when the attempt fails.

The pipeline

stateDiagram-v2
    [*] --> pending: Request received
    pending --> processing: Submitted to provider
    processing --> completed: Provider confirmed
    processing --> failed: Provider denied / rule violation
    processing --> unknown: Provider timeout
    unknown --> completed: Reconciliation resolved
    unknown --> failed: Reconciliation resolved
    completed --> [*]
    failed --> [*]

Every money-moving request follows the same pipeline:

Attempt recorded

Before any validation or provider call, the transaction is persisted as an entry with status pending and its own idempotency key. If your request is rejected later: validation, compliance, limits, provider outage: the entry still exists, marked failed with a machine-readable failure.code and the stage where it stopped. Nothing happens off the record.

Checks

PIN step-up, feature flags, compliance screening, and limit checks run against your account. A denial here is a failed transaction, not a silent rejection.

Provider submission

The engine routes to a payment provider, places a hold on funds, and posts the movement. A provider timeout leaves the transaction unknown: the outcome is never guessed. Reconciliation resolves unknown to completed or failed.

Ledger posting

The double-entry ledger is the source of truth. Every status you see via the API reflects a posted journal entry; pending means not yet posted.

Notification

Signed webhooks fire for every status transition. You always know the final state: you never have to poll for it (though GET is available).

The transaction object

{
  "id": "01M2S17G4A0N9MDMXYSCCX8SPS",
  "type": "payment",
  "status": "completed",
  "amount_minor": 101000,
  "fee_minor": 1000,
  "currency": "NGN",
  "customer_id": "01M2S17...",
  "reference": "TRF-2026-00042",
  "idempotency_key": "pay-001",
  "failure": null,
  "created_at": "2026-09-18T01:13:59Z",
  "updated_at": "2026-09-18T01:14:00Z"
}

Failure details

A failed transaction carries where and why it stopped:

"failure": {
  "code": "INSUFFICIENT_FUNDS",
  "message": "Wallet balance is insufficient for this payment.",
  "stage": "settlement"
}

stage is the pipeline position: method_selection, pin_stepup, compliance, limits, quote, order_persistence, provider_call, settlement: so you can distinguish a PIN denial from a provider outage without parsing text.

Status history

Every transition is an append-only event. GET /v1/transactions/{id}/events returns the full trail: who/what moved it, when, and the reason: for audit and support.

Data guarantees

  • Money is integer minor units: 10250 with "NGN" is ₦102.50, never floats
  • Every balance, hold, and movement is a ledger journal; history is append-only
  • Corrections are compensating entries, never edits to posted history
  • Webhooks are HMAC-signed and deduplicated by event_key

On this page