One2Pays

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:

  1. Open SettingsWebhooks (or IntegrationsWebhooks).
  2. Add an HTTPS endpoint URL.
  3. Subscribe to all events or choose event families/tokens.
  4. 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 tokenMatches
*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.createdOnly 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:

VersionSignature headerTimestamp headerVersion header
v1X-BroPay-SignatureX-BroPay-TimestampOmitted
v2X-Webhook-SignatureX-Webhook-TimestampX-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.

PropertyTypeNotes
eventstringEvent name, such as payment.received.
paymentIdstringOne2Pays payment ID.
referenceIdstringMerchant reference.
statusstringNormalized payment status.
amountstringDecimal amount; keep it as a decimal string.
currencystringCurrency code such as THB.
paymentMethodstringPayment method, for example promptpay.
clientSecretstring or nullOptional method/state-specific client secret.
nextActionobject or nullLegacy v1 only for payment.received; omitted from v2.
expiresAtstringOptional 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 — render nextAction.qrCode.payload.
  • display_bank_transfer_instructions — show nextAction.bankTransferInstructions.
  • use_payment_app — redirect to nextAction.paymentAppUrl.
  • redirect — redirect to nextAction.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; inspect errorCode and errorMessage when 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.expired
  • settlement.created, settlement.completed
  • customer.created, customer.updated
  • kyc.pending, kyc.processing, kyc.verified, kyc.failed
  • merchant.updated
  • custom

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.

On this page