Compose Finance

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

  1. Go to Settings > API in your Compose dashboard
  2. Click Enable Webhooks
  3. You will be redirected to the webhook management portal
  4. Add your endpoint URL (e.g., https://yourapp.com/webhooks/compose)
  5. 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:

  1. Go to your endpoint in the webhook portal
  2. Click the Testing tab
  3. Select an event type and send a test message
  4. 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

EventDescription
Customer Events
customer.createdNew customer record created
customer.updatedCustomer information changed (name, email, expected_monthly_volume)
customer.enabledCustomer account enabled
customer.disabledCustomer account disabled
KYC Events
customer.kyc.submittedCustomer submitted documents for KYC review
customer.kyc.approvedCustomer passed KYC verification
customer.kyc.rejectedCustomer failed KYC verification
customer.kyc.level_changedCustomer moved to a different KYC level (e.g., basic to enhanced)
Deposit Events
deposit.createdNew deposit detected
deposit.status_changedDeposit status updated (PROCESSING, COMPLETED, FAILED, CANCELLED, EXPIRED)
Withdrawal Events
withdrawal.createdWithdrawal request created
withdrawal.status_changedWithdrawal status updated (PROCESSING, PROPOSED, PARTIALLY_SIGNED, COMPLETED, FAILED, CANCELLED, EXPIRED)
Withdrawal Bank Events
withdrawal_bank.createdNew withdrawal bank account added
withdrawal_bank.approvedWithdrawal bank account approved
withdrawal_bank.rejectedWithdrawal bank account rejected
Virtual Account Events
virtual_account.createdVirtual account creation initiated
virtual_account.approvedVirtual account approved (IBAN assigned)
virtual_account.rejectedVirtual 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 identifier
  • svix-timestamp - Timestamp when the message was sent
  • svix-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:

AttemptDelay after previous
1Immediate
25 seconds
35 minutes
430 minutes
52 hours
65 hours
710 hours
810 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

On this page