Payload structure
Every webhook has the same envelope. Transaction details live underdata. 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.
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.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:- 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_amountexcludes the fee here, because the fee never touched this transaction. It equalsamount.- On the fee transaction itself,
fee_amountis0and the fee value is carried as itssource_amount/destination_amount. This keeps the fee from being counted twice when totallingfee_amountacross transactions. Read the fee from the payout’sfee_amount.
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.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
nameandaccount_number;bank_nameandcountryare included when the sending rail reports them. - NGN —
name,account_numberandbank_namefrom the NIP transfer;countryis not reported. - Rolla transfer (receiving side) —
nameis the sending account’s name andbank_nameis"Rolla". The sender’s email is never included. - Sandbox simulated USD deposit —
nameonly: thesender_nameyou 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, pluscustomer_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 whatreview.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.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.
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.