Why Use Webhooks
- Real-time updates: Immediate notification when payouts and orders complete
- Reliable delivery: Built-in retry mechanisms for failed deliveries
- Reduced polling: No need to constantly check payout status
- Better UX: Instant confirmation and account updates
Webhook Events
Zuba sends webhooks for the following events:Payout Events
payout.processing- Payout is being processed by the payment providerpayout.paid- Payout completed successfullypayout.failed- Payout failed before settlementpayout.reverted- A payout that had already settled (payout.paid) was later rejected by the provider’s downstream bank; an operator manually refunded the merchant and the funds were credited back. Unlikepayout.failed(a pre-settlement failure), this payout did originally succeed: do not automatically re-issue it. (Not yet delivered, pending a backend enum migration. You can register a handler now, but it will only start firing once that migration ships.)payout.cancelled- Payout was cancelled downstream while processing. Cancellations you initiate viaPOST /v1/payouts/{id}/cancelare confirmed synchronously by the cancel response and do not emit a webhook
Payout Lifecycle
Where each event fires in a payout’s life.created and queued are internal pre-processing states and fire no webhook. The first delivery you receive is normally payout.processing, but a payout that fails validation or funding before processing (for example, insufficient balance) emits payout.failed as its first and only event:
The decline reason is not included in the
payout.failed webhook payload.
When you receive one, fetch GET /v1/payouts/{id} and read the
failureReason field.Order Events
Orders (POST /v1/orders) are executed manually by Zuba’s trade desk; webhooks are how you learn the outcome. See Trade Desk Orders for the full lifecycle.
order.created- The order was created. Fires together withorder.funds_received(balance-funded) ororder.awaiting_funds(settlement-funded)order.awaiting_funds- Funding instructions were issued; the payload’ssettlementblock carries the account or address, amount, and reference to fund the orderorder.funds_received- The source amount is held and the order is queued for desk executionorder.completed- The desk delivered to your beneficiary;uetris present for wire deliveriesorder.failed- The order failed (failureReasonset) and the held funds were returned to your balanceorder.expired- The funding window closed before the order was funded; a transfer arriving later credits the balance instead
order.cancelled event (cancellation is answered synchronously by POST /v1/orders/{id}/cancel) and no event for the executing status (desk review is internal).
Account Events
account.created- An account record was created viaPOST /v1/accounts
Application Events
Your onboarding application moves through review out of band, so these events report where it stands.application.under_review- The application was submitted and is being reviewedapplication.more_info_requested- Review needs more from you; the payload addsmoreInfoReasonand theeditableSectionsto completeapplication.approved- The application was approvedapplication.rejected- The application was rejected
Virtual Account Events
Most currencies issue virtual accounts asynchronously:POST /v1/virtual-accounts returns status: "pending" and the account completes out of band, which can take far longer than a request cycle. These events are the completion signal, so use them rather than a polling loop. A delivery can be retried but is never replayed on demand, so if you have been waiting longer than you expect, repeat the POST: it is idempotent per owner and currency, and answers with the pending account, the active one, or the refusal that stands. GET /v1/virtual-accounts lists active accounts only, so a pending account and a refused one are both simply absent from it.
A refusal answers on the POST itself when the provider can settle it inside the request: 409 VIRTUAL_ACCOUNT_VERIFICATION_REQUIRED means the provider requires verification material for your business or its owner beyond the application’s fields. Complete the outstanding onboarding verification and repeat the POST — the request is refused with the same code until the verification passes. A repeat within ten minutes of the last refusal is answered from that refusal without consulting the provider, so allow that interval before retrying. 409 VIRTUAL_ACCOUNT_DEACTIVATED is different: the account was deactivated, and only support can re-issue it.
virtual_account.active- The account was approved and now carries its issued details; fetch them withGET /v1/virtual-accountsvirtual_account.failed- The account was refused after issuance, or can never produce usable payment details. A repeatPOST /v1/virtual-accountsfor the same currency answers with the refusal that stands (409 VIRTUAL_ACCOUNT_VERIFICATION_REQUIREDfor a provider verification refusal,409 VIRTUAL_ACCOUNT_DEACTIVATEDfor a deactivation only support can lift)
XOF/XAF/GHSaccounts are approved after creation, so a refusal lands on an account you already hold aspending— these are the currenciesvirtual_account.failedfires for.EUR/GBPaccounts are approved before creation: a refusal surfaces as an error onPOST /v1/virtual-accountsitself. Expectvirtual_account.activeonly; these two currencies emit novirtual_account.failed, so an account that stayspendingfar longer than usual needs support rather than a retry.USDaccounts are verified after creation, and part of the verification can also settle inside the request: aPOSTthe provider refuses outright answers409 VIRTUAL_ACCOUNT_VERIFICATION_REQUIREDwith nothing created — complete the outstanding verification and repeat the request. Once thePOSTanswers201withstatus: "pending", the remaining verification can run for days:virtual_account.activecarries the issued wire details, andvirtual_account.failedmeans the verification refused the account — a repeat request then answers409 VIRTUAL_ACCOUNT_VERIFICATION_REQUIREDwhile the refusal stands; where the refusal names verification material you can supply, completing it and repeating the request is the recovery, otherwise contact support.
accountId is not null the account belongs to that sub-account, and both follow-up calls are owner-scoped: send Zuba-Account-Id: <accountId> on the GET or the repeat POST, or the request addresses your master account’s accounts instead.
The payload identifies the account but never carries bank details — always read those from the authenticated GET /v1/virtual-accounts.
NGN accounts emit no event. In production nobody issues them through this endpoint — POST /v1/virtual-accounts answers 422 VIRTUAL_ACCOUNT_SELF_SERVE_UNSUPPORTED for NGN. NG-incorporated workspaces have one provisioned during onboarding review and read it from GET /v1/virtual-accounts once the application is approved; anyone else needing NGN collection should contact support. In the sandbox, POST /v1/virtual-accounts returns the account number directly for your master account.
Other Events
webhook.test- Test event sent via the test endpoint
Event Payload
Every webhook delivery is a JSON POST request with this envelope:Event-specific data fields
payout.* events
order.* events
account.created
application.* events
virtual_account.* events
Each request includes these headers:
Setting Up Webhook Endpoints
Webhook endpoint URLs must use HTTPS: registering anhttp:// URL is rejected with a 400.
Basic Webhook Handler
Create an endpoint to receive webhook notifications:Processing Different Event Types
Handle specific webhook events:Webhook Security
Verifying Signatures
Every webhook delivery includes a cryptographic signature so you can verify the request genuinely originated from Zuba and has not been tampered with. You should always verify signatures before processing webhook events. The signature is computed as:signing_secretis the plaintext secret (thewhsec_-prefixed value returned when you created the endpoint)timestampis the value from theX-Zuba-Timestampheaderraw_bodyis the raw HTTP request body (not parsed JSON)- The result is hex-encoded (64 characters)
Verification Steps
- Extract the
X-Zuba-SignatureandX-Zuba-Timestampheaders - Concatenate the timestamp, a literal
., and the raw request body - Compute the HMAC-SHA256 of that string using your signing secret
- Compare your computed signature against the header value using a constant-time comparison
- Check that the timestamp is recent (within your tolerance window) to prevent replay attacks
Code Examples
Replay Protection
The timestamp is included in the signed content specifically to enable replay protection. An attacker who intercepts a valid webhook cannot replay it at a later time if you enforce a timestamp tolerance. We recommend rejecting any webhook where theX-Zuba-Timestamp is more than 5 minutes from your server’s current time. All the examples above implement this check.
Make sure your server’s clock is synchronized with NTP. A clock that drifts
significantly could cause valid webhooks to be rejected.
IP Allowlisting
Zuba does not currently publish a static egress IP range for webhook deliveries. Signature verification (above) is the supported way to authenticate incoming webhooks: it proves both origin and integrity, which an IP check alone does not.Handling Webhook Failures
Idempotency
Ensure webhooks can be safely retried:Retry Logic
Implement exponential backoff for webhook processing:Delivery and Retries
Zuba retries failed deliveries up to 5 times (6 attempts in total) with exponential backoff. A delivery is successful when your endpoint responds with a 2xx status code within 30 seconds. Redirects are not followed: a 3xx response counts as a failed attempt.
Delays include random jitter and can be up to twice these values. Treat them as lower bounds.
After all attempts are exhausted, the delivery is marked as failed. You can view delivery history via the API:
GET /v1/webhooks/{endpoint_id}/deliveries/{delivery_id}.
Testing Webhooks
Sending a Test Event
Trigger awebhook.test delivery to a registered endpoint:
"test": true in the envelope and is signed like any other event, so you can exercise your signature verification and processing end to end.
Local Testing with ngrok
Expose your local development server with ngrok:Complete Example
Next Steps
- Learn about error handling strategies for webhook failures
- Explore the API reference for complete event schemas
- Set up monitoring and alerting for production webhook endpoints
- Implement webhook event replay for failed processing scenarios