Banxa sends webhook notifications to your webhook endpoint URL whenever an order's status changes or a user's KYC/Account state updates. This eliminates the need to poll our APIs and allows you to track the complete user journey in real-time.
Configure your webhook endpoint URL in the Partner Dashboard. You can set separate URLs for sandbox and production.
The webhook URL must:
- Be publicly accessible over HTTPS.
- Return a
200HTTP response to acknowledge receipt.
Order webhooks are triggered on all order status transitions. We will send an HTTP POST request with a JSON body.
Payload Example:
{
"order_id": "d9efc5d228cb7edfc4b6bb82f7b39f94",
"status": "complete",
"status_date": "2026-01-1604:04:21",
"created_at": "2026-01-1604:04:20",
"updated_at": "2026-01-1604:04:20",
"external_id": null,
"order_type": "BUY",
"crypto_coin": "USDT",
"crypto_blockchain": "ETH",
"crypto_amount": "67.1000000000000000",
"fiat_currency": "AUD",
"fiat_amount": "100",
"asset_price": "1.490312965722801",
"payment_method": "payid-bank-transfer",
"processing_fee": "0",
"network_fee": "0",
"usd_exchange_rate": "1.4923330",
"transaction_hash": "0",
"metadata": []
}| Field | Description |
|---|---|
order_id | The unique Banxa identifier for the order. |
status | The new order status. See Order Statuses for all possible values. |
status_date | ISO 8601 timestamp of the most recent status transition. |
created_at | ISO 8601 timestamp of when the order was initially created. |
updated_at | ISO 8601 timestamp of the last time the order record was modified. |
external_id | The unique ID provided by the partner during order creation (if applicable). |
order_type | The transaction direction: BUY (Fiat to Crypto) or SELL (Crypto to Fiat). |
crypto_coin | The ticker symbol of the cryptocurrency (e.g., USDT, BTC). |
crypto_blockchain | The specific network/blockchain used for the transaction (e.g., ETH, SOL). |
crypto_amount | The total amount of cryptocurrency involved in the transaction. |
fiat_currency | The 3-letter ISO code of the fiat currency used (e.g., AUD, CAD). |
fiat_amount | The total fiat amount of the transaction. |
asset_price | The price of one unit of the cryptocurrency in the source fiat currency. |
payment_method | The specific payment rail used for the transaction (e.g., payid-bank-transfer). |
processing_fee | The service/gateway fee charged by Banxa in the source fiat currency. |
network_fee | The blockchain network (gas) fee in the source fiat currency. |
usd_exchange_rate | The exchange rate used to convert 1 unit of source fiat to USD at the time of order creation. |
transaction_hash | The on-chain identifier (TXID) for the crypto transfer, if available. |
metadata | A collection of custom key-value pairs passed by the partner for tracking purposes. |
After receiving a webhook, use the order_id to fetch full order details from the order lookup endpoint if you need additional data.
KYC webhooks are triggered whenever a user's identity verification state changes.
Please reach out to your Banxa contact if you would like to have this webhook enabled.
Payload Example:
{
"external_customer_id": "demomerchant-61466523855",
"account": {
"exists": true,
"blocked": false,
"createdAt": "2026-03-30T05:08:11Z"
},
"kyc": {
"status": "UNDER_REVIEW"
}
}| Field | Description |
|---|---|
external_customer_id | Your system's unique identifier for the user. |
account.exists | Boolean indicating if the user profile is successfully created in our system. |
account.blocked | Boolean indicating if the user has been banned, suspended, or hit a compliance block. |
account.createdAt | ISO 8601 timestamp of account creation. |
kyc.status | The verification outcome of the customer's submitted identity documents (selfie + document). See status table below. |
kyc.status values:
| Status | Description |
|---|---|
PENDING | No identity documents have been submitted yet. |
UNDER_REVIEW | Documents are being reviewed. |
ACTION_REQUIRED | Additional action is required from the customer. |
VERIFIED | Document and liveness verification passed. |
REJECTED | Verification was unsuccessful. |
Important:
kyc.statusreflects document and liveness verification only — it does not account for supplementary fields (e.g. purpose of transaction, occupation) or overall transaction eligibility. AVERIFIEDstatus means the identity documents passed review; it does not mean the customer is eligible to transact. Use the order creation flow to determine whether a customer can proceed.
This identity-level webhook is triggered when an order requires manual intervention or additional documentation (Enhanced Due Diligence). It allows you to monitor when a user has been flagged for specific verification steps beyond standard KYC.
Please reach out to your Banxa contact if you would like to have this webhook enabled.
Payload Example::
{
"identity_reference": "demomerchant-61466233701",
"status": "extraVerification",
"status_date": "2026-02-13 04:39:38",
"internal_reason": "Customer requires extra verification",
"external_reason": "Your order is pending additional verification. We'll notify you once it's complete"
}| Field | Description |
|---|---|
identity_reference | The unique identifier for the user (matches external_customer_id or account_reference). |
status | The verification status (typically extraVerification). |
status_date | ISO 8601 timestamp of when the additional verification was triggered. |
internal_reason | The internal system categorization for the verification request. |
external_reason | The user-facing message explaining why the transaction is pending. |
Triggers: This webhook is sent when any of the following identity-level exceptions are required:
- Enhanced Due Diligence (EDD): High-level manual review.
- Verification Phone Call (VPC): Requirement for a manual call with the user.
- Questionnaire: Scam check or suitability assessment.
- Proof of Address (POA): Request for residency documentation.
- Source of Funds (SOF): Request for documentation regarding the origin of funds.
This identity-level webhook is triggered when a customer has been restricted from creating or completing orders due to compliance or risk policies. It provides the necessary context to inform your internal teams or the end user.
Payload Example:
{
"identity_reference": "partner-customer-123",
"status": "cancelled",
"status_date": "2026-03-05 19:53:08",
"internal_reason": "Account is blocked.",
"external_reason": "Your account has been restricted from further transactions."
}| Field | Description |
|---|---|
identity_reference | A unique customer identifier provided by the partner |
status | The specific account status. Enum: cancelled. |
status_date | ISO 8601 timestamp of the status update. |
internal_reason | Internal system message detailing the specific reason for the block. |
external_reason | The designated message to be displayed to the end customer via your UI. |
The webhook retry mechanism ensures reliable delivery of event notifications. If your endpoint does not respond with 200 OK, Banxa will automatically retry delivery. The payload is unchanged across all retry attempts.
Retries follow a Fibonacci sequence: 1s, 2s, 3s, 5s, 8s, 13s, and so on. This continues for a maximum of 2 hours, with up to 18 retries over that period.
Respond with 200 OK immediately and process the event asynchronously to avoid unnecessary retries.
We recommend implementing idempotent webhook handling. In rare cases your endpoint may receive the same event more than once. Using order_id and status as a deduplication key will prevent duplicate processing.
Banxa signs every webhook it sends using HMAC-SHA256. You verify this signature to confirm the request genuinely came from Banxa.
Webhook verification is the reverse of request signing. When you sign outbound API requests, you use a Banxa API path in the canonical string. When you verify an incoming webhook, you use the URI path of your own webhook endpoint — not a Banxa API path. Everything else is the same algorithm.
Each webhook includes an Authorization header:
Authorization: Bearer {API_KEY}:{SIGNATURE}:{NONCE}Banxa constructs the signature from:
POST\nYOUR_WEBHOOK_PATH\nNONCE\nPAYLOADWhere YOUR_WEBHOOK_PATH is the path of the endpoint Banxa is calling — for example, /webhooks/banxa — not any Banxa API path.
- Extract the
Authorizationheader from the incoming request - Strip the
Bearerprefix and split on:to getreceivedKey,receivedSignature,nonce - Recompute the expected signature using the same algorithm, your secret, and
YOUR_WEBHOOK_PATH - Use a timing-safe comparison to check that
receivedSignaturematches — never use==
import hmac
MY_WEBHOOK_PATH = '/webhooks/banxa'
KEY = '[YOUR_API_KEY]'
SECRET = '[YOUR_API_SECRET]'
def verify_webhook(auth_header, request_body):
_, token = auth_header.split('Bearer ', 1)
received_key, received_signature, nonce = token.split(':')
data = f"POST\n{MY_WEBHOOK_PATH}\n{nonce}\n{request_body}"
expected = hmac.new(SECRET.encode('utf-8'), data.encode('utf-8'), 'sha256').hexdigest()
return hmac.compare_digest(received_signature, expected)