Webhooks
Use webhooks to receive real-time updates for customer, deposit, withdrawal, and KYC events.
Webhooks are how we notify your service of events. At their core, they are POST requests to an endpoint you specify. Instead of polling the API, your server receives real-time notifications when customers are created, KYC status changes, transactions complete, and more.
Your endpoint should return a 2xx response within 15 seconds to indicate successful receipt. It's also important to disable CSRF protection for this endpoint if your framework enables it by default.
Setup
- Go to Settings > API in your Compose dashboard
- Click Enable Webhooks
- You will be redirected to the webhook management portal
- Add your endpoint URL (e.g.,
https://yourapp.com/webhooks/compose) - Select which event types to receive (or receive all by default)
Adding an Endpoint
When adding an endpoint, provide a URL that you control. If you don't specify any event types, your endpoint will receive all events by default. We recommend filtering to only the events you need to avoid processing unnecessary messages.
If your endpoint isn't ready yet, you can use Svix Play to generate a temporary test URL and inspect incoming webhooks.
Testing Your Endpoint
Once you've added an endpoint, test it by sending sample events from the webhook dashboard:
- Go to your endpoint in the webhook portal
- Click the Testing tab
- Select an event type and send a test message
- View the message payload, delivery attempts, and whether it succeeded or failed
You can also use Svix Play to generate a temporary URL for testing without setting up your own server.
Event Types
| Event | Description |
|---|---|
| Customer Events | |
customer.created | New customer record created |
customer.updated | Customer information changed (name, email, expected_monthly_volume) |
customer.enabled | Customer account enabled |
customer.disabled | Customer account disabled |
| KYC Events | |
customer.kyc.submitted | Customer submitted documents for KYC review |
customer.kyc.approved | Customer passed KYC verification |
customer.kyc.rejected | Customer failed KYC verification |
customer.kyc.level_changed | Customer moved to a different KYC level (e.g., basic to enhanced) |
| Deposit Events | |
deposit.created | New deposit detected |
deposit.status_changed | Deposit status updated (PROCESSING, COMPLETED, FAILED, CANCELLED, EXPIRED) |
| Withdrawal Events | |
withdrawal.created | Withdrawal request created |
withdrawal.status_changed | Withdrawal status updated (PROCESSING, PROPOSED, PARTIALLY_SIGNED, COMPLETED, FAILED, CANCELLED, EXPIRED) |
| Withdrawal Bank Events | |
withdrawal_bank.created | New withdrawal bank account added |
withdrawal_bank.approved | Withdrawal bank account approved |
withdrawal_bank.rejected | Withdrawal bank account rejected |
| Virtual Account Events | |
virtual_account.created | Virtual account creation initiated |
virtual_account.approved | Virtual account approved (IBAN assigned) |
virtual_account.rejected | Virtual account request rejected |
Payload Structure
All webhooks follow this format:
{
"event_id": "evt_lz4k8m_a1b2c3d4",
"event_type": "customer.created",
"created_at": "2026-01-27T16:20:44.751Z",
"api_version": "v2",
"org_id": "96884f9b-6ec3-4c1b-8efa-a3ffbda8b960",
"data": {
"customer_id": "550e8400-e29b-41d4-a716-446655440001"
}
}Event Payloads
Customer Events (customer.created, customer.enabled, customer.disabled, customer.kyc.approved, customer.kyc.rejected):
{ "customer_id": "550e8400-e29b-41d4-a716-446655440001" }Customer Updated (customer.updated):
{ "customer_id": "550e8400-e29b-41d4-a716-446655440001", "changed_fields": ["name", "email"] }Possible changed_fields: name, email, expected_monthly_volume
KYC Submitted (customer.kyc.submitted):
{ "customer_id": "550e8400-e29b-41d4-a716-446655440001", "attempt": 1 }KYC Level Changed (customer.kyc.level_changed):
{ "customer_id": "550e8400-e29b-41d4-a716-446655440001", "previous_level": "customers-api-basic", "new_level": "customers-api-enhanced" }Note: previous_level is null for first-time level assignment.
Deposit Events (deposit.created, deposit.status_changed):
{ "transaction_id": "550e8400-e29b-41d4-a716-446655440001", "customer_id": "550e8400-e29b-41d4-a716-446655440002", "status": "COMPLETED" }Possible statuses: PROCESSING, COMPLETED, FAILED, CANCELLED, EXPIRED
Withdrawal Events (withdrawal.created, withdrawal.status_changed):
{ "transaction_id": "550e8400-e29b-41d4-a716-446655440001", "customer_id": "550e8400-e29b-41d4-a716-446655440002", "status": "PROPOSED" }Possible statuses: PROCESSING, PROPOSED, PARTIALLY_SIGNED, COMPLETED, FAILED, CANCELLED, EXPIRED
Withdrawal Bank Events (withdrawal_bank.created, withdrawal_bank.approved, withdrawal_bank.rejected):
{ "withdrawal_bank_id": "550e8400-e29b-41d4-a716-446655440001", "customer_id": "550e8400-e29b-41d4-a716-446655440002", "status": "ACTIVE" }Possible statuses: PENDING, ACTIVE, REJECTED
Virtual Account Events (virtual_account.created, virtual_account.approved, virtual_account.rejected):
{ "correlation_id": "corr-12345-abcde", "customer_id": "550e8400-e29b-41d4-a716-446655440001", "status": "APPROVED" }Possible statuses: PENDING, APPROVED, REJECTED
Verifying Webhook Signatures
Webhook signatures let you verify that messages are actually sent by us and not a malicious actor. All webhooks include three headers for verification:
svix-id- Unique message identifiersvix-timestamp- Timestamp when the message was sentsvix-signature- Signature to verify authenticity
JavaScript Example:
import { Webhook } from "svix";
const secret = "whsec_your_secret_here"; // From webhook dashboard
const wh = new Webhook(secret);
// Verify the payload (throws on failure)
const payload = wh.verify(rawBody, headers);Python Example:
from svix.webhooks import Webhook
secret = "whsec_your_secret_here" # From webhook dashboard
wh = Webhook(secret)
# Verify the payload (raises exception on failure)
payload = wh.verify(raw_body, headers)For more languages and details, see the Svix verification documentation.
Retry Schedule
Failed deliveries are retried with exponential backoff:
| Attempt | Delay after previous |
|---|---|
| 1 | Immediate |
| 2 | 5 seconds |
| 3 | 5 minutes |
| 4 | 30 minutes |
| 5 | 2 hours |
| 6 | 5 hours |
| 7 | 10 hours |
| 8 | 10 hours |
For example, a message that fails three times before succeeding will be delivered roughly 35 minutes and 5 seconds after the first attempt.
Your endpoint must return a 2xx status code (200-299) within 15 seconds. Any other response code, including 3xx redirects, is treated as a failure.
Idempotency
Use event_id to deduplicate webhook deliveries. Store processed event IDs and skip any duplicates. Svix may retry deliveries even after your endpoint returns success (e.g., due to network issues), so idempotent handling is essential.
Event Filtering
You can filter which events your endpoint receives via the webhook configuration. This allows you to subscribe only to the events relevant to your integration, reducing unnecessary processing.
Troubleshooting
Common reasons why webhook endpoints fail:
Not using the raw payload body When verifying signatures, use the raw string body exactly as received. If you parse and re-stringify the JSON, the signature won't match.
Using the wrong secret key Secrets are unique to each endpoint. Make sure you're using the correct secret from the webhook dashboard.
Returning wrong response codes
A 2xx response indicates success. Even if you include an error in the response body, we'll treat it as successful if the status code is 2xx.
Response timeouts If your endpoint takes longer than 15 seconds to respond, the delivery is marked as failed. Process webhooks asynchronously - receive the message, add it to a queue, and respond immediately.
Failure Recovery
Re-enabling a disabled endpoint If all attempts to an endpoint fail for 5 days, it will be disabled. Re-enable it from the webhook dashboard by finding the endpoint and clicking "Enable Endpoint".
Replaying failed messages If your service had downtime, you can recover missed messages:
- To replay a single message: Find it in the dashboard and click "Resend"
- To replay all failed messages: Go to your endpoint, click "Options > Recover Failed Messages", and select the time range