Webhooks
Do not read the API again and again for a status. Give us an HTTPS endpoint. We send a signed event to it immediately after money moves.
Webhooks cover all the movements you can make with the API: orchestrations, collections, payouts, and deposits into a virtual account.
Configure
Set your endpoint. The response gives a secret one time. Keep it safe:
PATCH /merchants/:merchantId/webhook
{ "url": "https://api.yoursite.com/webhooks/figo" }
{
"code": 200,
"message": "Webhook updated",
"data": {
"webhook_url": "https://api.yoursite.com/webhooks/figo",
"webhook_secret": "whsec_…"
}
}
The URL must be https://. Send an empty url to disable.
Events
| Event | Fires when |
|---|---|
orchestration.awaiting_payin |
An orchestration is created, awaiting the pay-in |
orchestration.paying_out |
The pay-in settled; the pay-out is on its way |
orchestration.completed |
The orchestration recipient has been paid |
orchestration.failed |
The orchestration could not complete |
checkout.session.completed |
A checkout session was paid |
collection.completed |
A collection settled to your balance |
collection.failed |
A collection failed |
payout.completed |
A payout was delivered |
payout.failed |
A payout failed |
virtual_account.deposit.completed |
Money paid into a customer's virtual account settled to your balance |
virtual_account.deposit.failed |
A virtual-account deposit could not be settled |
Payload
Every delivery has the same envelope — the event, its data, and sent_at.
Amounts are USD-faced. There are two data shapes: one for transaction
events (collections, payouts, virtual-account deposits) and one for
orchestration events.
Transaction events
collection.*, payout.* and virtual_account.deposit.* all carry a
transaction. type is deposit for money in, withdrawal for money out;
local_amount/local_currency show the customer/recipient side.
When that side is crypto there is no local_currency. The payload carries
local_asset_id and local_asset_symbol instead:
{
"local_amount": 1.169813,
"local_currency": null,
"local_asset_id": "usdc-solana",
"local_asset_symbol": "USDC"
}
Read local_asset_symbol for the ticker rather than splitting the handle —
the handle names the network too, and USDC on Solana is USDC, not SOL.
// collection.completed — a pay-in settled to your USD balance
{
"event": "collection.completed",
"data": {
"id": "txn_9f2a…",
"type": "deposit",
"status": "success",
"amount": 32.5,
"currency": "USD",
"reference": "invoice_8842",
"local_amount": 50000,
"local_currency": "NGN",
"created_at": "2026-07-24T10:16:57.000Z"
},
"sent_at": "2026-07-24T10:17:03.120Z"
}
// payout.completed — a pay-out was delivered
{
"event": "payout.completed",
"data": {
"id": "txn_1b7c…",
"type": "withdrawal",
"status": "success",
"amount": 100.0,
"currency": "USD",
"reference": "settlement-99",
"local_amount": 158200,
"local_currency": "NGN",
"created_at": "2026-07-24T10:16:57.000Z"
},
"sent_at": "2026-07-24T10:17:03.120Z"
}
// virtual_account.deposit.completed — money paid into a customer's VA
{
"event": "virtual_account.deposit.completed",
"data": {
"id": "txn_4d0e…",
"type": "deposit",
"status": "success",
"amount": 64.9,
"currency": "USD",
"reference": "va_topup_211",
"local_amount": 100000,
"local_currency": "NGN",
"created_at": "2026-07-24T10:16:57.000Z"
},
"sent_at": "2026-07-24T10:17:03.120Z"
}
The .failed variants (collection.failed, payout.failed,
virtual_account.deposit.failed) are identical with "status": "failed".
Orchestration events
orchestration.* carry the orchestration and echo your source/destination.
The same shape arrives as the status advances
awaiting_payin → paying_out → completed (or failed).
// orchestration.completed — the destination leg was delivered
{
"event": "orchestration.completed",
"data": {
"id": "orch_7c1f…",
"status": "completed",
"source": {
"kind": "fiat",
"amount": 50000,
"country": "NG",
"currency": "NGN"
},
"destination": {
"kind": "crypto",
"asset_id": "btc-bitcoin",
"address": "bc1q…"
},
"reference": "order_8841",
"customer_id": "cus_123",
"failure_reason": null,
"created_at": "2026-07-24T10:16:57.000Z"
},
"sent_at": "2026-07-24T10:17:03.120Z"
}
// orchestration.failed — failure_reason is populated
{
"event": "orchestration.failed",
"data": {
"id": "orch_7c1f…",
"status": "failed",
"source": {
"kind": "fiat",
"amount": 50000,
"country": "NG",
"currency": "NGN"
},
"destination": {
"kind": "crypto",
"asset_id": "btc-bitcoin",
"address": "bc1q…"
},
"reference": "order_8841",
"customer_id": "cus_123",
"failure_reason": "Destination leg could not be delivered; funds held in your balance.",
"created_at": "2026-07-24T10:16:57.000Z"
},
"sent_at": "2026-07-24T10:17:03.120Z"
}
orchestration.awaiting_payin and orchestration.paying_out are the same shape
with status set accordingly (and failure_reason: null).
Verify every delivery
We sign the raw request body with your webhook secret (HMAC-SHA512) and send it
in the x-figo-signature header. Recompute it and compare:
const expected = crypto
.createHmac('sha512', webhookSecret)
.update(rawBody) // the exact bytes we sent
.digest('hex');
if (expected !== req.headers['x-figo-signature']) return res.sendStatus(401);
The event name is also in the x-figo-event header.
Delivery & retries
Store the event, then immediately return a 2xx. Process the event after that.
A response that is not 2xx, or a slow response, tells us that the event did not
arrive.
We keep a record of each try. You can see the deliveries and send any of them again. Use the dashboard (Webhooks) or the API.