Event types
The gateway emits one webhook per state transition on a
transaction. The body is a flat JSON object keyed by
transaction_id and status. There is no top-level event_type
field today — the status value tells you everything.
Event shape
Section titled “Event shape”{ "transaction_id": "txn_8f3a4c2e9b1d7a6f5c0e8d", "customer_reference": "cust_abc123", "status": "approved", "amount": "12.50", "currency": "usd", "fee_amount": "0.50", "total_amount": "13.00", "created_at": "2026-05-05T10:15:00Z", "completed_at": "2026-05-05T10:15:08Z", "additional_data": { "provider_reference": "onafriq_ref_abc123" }}Field-by-field:
| Field | Always present? | Notes |
|---|---|---|
transaction_id | Yes | Match against your local order. |
customer_reference | Yes | Echoed from your /payments/collect body. |
status | Yes | The transition. See enum below. |
amount | Yes | Decimal string. |
currency | Yes | Lowercase ISO 4217. |
fee_amount | On terminal states | The fee charged. |
total_amount | On terminal states | amount + fee_amount. |
created_at | Yes | When the transaction was first created. |
completed_at | On terminal states | Settlement time. |
failure_reason | On failed / declined / cancelled | Human-readable reason. |
additional_data | Sometimes | Provider-specific metadata. |
status values you can expect
Section titled “status values you can expect”The full lifecycle:
| Status | Meaning | Terminal? |
|---|---|---|
pending | Just created, awaiting processor | No |
processing | Processor accepted, doing the work | No |
pending_otp_verification | Waiting for customer OTP | No |
pending_mobile_money_verification | Waiting for carrier-side authorization | No |
pending_email_verification | Waiting for email-based confirmation | No |
pending_bank_validation | Bank-side validation in progress | No |
pending_bank_proof_upload | Customer must upload proof of wire | No |
pending_bank_submission | Awaiting our operator review | No |
bank_payment_validated | Operator confirmed wire received | No |
approved | Money moved successfully | Yes |
declined | Issuer / processor declined | Yes |
failed | System or processor error | Yes |
cancelled | Customer or system cancelled | Yes |
expired | TTL exceeded without completion | Yes |
refunded | Approved earlier, then refunded | Yes |
You should expect:
- Multiple webhooks per transaction. A typical card payment
emits
pending→processing→approved. Your handler must be idempotent and tolerate duplicates. - No webhook for
pendingin some flows where the customer is still on your collect page. The first webhook may beprocessingor directlyapproved. refundedmay arrive much later than the originalapproved— refunds happen on a different timescale.
B2C payouts
Section titled “B2C payouts”/payments/send emits webhooks per beneficiary, not per batch:
{ "transaction_id": "txn_b2c_8f3a4c", "customer_reference": "payout_2026_05_05_001", "status": "completed", "amount": "10.00", "currency": "usd", "additional_data": { "batch_id": "batch_xyz789", "beneficiary_phone_number": "+243998857000", "beneficiary_index": 0 }, "created_at": "2026-05-05T10:15:00Z", "completed_at": "2026-05-05T10:15:05Z"}For a batch of 100 beneficiaries you’ll receive up to 200 webhooks
(one for pending/processing plus one for each terminal state).
Plan accordingly.
Recurring schedules
Section titled “Recurring schedules”sending_type: recurring emits one set of webhooks per
occurrence. The first occurrence’s webhooks arrive on
recurring_start_date; subsequent occurrences as the schedule
fires. Each occurrence has its own transaction_id.
The customer_reference is the same across all occurrences — use
transaction_id to disambiguate.
Status transitions you should NOT see
Section titled “Status transitions you should NOT see”Some transitions are illegal. If you see them, treat as a bug report:
approved→pendingdeclined→approved- Any terminal state → any non-terminal state (except
refunded, which is a fresh terminal state on a previously-approvedtransaction)
See also
Section titled “See also”- Webhooks overview — delivery semantics
- Concepts → Errors — a
failedevent includes afailure_reason - Concepts → Payment methods — sync vs async per method