1. Webhooks
CrossFX Payment APIs
  • Guides
    • CrossFX API Documentation
    • Support & Contact
    • API Basics
    • Authentication Guide
    • FX Rates
      • FX Rates Guide
    • Corridors
      • Supported Corridors Guide
    • Banks
      • Fetching Banks List Guide
      • Fetching Bank Branches Guide
    • Sanctions
      • Sanctions Screening Guide
    • Beneficiaries
      • Create Beneficiary Guide
      • Get Beneficiary Guide
    • Transfers
      • Generate Quote Guide
      • Confirm Transfer Guide
      • Get Transfer Status Guide
    • Crypto Deposit Address
      • Crypto Deposit Address Guide
    • Payments
      • Initiate Deposit Guide
      • Initiate Payout Guide
    • Webhooks
      • Webhooks Guide
      • Webhook Subscriptions Guide
      • Webhook Events Guide
      • Webhook Delivery Attempts Guide
    • Sandbox
      • Sandbox Data & Testing Guide
  1. Webhooks

Webhooks Guide

Notifications for deposit, payout, refund, and transfer lifecycle events.

Introduction#

CrossFX is asynchronous. Key transfer events, from deposit confirmation to payout completion, reach your system as webhooks, so your application does not have to poll for state that has not changed yet.
This page covers subscribing an endpoint, verifying the signature on every delivery, handling retries, and the payload of each event type.

How webhooks work#

When a transfer, deposit, payout, or refund changes state, CrossFX sends an HTTPS POST to your subscribed URL. Each request carries:
A JSON body describing the event
Three signature headers used to verify authenticity
A unique event_id
The event_type and a created_at timestamp
Your handler should verify the signature, process the event, then respond 2xx. An unacknowledged event is retried.

The webhook endpoints#

The webhook surface splits into three families. Each has its own guide, and you reach for them at different moments.
FamilyAnswersGuide
SubscriptionsWhich of my endpoints receive which events, and what signs themWebhook Subscriptions Guide
EventsWhat did the platform raise, and can I have it againWebhook Events Guide
Delivery attemptsWhat happened when it was sent to one endpointWebhook Delivery Attempts Guide
Set up with the first. Debug with the other two: start from the event to find what was raised, then read its delivery attempts to find out why one endpoint did not get it.
Webhook Subscriptions Guide
Register an endpoint, choose its event types, and rotate its signing secret.
Webhook Events Guide
Search what was emitted, read the sent payload, and replay what dead-lettered.
Webhook Delivery Attempts Guide
The forensic record of one HTTP request to one endpoint.

Subscribing an endpoint#

Your endpoint must:
Be reachable over HTTPS
Answer 2xx on successful receipt
Process events idempotently, keyed on event_id
Verify the signature before acting on the body
Answer within the read timeout of 30 seconds
Subscriptions are created against your organisation. Each subscription selects the event types it wants and receives its own signing secret. An organisation may hold up to 10 subscriptions that have not been deleted, and a paused subscription still occupies one of those slots.
Call List Webhook Event Types for the event names available to subscribe to, so you do not hard-code the list.
WARNING
Creating a subscription does not start deliveries. A new subscription is pending_verification and receives nothing until you update its status to active. There is no separate verification step to wait for.

Verifying a delivery#

Every delivery is signed with HMAC-SHA256. Three headers carry the proof:
HeaderValue
X-Webhook-Signaturev1=<hex digest>
X-Webhook-TimestampUnix epoch seconds, as a string
X-Webhook-Delivery-IdUnique per delivery attempt, so a retry of the same event carries a new one
The signed message is the timestamp, a literal ., then the raw request body:
message   = "{X-Webhook-Timestamp}.{raw request body}"
signature = "v1=" + hex(HMAC_SHA256(signing_secret, message))
Verify against the bytes you received. Parsing the JSON and re-serialising it changes key order and whitespace, and the digest will not match. Compare digests in constant time.
Reject a delivery whose signature does not match, and reject one whose X-Webhook-Timestamp is far from your own clock, so a captured request cannot be replayed later.

Your signing secret#

CrossFX generates the secret. You do not supply one.
Each subscription is issued a secret of the form whsec_ followed by 64 hexadecimal characters. It is returned in exactly two places:
The response to creating the subscription
The response to rotating the secret
It is never returned by a read of the subscription, so store it securely when you first receive it. If it is lost, rotate to get a new one.
Rotate with Rotate Webhook Signing Secret. Deliveries are signed with the new secret from the moment rotation completes, so deploy the new secret before you rotate, or accept a short window of rejected deliveries that will be retried.

Delivery & retry#

Delivery is at-least-once, so duplicates are possible and your handler MUST be idempotent. Deduplicate on event_id.
SettingValue
Attempts after the firstmax_retries on the subscription, 8 by default
BackoffExponential: 30 seconds, then doubling on each attempt
Maximum delay between attempts4 hours
Connect timeout10 seconds
Read timeout30 seconds
So the delays run 30 seconds, 1 minute, 2 minutes, 4 minutes, and on up, capped at 4 hours between attempts.
An attempt is retried when your endpoint times out, answers a 4xx or 5xx, or the connection or TLS handshake fails. Once the attempts are exhausted the event moves to dead_letter and CrossFX stops. Nothing is lost: fix your endpoint, then replay the event. See the Webhook Events Guide.
A subscription that fails 50 deliveries in a row moves to auto_disabled and stops receiving events. Watch consecutive_failures on the subscription, and set it back to active yourself once your endpoint is healthy. The platform does not re-enable it for you.

Event types#

Event typeDescription
deposit.succeededA deposit was received and confirmed.
deposit.failedA deposit failed or expired.
payout.succeededA payout was delivered.
payout.failedA payout failed.
refund.succeededA refund succeeded.
refund.failedA refund failed.
transfer.status.changedA transfer moved to a new status.

Envelope#

Every event body has the same four top-level keys. The event-specific fields live under data.
KeyDescription
event_idevt_ followed by 24 hexadecimal characters. Deduplicate on this.
event_typeOne of the event types above
created_atWhen the event was raised, ISO-8601 UTC
dataThe deposit, payout, refund, or transfer that changed
WARNING
details is opaque. On a deposit, data.details is a passthrough of method-specific funding detail. Its keys vary by funding path and are not a stable contract. Read the named fields beside it, and treat details as diagnostic only.

Sample Event Payloads#

deposit.succeeded#

Fired when funds for a deposit have been successfully received and confirmed. The associated Transfer object will now have a status of pending_payout.
{
  "event_id":"evt_1M8abc2eZvKYlo2Cg6vY7aBc",
  "event_type":"deposit.succeeded",
  "created_at":"2026-04-28T09:22:03.840890+00:00",
  "data": {
      "cbtx_tracking_id":"bb88884b-5555-4eb6-ad36-6c888841b6e4",
      "deposit_id":"bb88884b-2222-4eb6-ad36-6c888841b6e4",
      "status":"succeeded",
      "method":"CRYPTO",
      "amount_expected":"102.00",
      "amount_received":"102.00",
      "currency_code":"USDT",
      "details":{
         "transaction_hash":"0xaae9f30931c20f69c7c6974cd4fcbf6392692242a661d9a2206a2ed0d55eff49",
         "chain":"POLYGON",
         "method":"CRYPTO",
         "user_id":48213
      },
      "provider_reference":"0xaae9f30931c20f69c7c6974cd4fcbf6392692242a661d9a2206a2ed0d55eff49",
      "failure_reason":null,
      "received_at":"2026-04-28T09:20:03.840890+00:00",
      "initiated_at":"2026-04-28T09:20:03.840890+00:00",
      "failed_at":null,
      "created_at":"2026-04-28T09:20:03.840890+00:00"
   }
}

deposit.failed#

Fired when a deposit fails (e.g., a fiat payment was declined). The associated Transfer object will now have a status of failed.
{
  "event_id":"evt_1M8def2eZvKYlo2Cg7vY8cDe",
  "event_type":"deposit.failed",
  "created_at":"2026-04-28T06:39:13.344039+00:00",
  "data": {
      "cbtx_tracking_id":"bb88884b-5555-4eb6-ad36-6c888841b6e4",
      "deposit_id":"bb88884b-2222-4eb6-ad36-6c888841b6e4",
      "status":"failed",
      "method":"CRYPTO",
      "amount_expected":"102.00",
      "amount_received":"102.00",
      "currency_code":"USDT",
      "details":{
         "transaction_hash":"0xaae9f30931c20f69c7c6974cd4fcbf6392692242a661d9a2206a2ed0d55eff49",
         "chain":"POLYGON",
         "method":"CRYPTO",
         "user_id":51774
      },
      "provider_reference":"0xaae9f30931c20f69c7c6974cd4fcbf6392692242a661d9a2206a2ed0d55eff49",
      "failure_reason":"Network error",
      "received_at":"2026-04-28T06:37:10.320221+00:00",
      "initiated_at":"2026-04-28T06:37:12.320221+00:00",
      "failed_at":"2026-04-28T06:37:13.320221+00:00",
      "created_at":"2026-04-28T06:37:04.568509+00:00"
   }
}

payout.succeeded#

Fired when a payout has been successfully delivered to the beneficiary's account. The associated Transfer object will now have a status of completed.
{
  "event_id": "evt_1M8fgh2eZvKYlo2Ch7iJ0kFg",
  "event_type": "payout.succeeded",
  "created_at": "2026-04-29T13:33:16.938723+00:00",
  "data": {
      "cbtx_tracking_id":"bb88884b-9999-4eb6-ad36-6c888841b6e4",
      "payout_id":"bb88884b-2222-4eb6-ad36-6c888841b6e4",
      "status":"succeeded",
      "amount":"12900.00",
      "currency_code":"KES",
      "provider_reference":"reference_001",
      "beneficiary_id":"b0953e06-4444-4e00-9600-e99d22eff9c4",
      "created_at":"2026-04-29T13:33:15.766744+00:00",
      "paid_at":"2026-04-29T13:33:16.887975+00:00",
      "initiated_at":"2026-04-29T13:33:15.874079+00:00",
      "failed_at":null,
      "failure_reason":null
   }
}

payout.failed#

Fired when a payout could not be delivered to the beneficiary (e.g., invalid account details, beneficiary's account is frozen). The associated Transfer object will now have a status of refunding. This transfer is now eligible for a refund.
{
  "event_id": "evt_1M8ijk2eZvKYlo2Ci8jK1lGh",
  "event_type": "payout.failed",
  "created_at": "2026-04-29T13:39:51.094603+00:00",
  "data": {
      "payout_id": "bb88884b-2222-4eb6-ad36-6c888841b6e4",
      "cbtx_tracking_id": "bb88884b-9999-4eb6-ad36-6c888841b6e4",
      "status": "failed",
      "amount": "130.00",
      "currency_code": "KES",
      "beneficiary_id": "b0953e06-4444-4e00-9600-e99d22eff9c4",
      "failure_reason":"Provider rejected the payout",
      "provider_reference": "reference_001",
      "created_at": "2026-04-29T13:39:29.686921+00:00",
      "paid_at":null,
      "initiated_at":"2026-04-29T13:39:32.922633+00:00",
      "failed_at": "2026-04-29T13:39:51.060240+00:00"
   }
}

refund.succeeded#

Fired when a refund has been successfully processed to the client's account. The associated Transfer object will now have a status of refunded.
{
   "data":{
      "amount":"1.00",
      "status":"succeeded",
      "failed_at":null,
      "refund_id":"fda02b85-dddd-48dc-9c7d-11a96d98d19b",
      "created_at":"2026-04-29T13:42:49.501420+00:00",
      "refunded_at":"2026-04-29T13:49:14.144880+00:00",
      "initiated_at":"2026-04-29T13:42:53.480430+00:00",
      "currency_code":"USDC",
      "beneficiary_id":"21b1a7bd-3248-4c73-6666-20c93bcfa3a8",
      "failure_reason":null,
      "cbtx_tracking_id":"b47d984a-d90b-cccc-a35d-c6405bfcdf64",
      "provider_reference":"8d9b2723-3333-40f4-a0c8-a61e0be1963c"
   },
   "event_id":"evt_c4109e67rrrr42a4885b003b",
   "created_at":"2026-04-29T13:49:14.164389+00:00",
   "event_type":"refund.succeeded"
}

refund.failed#

Fired when a refund could not be processed. The associated Transfer object will now have a status of refund_failed.
{
   "data":{
      "amount":"1.00",
      "status":"failed",
      "failed_at":"2026-04-29T13:43:17.406046+00:00",
      "refund_id":"fda02b85-2222-48dc-9c7d-11a96d98d19b",
      "created_at":"2026-04-29T13:42:49.501420+00:00",
      "refunded_at":null,
      "initiated_at":"2026-04-29T13:42:53.480430+00:00",
      "currency_code":"USDC",
      "beneficiary_id":"21b1a7bd-3248-4c73-8888-20c93bcfa3a8",
      "failure_reason":"Refund rejected by the provider",
      "cbtx_tracking_id":"b47d984a-6666-4b1c-a35d-c6405bfcdf64",
      "provider_reference":"8d9b2723-aaaa-40f4-a0c8-a61e0be1963c"
   },
   "event_id":"evt_61fcaa6f746555afb5a5a112",
   "created_at":"2026-04-29T13:43:17.424393+00:00",
   "event_type":"refund.failed"
}

transfer.status.changed#

This is a generic event fired every time the status field of a Transfer object changes. It provides a complete, high-level view of the transfer's progress.
{
   "data":{
      "payout":{
         "amount":"130.00",
         "status":"succeeded",
         "paid_at":"2026-04-29T13:33:16.887975+00:00",
         "failed_at":null,
         "payout_id":"4fe98bfa-4444-4e4d-91c8-bdaf6e385074",
         "created_at":"2026-04-29T13:33:15.766744+00:00",
         "initiated_at":"2026-04-29T13:33:15.874079+00:00",
         "currency_code":"KES",
         "beneficiary_id":"b0953e06-044d-0000-9600-e99d22eff9c4",
         "failure_reason":null,
         "cbtx_tracking_id":"7782e623-16c7-7777-9d70-6f701399caf7",
         "provider_reference":"reference_001"
      },
      "refund":null,
      "status":"completed",
      "deposit":{
         "method":"CRYPTO",
         "status":"succeeded",
         "details":{
            "chain":"POLYGON",
            "method":"CRYPTO",
            "user_id":48213,
            "transaction_hash":"0xaae9f30931c20f5dc7c6974cd4fcbf6392472242a661d9a3336a2ed0d55eff49"
         },
         "failed_at":null,
         "created_at":"2026-04-29T13:33:00.586441+00:00",
         "deposit_id":"26685d7b-30b9-4019-9460-5176fca6cf16",
         "received_at":"2026-04-29T13:33:05.221840+00:00",
         "initiated_at":"2026-04-29T13:33:03.559304+00:00",
         "currency_code":"USDC",
         "failure_reason":null,
         "amount_expected":"1.00",
         "amount_received":"1.00",
         "cbtx_tracking_id":"7782e623-16c7-7777-9d70-6f701399caf7",
         "provider_reference":"0xaae9f30931c20f5dc7c6974cd4fcbf6392472242a661d9a3336a2ed0d55eff49"
      },
      "fx_rate":"130.0000",
      "last_name":"LastName",
      "created_at":"2026-04-29T13:32:50.751325+00:00",
      "first_name":"FirstName",
      "entity_type":"individual",
      "completed_at":"2026-04-29T13:34:33.029858+00:00",
      "date_of_birth":null,
      "source_amount":"1.00",
      "beneficiary_id":"b0953e06-044d-0000-9600-e99d22eff9c4",
      "cbtx_tracking_id":"7782e623-16c7-7777-9d70-6f701399caf7",
      "destination_amount":"130.00",
      "source_currency_code":"USDC",
      "destination_currency_code":"KES"
   },
   "event_id":"evt_839cafbf3a5e77777d0ed1",
   "created_at":"2026-04-29T13:34:33.925669+00:00",
   "event_type":"transfer.status.changed"
}
{
   "data":{
      "payout":{
         "amount":"130.00",
         "status":"failed",
         "paid_at":null,
         "failed_at":"2026-04-29T13:42:49.452765+00:00",
         "payout_id":"2a6579f8-23a1-ffff-bbc6-c0c75111d24d",
         "created_at":"2026-04-29T13:42:30.032960+00:00",
         "initiated_at":"2026-04-29T13:42:30.107757+00:00",
         "currency_code":"KES",
         "beneficiary_id":"21b1a7bd-3248-3333-9dde-20c93bcfa3a8",
         "failure_reason":"Provider rejected the payout",
         "cbtx_tracking_id":"b47d984a-d90b-cccc-a35d-c6405bfcdf64",
         "provider_reference":"reference_001"
      },
      "refund":{
         "amount":"1.00",
         "reason":"Payout failed",
         "status":"succeeded",
         "failed_at":null,
         "refund_id":"fda02b85-6cbb-48dc-9c7d-11a96d98d19b",
         "created_at":"2026-04-29T13:42:49.501420+00:00",
         "refunded_at":"2026-04-29T13:49:14.144880+00:00",
         "initiated_at":"2026-04-29T13:42:53.480430+00:00",
         "currency_code":"USDC",
         "beneficiary_id":"21b1a7bd-3248-3333-9dde-20c93bcfa3a8",
         "failure_reason":null,
         "cbtx_tracking_id":"b47d984a-d90b-cccc-a35d-c6405bfcdf64",
         "provider_reference":"8d9b2723-29da-4444-a0c8-a61e0be1963c"
      },
      "status":"refunded",
      "deposit":{
         "method":"CRYPTO",
         "status":"succeeded",
         "details":{
            "chain":"POLYGON",
            "method":"CRYPTO",
            "user_id":48213,
            "transaction_hash":"0xaae9f30931c20f5dc7c6974cd4fcbf6392472242a661d9a3336a2ed0d55eff49"
         },
         "failed_at":null,
         "created_at":"2026-04-29T13:42:14.823115+00:00",
         "deposit_id":"67f657f9-4a1c-41a7-b2d6-e607634ec06a",
         "received_at":"2026-04-29T13:42:20.114905+00:00",
         "initiated_at":"2026-04-29T13:42:17.790207+00:00",
         "currency_code":"USDC",
         "failure_reason":null,
         "amount_expected":"1.00",
         "amount_received":"1.00",
         "cbtx_tracking_id":"b47d984a-d90b-cccc-a35d-c6405bfcdf64",
         "provider_reference":"0xaae9f30931c20f5dc7c6974cd4fcbf6392472242a661d9a3336a2ed0d55eff49"
      },
      "fx_rate":"130.0000",
      "last_name":"LastName",
      "created_at":"2026-04-29T13:42:05.040860+00:00",
      "first_name":"FirstName",
      "entity_type":"individual",
      "completed_at":null,
      "date_of_birth":null,
      "source_amount":"1.00",
      "beneficiary_id":"21b1a7bd-3248-3333-9dde-20c93bcfa3a8",
      "cbtx_tracking_id":"b47d984a-d90b-cccc-a35d-c6405bfcdf64",
      "destination_amount":"130.00",
      "source_currency_code":"USDC",
      "destination_currency_code":"KES"
   },
   "event_id":"evt_408affa2d2378ea394d0da91",
   "created_at":"2026-04-29T13:49:14.175072+00:00",
   "event_type":"transfer.status.changed"
}

Related#

CrossFX API Documentation
The transfer flow these events describe.
Get Transfer Status Guide
Read the current state on demand when a delivery is missed.
Sandbox Data & Testing Guide
The sandbox emits no webhooks. Drive states with the testing headers instead.
Support & Contact
What to send us when a delivery goes wrong.
Modified at 2026-09-08 10:39:43
Previous
Initiate Payout Guide
Next
Webhook Subscriptions Guide
Built with