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.