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 differ from the REST transaction endpoints. Webhook payloads report amounts as major × 100 (e.g. 1000 = 10.00).TheRESTtransactionresponsesGET/wallet/transactions,GET/wallet/transaction/id,andthewithdraw/transfer/swapresponsesreporttransactionamountsinmajorunits(e.g.10=10.00). The REST transaction responses — `GET /wallet/transactions`, `GET /wallet/transaction/{id}`, and the `withdraw` / `transfer` / `swap` responses — report transaction amounts in **major units** (e.g. `10` = 10.00). So the same transaction shows 1000 in its webhook and 10 over REST. Match a webhook to a REST record by transaction_id (or reference), not by comparing raw amounts, and apply the right scale for each source.
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 · wallet_address · wallet_chain · email · contact_person · label · beneficiary_address
wallet_chain is the full network name (Solana). Inside bank_address / beneficiary_address, country is the full country name (e.g. Côte d'Ivoire, not CI). 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.

The virtual_account object (deposits)

The destination account a deposit was received into. For a bank (fiat) deposit: id (the virtual account’s Rolla id), currency, method (bank_transfer), provider, bank_name, account_number, account_name, and — when the account has them — routing_number / swift_code. For a stablecoin deposit: id, currency, method (crypto), provider, wallet_address, network. Empty fields are omitted.

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.
payer is omitted when the provider sends no sender details. Crypto deposits expose the sender via the network.source_address field instead of payer.

Sample: Fiat deposit completed


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: 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)

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.