Unified Envelope Format
All v2 webhooks share the same outer structure:
{
"event": "<event_name>",
"timestamp": "2026-03-22T10:00:00+07:00",
"data": { }
}
| Field | Type | Description |
|---|---|---|
| event | string | The event name that triggered this webhook |
| timestamp | string | Webhook dispatch timestamp (ISO 8601) |
| data | object | Event-specific payload (see individual event docs) |
Webhook Registration
Webhook endpoints are configured during client onboarding or can be set up later via the admin panel. Each event type requires a separate webhook configuration with:
- Base URL (e.g., https://yourdomain.com)
- Path (e.g., /api/webhooks/payment)
The complete webhook URL is built as: {base_url}{path}
V2 event names follow a structured naming convention: payment.v2.completed, disbursement.v2.completed, etc. Register each event independently — you can subscribe to v1, v2, or both simultaneously.
Response Requirements
Your webhook endpoint must return a standard HTTP status code:
- 200-299: Success (webhook handled successfully)
- 4xx: Client error (retries will not be attempted, except 408 and 429 — see Error Handling)
- 5xx: Server error (retries will be attempted)
Keep response time under 5 seconds to prevent timeout.
Idempotency
Request Headers
All v2 webhook requests include the same authentication headers as v1:
| Header | Description |
|---|---|
| X-PARTNER-ID | Your client identifier |
| X-TIMESTAMP | Request timestamp in ISO8601 format (e.g., 2024-12-18T16:21:07+07:00) |
| X-SIGNATURE | RSA signature for request verification (see Signature Generation) |
Security Best Practices
- Always validate the X-SIGNATURE header to authenticate requests
- Confirm the X-PARTNER-ID matches your client ID
- Validate the X-TIMESTAMP to prevent replay attacks
- Use HTTPS for your webhook endpoint
- Implement idempotency checks using identifiers inside
datato prevent duplicate processing
V2 Events
| Event | Description |
|---|---|
payment.v2.completed |
Payment is successfully completed |
disbursement.v2.completed |
Disbursement is successfully completed |
disbursement.v2.fail |
Disbursement fails |
disbursement.v2.expired |
Disbursement expires |
transaction_settlement.settled |
A transaction settlement is finalised and funds are transferred |
Error Handling
If your endpoint returns a non-2xx status code or times out:
- 5xx responses: retried (up to 3 attempts total, including the first, with 10-second and 30-second delays between retries)
- 408 (Request Timeout) and 429 (Too Many Requests): also retried, on the same schedule as 5xx
- Other 4xx responses: No retry will be attempted — review your endpoint logic
- Timeout (> 5 seconds): Treated as a failure and retried. The 5-second timeout applies per attempt — each retry gets a fresh window, not a cumulative budget across all 3 attempts.
- Connection errors (connection refused, DNS failure, SSL errors, unreachable host): also treated as a failure and retried, same as the cases above.
Ensure your webhook handler processes quickly and offloads heavy work asynchronously.
V1 to V2 Migration Guide
The key structural difference between v1 and v2 is the unified envelope:
- V1 payment — fields are at the root level of the payload (flat structure)
- V1 disbursement — uses an
{ event, data }envelope (notimestamp) - V2 all events — always use
{ event, timestamp, data }— consistent across all event types
V2 keeps every v1 field, with the same name and type (amount is a string in both). The differences:
| V1 event | V2 event | Fields added in V2 |
|---|---|---|
payment.completed |
payment.v2.completed |
payment_channel, payment_at, service_fee_rate_str, service_fee_amount_str, nett_amount_str |
disbursement.completed |
disbursement.v2.completed (success), disbursement.v2.fail (fail), disbursement.v2.expired (expired) |
valid_account_name, reason, payment_at |
V2 may add more fields later; each V2 event page lists its current fields. V2 also has transaction_settlement.settled, with no v1 equivalent.
To migrate:
- Register a new webhook for the corresponding v2 event name (e.g.,
payment.v2.completed) - Update your handler to read fields from
data.*instead of the root level (for payment events) - Add handling for the new
timestampenvelope field - Verify the updated
eventstring matches the v2 name - Once confirmed working, you may unregister the v1 webhook