Webhook Events
Webhooks deliver real-time notifications when an event occurs in your One2Pays account. They are useful for updating your order and ledger state without polling the API.
Tip
A delivery can be retried. Verify it first, then atomically deduplicate the authenticated
X-Webhook-Id for v2. In v1 the ID is not signed, so use business identity from the signed
payload.
Setting up webhooks
Configure merchant webhook endpoints through your Merchant Dashboard:
- Open Settings → Webhooks (or Integrations → Webhooks).
- Add an HTTPS endpoint URL.
- Subscribe to all events or choose event families/tokens.
- Save the endpoint and use the Integration Secret Key for its integration when verifying deliveries.
Dashboard configuration
Merchant endpoint registration is managed in the dashboard. API provider webhook routes are for payment-provider callbacks and do not register merchant endpoints.
Event subscriptions
An endpoint may subscribe to a subset of events. * receives every event; a family token such as
payment.* receives all events in that family; an exact token such as payment.created receives
only that event.
| Subscription token | Matches |
|---|---|
* | Every event |
payment.* | Every payment.* event |
deposit.* | Every deposit.* event |
withdrawal.* | Every withdrawal.* event |
settlement.* | Every settlement.* event |
customer.* | Every customer.* event |
kyc.* | Every kyc.* event |
merchant.* | Every merchant.* event |
payment.created | Only payment.created |
Events that do not match an endpoint's subscription are skipped for that endpoint. They may still be recorded in the Webhook Deliveries view when another endpoint receives them.
Delivery headers
Every delivery includes event metadata headers and, when an Integration Secret Key is configured, signing headers:
| Version | Signature header | Timestamp header | Version header |
|---|---|---|---|
| v1 | X-BroPay-Signature | X-BroPay-Timestamp | Omitted |
| v2 | X-Webhook-Signature | X-Webhook-Timestamp | X-Webhook-Signature-Version: v2 |
Every version also includes X-Webhook-Event, X-Webhook-Id, and
Content-Type: application/json.
v1 has no version header and signs the exact raw body. v2 requires
X-Webhook-Signature-Version: v2, strictly parses I-JSON, canonicalizes the entire payload with RFC
8785/JCS, and signs a domain-separated UTF-8 message containing the timestamp, webhook ID, event
type, and canonical payload. Pin the receiver to its endpoint's expected version. See
Signature Verification for the exact preimages and
replay-window guidance. JSON object key order is not semantically meaningful; v2 canonicalization
makes key reordering safe, while v1 does not.
Payload shape
The payload is a JSON object whose event identifies the event. Payment payloads commonly include
paymentId, referenceId, status, amount, currency, and paymentMethod. Additional fields are
event- and method-dependent. Object keys may arrive in any order.
payment.received
This event indicates that the payment has reached the received/succeeded state. Signature v2
deliveries omit nextAction because payment instructions are no longer actionable after success.
The legacy v1 payload may still include nextAction. The following is a realistic v2 shape;
clientSecret and expiresAt can be absent or null according to the payment method and state:
{
"event": "payment.received",
"paymentId": "550e8400-e29b-41d4-a716-446655440000",
"referenceId": "order-12345",
"status": "succeeded",
"amount": "1000.00",
"currency": "THB",
"paymentMethod": "promptpay",
"clientSecret": null,
"expiresAt": "2024-01-01T01:00:00.000Z"
}When no client secret applies, it may be null or omitted. expiresAt, when present, is an ISO
timestamp. Do not assume that a particular key order or optional field exists for every payment
method.
| Property | Type | Notes |
|---|---|---|
event | string | Event name, such as payment.received. |
paymentId | string | One2Pays payment ID. |
referenceId | string | Merchant reference. |
status | string | Normalized payment status. |
amount | string | Decimal amount; keep it as a decimal string. |
currency | string | Currency code such as THB. |
paymentMethod | string | Payment method, for example promptpay. |
clientSecret | string or null | Optional method/state-specific client secret. |
nextAction | object or null | Legacy v1 only for payment.received; omitted from v2. |
expiresAt | string | Optional ISO timestamp. |
payment.created
Sent when payment instructions are ready. A PromptPay example:
{
"event": "payment.created",
"paymentId": "550e8400-e29b-41d4-a716-446655440000",
"referenceId": "order-12345",
"status": "processing",
"amount": "1000.00",
"currency": "THB",
"paymentMethod": "promptpay",
"clientSecret": null,
"nextAction": {
"type": "display_qr_code",
"paymentAppUrl": "https://pay.example.com/en/pay/token_123",
"qrCode": {
"payload": "000201010212...",
"promptpayId": "0123456789",
"expiresAt": "2024-01-01T01:00:00.000Z"
}
},
"expiresAt": "2024-01-01T01:00:00.000Z"
}Use nextAction.type to select the appropriate customer experience:
display_qr_code— rendernextAction.qrCode.payload.display_bank_transfer_instructions— shownextAction.bankTransferInstructions.use_payment_app— redirect tonextAction.paymentAppUrl.redirect— redirect tonextAction.redirectUrl.
Other payment events
payment.updated— payment details changed but are not terminal.payment.completed— payment completed (legacy integrations may receive this name).payment.failed— payment failed; inspecterrorCodeanderrorMessagewhen present.payment.expired— payment expired before completion.payment.refunded— payment was refunded.
Example failure payload:
{
"event": "payment.failed",
"paymentId": "550e8400-e29b-41d4-a716-446655440000",
"referenceId": "order-12345",
"status": "failed",
"amount": "1000.00",
"currency": "THB",
"paymentMethod": "promptpay",
"errorCode": "PAYMENT_FAILED",
"errorMessage": "Payment could not be completed"
}Withdrawal events
The withdrawal family includes withdrawal.created, withdrawal.processing,
withdrawal.completed, withdrawal.paid, withdrawal.failed, withdrawal.cancelled, and
withdrawal.expired. withdrawal.paid means funds reached the destination; failed and cancelled
events include a credit-back before the event is emitted where applicable.
{
"event": "withdrawal.paid",
"id": "b34028d2-345f-4c63-b3c4-dfc40d91feee",
"withdrawalId": "payout-12345",
"referenceId": "payout-12345",
"status": "paid",
"amount": "1000.00",
"currency": "THB",
"feeAmount": "15.00",
"netAmount": "985.00",
"createdAt": "2024-01-01T00:00:00.000Z",
"paidAt": "2024-01-01T00:05:00.000Z"
}Legacy wallet-withdrawal integrations may still receive withdrawal.completed with a different
shape. New integrations should subscribe to withdrawal.paid.
Deposit, settlement, customer, KYC, and merchant events
The remaining event families are:
deposit.created,deposit.completed,deposit.failed,deposit.expiredsettlement.created,settlement.completedcustomer.created,customer.updatedkyc.pending,kyc.processing,kyc.verified,kyc.failedmerchant.updatedcustom
Fields beyond the common event/resource identifiers are specific to each family. Validate the payload for the event you received and ignore fields you do not understand.
For v2, the authenticated X-Webhook-Event header and the payload's event field both use the
authoritative kyc.* names above. Existing v1 KYC callback bodies remain byte-compatible: they may
contain customer.kyc.verified or customer.kyc.failed, and historical pending/processing bodies
may contain customer.kyc.verified. Continue using your existing v1 parser and the payload's
kycStatus until the endpoint is deliberately upgraded to v2.
Handling deliveries
Verify the signature before parsing (v1) or processing (v2), enforce your replay window, and apply
idempotency atomically. Return 2xx only after durable acceptance. A 4xx response is terminal;
temporary 5xx, timeout, and network failures may be retried according to endpoint configuration.
For a handler outline, see Handle webhook example.
export async function POST(request: Request) {
// This endpoint is configured for v2. The helper verifies the signature before returning the
// authenticated delivery ID, event type, and strictly parsed payload.
const verified = await verifyWebhookRequest(request, { expectedVersion: 'v2' });
await acceptEventAtomically(verified.webhookId, verified.payload);
return new Response('OK');
}Retries and idempotency
5xx responses, timeouts, and network failures can result in another delivery attempt; 4xx
responses are not retried. The same authenticated v2 X-Webhook-Id may therefore appear more than
once. Persist it with the state change in one transaction. For v1, use signed payload business
identity instead of trusting the unsigned ID header. Do not assume event arrival order.