BlockBee notifies your endpoint when a payout reaches a terminal state: confirmed (status=done) or failed (status=error). Use it to reconcile withdrawals: mark a payout complete in your system, or surface the failure reason when one occurs.
You configure the destination URL and method per profile in your BlockBee dashboard payout settings. If no URL is set, BlockBee sends no payout webhook.
Method: You choose in your payout settings:
POST(default) - Payload sent in the request body asapplication/x-www-form-urlencoded.GET- Payload sent as URL query parameters.
Content-Type: The payout webhook is always delivered as form data: application/x-www-form-urlencoded for POST, query-string parameters for GET. There is no JSON option.
How It Works
When a payout settles or fails, BlockBee sends one signed HTTP request to your configured URL and waits for an acknowledgement. Respond with HTTP 200 and you're done. Respond with anything else, or time out, and BlockBee retries with exponential backoff.
BlockBee sends exactly one notification per state transition:
done- The payout was confirmed on-chain.error- The payout failed. Theerrorfield carries the reason.
Expect both states. A payout that fails still produces a webhook, with status=error and error populated. Requests arrive with the User-Agent BlockBee/1.0 (+https://docs.blockbee.io/webhooks).
The payout webhook does not include the on-chain txid. Find the transaction hash on the payout itself in your dashboard.
Configuring the Webhook
Payout webhook settings are configured per profile from your dashboard. You never call an API to set them up.
- In your BlockBee dashboard, select the profile you want to configure and open Profile Settings → Payout settings.
- Enter your Endpoint URL, a public HTTPS URL. Internal and loopback addresses are rejected, including on redirects.
- Pick the Method:
POST(default) orGET. - Click Save.
The page prefills with the URL and method you last saved. To turn the webhook off, clear the Endpoint URL and save again. The dashboard asks you to confirm first, since this stops all payout notifications for the profile.
After saving, use the Send test button to validate your endpoint before a real payout depends on it (see Testing your endpoint). Each payout's delivery history is available from its Webhook activity view (see Delivery logs).
Webhook Fields
All values are delivered as strings.
idstring•Required
Payout UUID. Stable across retries and across the done/error states.
Important: Use the id together with status for idempotency. The same payout/status can arrive more than once if a retry fires after you already processed it.
afe11bea-768b-47ae-ba0f-907379fbe5efstatusstring•Required
Machine-readable status. done when the payout is confirmed, error when it failed.
doneExample Payload
{
"id": "afe11bea-768b-47ae-ba0f-907379fbe5ef",
"status": "done",
"display_status": "Done",
"total_requested": "0.5",
"total_requested_fiat": "32150.00",
"total_with_fee": "0.5005",
"total_with_fee_fiat": "32182.15",
"error": "",
"blockchain_fee": "0.0005",
"fee": "0",
"coin": "btc",
"timestamp": "08/06/2026 14:22:01"
}Security: Verify Webhook Signatures
Security Warning! Always verify incoming webhook signatures. For a complete guide, see our Verify Webhook Signatures Guide.
Acknowledging and Retries
Respond with HTTP 200 to acknowledge the webhook. The payout webhook keys off the status code only; the response body is not inspected. Keep your response small and return it quickly, since slow or oversized responses are treated as failures.
Any non-200 status, timeout, or connection error is treated as a failure, and BlockBee retries with exponential backoff of 6 × 2^(attempt-1) minutes:
6m → 12m → 24m → 48m → 96m → 192m → 384m → 768m → 1536m → 3072mAfter 11 attempts (~3–4 days), BlockBee stops retrying.
Implementation Examples
The payout webhook delivers form-encoded fields, so read them from the parsed form (POST) or query string (GET), not from a JSON body. Signature verification is delegated to a verifyWebhookSignature helper; see the Verify Webhook Signatures Guide.
// Express.js webhook handler
// Accept both POST (form body) and GET (query string).
app.all('/payout-webhook', express.urlencoded({ extended: true }), (req, res) => {
// 1. Verify the webhook signature
if (!verifyWebhookSignature(req)) {
return res.status(403).send('Invalid signature');
}
// 2. Extract the fields
const fields = req.method === 'POST' ? req.body : req.query;
const { id, status, error, total_requested, coin } = fields;
// 3. Ignore test sends (all-zero sentinel id)
if (id === '00000000-0000-0000-0000-000000000000') {
return res.status(200).send('ok');
}
// 4. Idempotency: de-dupe on (id, status)
if (isPayoutUpdateProcessed(id, status)) {
return res.status(200).send('ok');
}
// 5. Handle both terminal states
if (status === 'done') {
markPayoutComplete({ id, amount: total_requested, coin });
} else if (status === 'error') {
markPayoutFailed({ id, reason: error });
}
// 6. Record that this (id, status) was processed
markPayoutUpdateProcessed(id, status);
// MUST return 200 to acknowledge
res.status(200).send('ok');
});Key Checks
- Verify the signature - Confirm
x-ca-signatureagainst the raw bytes before acting. - Recognize test sends - Treat the all-zero
idas a no-op (see Testing your endpoint). - De-dupe on (
id,status) - The same terminal update can arrive more than once. - Handle both states - Process
doneanderror; the failure reason is inerror.
Testing Your Endpoint
Once a webhook URL is saved, a Send test button appears on the payout settings page. It fires a single, synchronous webhook at your configured URL so you can validate your endpoint, including signature verification, before any real payout depends on it. The panel previews the exact payload it will send and reports the result inline.
The test request is identical to a real one: same method (GET/POST), same x-ca-signature signing, same payload shape, with two differences:
- It is a one-off probe. It creates no payout record, writes no delivery log, and is never retried.
- The payload carries a sentinel
idof all zeros so you can recognize a test and avoid acting on it.
{
"id": "00000000-0000-0000-0000-000000000000",
"status": "done",
"display_status": "Done",
"total_requested": "1",
"total_requested_fiat": "50.00",
"total_with_fee": "1.001",
"total_with_fee_fiat": "50.05",
"error": "",
"blockchain_fee": "0.001",
"fee": "0",
"coin": "btc",
"timestamp": "08/06/2026 14:22:01"
}The button reports the result back: HTTP 200 means your endpoint accepted the test; any other value (including a transport failure such as a timeout or blocked connection) means it didn't. Tests are rate-limited to one per minute and protected by a CAPTCHA.
Recommended: Have your handler treat the all-zero id as a no-op: verify the signature, return 200, but don't create or modify any payout record. Test sends then never touch real data.
Delivery Logs
BlockBee records every delivery attempt for each payout. In your dashboard, open any payout and select Webhook activity (next to Payout information) to review its delivery history: the URL each attempt was sent to, the response received, the resulting status, and when it was sent. Use it to confirm a payout webhook was delivered, or to debug one that wasn't.
Reliability and Best Practices
- Verify signatures. Always check
x-ca-signatureagainst the raw request before trusting the payload. - Be idempotent. De-dupe on (
id,status); a retry can fire after you already processed the update but the200didn't reach BlockBee. - Return 200 fast. Do heavy processing asynchronously. The connection has a short timeout, and over-large or slow responses count as failures.
- Expect both states. You receive a webhook for the terminal
errorstate too, witherrorpopulated. - Use a public, SSRF-safe URL. Internal and loopback addresses are rejected, including on redirects.
For BlockBee's general webhook delivery, retry, and timeout behavior, see the How Webhooks Overview.