Skip to main content

Payload structure

Every webhook has the same envelope. Transaction details live under data. The envelope below shows every field that can appear — only the fields applicable to a given transaction are actually present (see the note).
The payload carries only the fields relevant to the transaction — inapplicable fields are omitted entirely, not returned as null. This applies at every level: a fiat bank payout has no network, exchange_rate, wallet_address or wallet_chain; a crypto payout has no bank fields; a deposit has no beneficiary. So a field’s absence simply means it doesn’t apply. The only exception is metadata, which is passed through exactly as you supplied it. Match transactions by transaction_id (or reference), and treat any missing field as not applicable.

Field reference

Amount format

All monetary fields — amount, source_amount, destination_amount, and fee_amount — are integers equal to the major-currency value × 100 (two implied decimal places). This is the same scale for every currency, fiat and stablecoin alike — to get the human-readable amount, always divide by 100. To display a value, divide by 100 (e.g. amount / 100) for any currency.
These values are not major units. An amount of 10000 on an NGN transaction means ₦100.00, not ₦10,000, and 10000 on a USDC transaction means 100.00 USDC. Treating the integer as a major amount will overstate it by 100×.
Webhook amounts match the REST transaction endpoints. Both report amounts as major × 100 (e.g. 1000 = $10.00), so the same transaction shows 1000 in its webhook and 1000 over REST. Match a webhook to a REST record by transaction_id (or reference) rather than by comparing amounts — a reference is stable where an amount is not unique.Before 9 September 2026 the REST transaction responses reported major units (10 = $10.00) while webhooks reported 1000. If your integration converted between the two, remove that conversion.
For a payout, amount is what you asked to send (the beneficiary’s amount). source_amount is what’s debited from the paying wallet. Where the fee is deducted from the payout itself, that means source_amount = amount + fee_amount. Where the fee is charged to a different account, source_amount = amount and the fee is debited separately — see below. Either way, fee_amount is the fee; don’t compute it from the other two. All scaled by 100.

Fees paid by another account

Your account can be configured so that fees on transactions are charged to a different account of yours rather than deducted from each transaction. When that applies, the fee is booked as its own transaction on the paying account, and the payout carries a link to it:
Three things to know when reconciling:
  • The fee does not arrive as its own webhook. Only deposits and payouts emit webhooks — fee transactions never do. The link above is the only notification you receive.
  • source_amount excludes the fee here, because the fee never touched this transaction. It equals amount.
  • On the fee transaction itself, fee_amount is 0 and the fee value is carried as its source_amount / destination_amount. This keeps the fee from being counted twice when totalling fee_amount across transactions. Read the fee from the payout’s fee_amount.
Looking the fee transaction up over REST (GET /wallet/transactions) returns it with related_transaction_id and related_reference pointing back at the payout, so you can reconcile from either direction.
Both fields are omitted entirely — not null — when no separate fee transaction exists. Branch on their presence. They were introduced on 18 August 2026; webhooks you captured before then do not carry them even where a fee transaction exists.

The beneficiary object (payouts)

Drawn from this whitelisted set — but only the fields relevant to the payout are returned. Empty/null fields are omitted, so a fiat bank payout returns bank-account fields (no wallet fields), while a crypto payout returns wallet fields (no bank fields). Internal fields (your business_id, row timestamps) are never included. id · account_name · account_number · bank_name · bank_code · bank_address · currency · withdrawal_method · routing_number · swift_code · iban · bic · sort_code · intermediary_bank_name · intermediary_bank_routing_number · account_owner_type · account_category · wallet_address · wallet_chain · email · contact_person · label · beneficiary_address
wallet_chain is the full network name (Solana). Address fields are passed through exactly as stored on the beneficiary, so inside bank_address / beneficiary_address, country is normally the 2-letter ISO code you supplied (CI); records created before that code was enforced may still hold a full country name. A present id means the payout went to a saved beneficiary (it’s the beneficiary’s UUID). One-time (inline) beneficiaries have no id field at all — use its absence to detect them. withdrawal_method is the one field that is never dropped: it is returned as null when no rail was set (an NGN local transfer), because “not set” and “not applicable” mean different things for a payout.
This object is a snapshot for reading, not a request body. If you feed it back into Create Beneficiary or Update Beneficiary, convert any country that is not already a 2-letter ISO code, and drop id. Those endpoints reject a full country name with a 400.

The virtual_account object (deposits)

The destination account a deposit was received into. method is always present and says which shape to expect; every other field is dropped when empty.
The object on a deposit carries no provider and no status (except the operator name on the mobile_money shape). The virtual_account on account.virtual_account.created is a different, slightly wider object: it does carry provider (always rolla) and status. Don’t assume one from the other.

The payer object (deposits)

Who sent the deposit, as reported by the provider. Fields: name, account_number, bank_name, country — only the ones the provider supplies are included, so availability varies by rail:
  • USD — always name and account_number; bank_name and country are included when the sending rail reports them.
  • NGN — name, account_number and bank_name from the NIP transfer; country is not reported.
  • Rolla transfer (receiving side) — name is the sending account’s name and bank_name is "Rolla". The sender’s email is never included.
  • Sandbox simulated USD deposit — name only: the sender_name you passed, or "Sandbox Sender" when you omitted it.
payer is omitted when the provider sends no sender details. Crypto deposits expose the sender via the network.source_address field instead of payer.

Which rail a deposit arrived on

payment_rail tells you how an incoming payment reached the account, so you can reconcile per channel instead of parsing the narration.
There is no swift value: an international wire is the SWIFT case, reported as wire_international.payment_rail is omitted — not null — when the provider does not report a transfer method, and it never appears on payouts or crypto deposits. Treat its absence as “rail unknown”, not as a specific rail.
In sandbox, pass payment_rail (ach, fedwire or swift) to Simulate an Account Deposit or Simulate a USD Deposit to receive each value here. Simulated deposits default to ach.

Sample: Fiat deposit completed


Sample: USD deposit completed (international wire)

A USD deposit received over SWIFT. payment_rail identifies the channel; the rest of the shape is the same as any other fiat deposit.

Sample: Deposit into a customer virtual account

Same shape as a fiat deposit, plus customer_identifier — the identifier you set when creating the account, so you can credit the right end-user. Only deposits into a customer virtual account include it.

Sample: Rolla transfer received

The receiving side of a Rolla transfer — for example a merchant account forwarding funds to your institutional account. payer names the sending account, and metadata is included because both accounts share an owner.

Sample: Fiat payout completed


Sample: FX payout (NGN → USD) pending

A cross-currency payout. exchange_rate is populated (the quoted rate for the pair), and the wire beneficiary’s bank_address.country is the full country name. Only the bank-relevant beneficiary fields are present.
Here exchange_rate is destination_amount / source_amount = 100000 / 160000000 = 0.000625 (USD minor units per NGN minor unit). Use source_amount and destination_amount for exact reconciliation — they are the authoritative debit and credit legs in minor units.

Sample: Stablecoin deposit completed


Sample: Stablecoin payout pending (one-time beneficiary)


Sample: Payout UETR assigned

transaction.uetr.assigned reports that a cross-border payout’s SWIFT reference became available — see Events. Its data is deliberately narrow: the reference, plus enough of the payout to match it to your own record without a further call.
The fee, FX and amount-leg fields (source_amount, exchange_rate, fee_amount, …) are not on this payload — nothing about the money changed. Take them from the payout’s own status events.

Account event payloads

Account events (account.onboarded, account.submitted, account.approved, account.rejected, account.changes_requested, account.virtual_account.created, account.verification.completed) describe an account rather than a transaction, so data carries the account fields. transaction_id is not present.

Sample: Account approved

Sample: Account rejected

review.reason is the reviewer’s own wording — there is no separate email, so this payload is the only notification carrying it.

Sample: Changes requested

The account reopens for editing. Fix what review.reason describes and resubmit — the account then emits account.submitted again.
review is omitted entirely when a decision was recorded without a note, so treat it as optional.

Sample: Virtual account created

Sample: Verification completed

Sent when an identity check finishes, so you learn the result instead of polling Get Requirements.
A passed check does not mean the account is submittable — ready_to_submit above is false because the application is still missing data. Call Get Requirements and read submitBlocker for what remains.
On a business, one event is sent per beneficial owner, with related_person_id naming who cleared. ready_to_submit stays false until the last of them is done.

Payer name event payloads

Payer name events (payer_name.approved, payer_name.changes_requested, payer_name.rejected, payer_name.active, payer_name.action_required) describe a payer name — the name a beneficiary sees on payouts you send under Named Payouts. data carries the payer name; transaction_id is not present.

Sample: Payer name active

The event that matters operationally — the name is registered and payouts can now go out under it.

Sample: Changes requested

reason is the reviewer’s own wording and is the entire point of this event — without it you know only that something is wrong.
Answer it with Update Payer Name, which resubmits the name for review.
Which payout partner is registering a name never appears in these payloads, the same way deposit events report the provider as rolla. status is the single view of how far the name has got, across our review and every partner’s.