Logo
API ReferenceWebhooks

Webhook Events

Webhooks allow you to receive real-time notifications when events happen in your One2Pays account. This is especially useful for updating your database when payments are completed or failed.

Tip

Webhooks are the most reliable way to track payment status changes. Don't rely solely on redirect URLs, as users might close their browser before being redirected.

Setting Up Webhooks

Webhook endpoints are configured through your Merchant Dashboard:

  1. Go to your One2Pays Dashboard
  2. Navigate to SettingsWebhooks (or IntegrationsWebhooks)
  3. Configure your webhook endpoint URL (must be HTTPS)
  4. Select the events you want to receive
  5. Save your webhook configuration

Note

Webhook configuration is managed through the dashboard, not via API endpoints. The API provides provider webhook endpoints for payment providers, not merchant webhook registration.

Webhook Structure

All webhook events have the following structure:

{
  "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"
}

Event Object Properties

PropertyTypeDescription
eventstringEvent name, such as payment.created or payment.received
paymentIdstringBroPay payment ID
referenceIdstringYour reference ID
statusstringNormalized webhook status
amountstringPayment amount as decimal string
currencystringCurrency code
paymentMethodstringPayment method, such as promptpay or bank_transfer
clientSecretstringClient secret when applicable, otherwise null
nextActionobjectPayment instructions for the merchant or customer, otherwise null
expiresAtstringISO timestamp when the payment expires, if available
completedAtstringISO timestamp when the payment completed or was canceled
errorCodestringError code for failed events, if available
errorMessagestringError message for failed events, if available

Delivery Headers

The HTTP request also includes delivery metadata in headers such as X-Webhook-Event, X-Webhook-Id, X-Webhook-Signature, and X-Webhook-Timestamp.

Event Types

Payment Events

payment.created

Sent when payment instructions are ready. Always includes nextAction with the full payment instructions for the merchant to embed or display.

PromptPay (QR code):

{
  "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"
}

Bank Transfer:

{
  "event": "payment.created",
  "paymentId": "550e8400-e29b-41d4-a716-446655440000",
  "referenceId": "order-12345",
  "status": "processing",
  "amount": "1000.00",
  "currency": "THB",
  "paymentMethod": "bank_transfer",
  "clientSecret": null,
  "nextAction": {
    "type": "display_bank_transfer_instructions",
    "paymentAppUrl": "https://pay.example.com/en/pay/token_123",
    "bankTransferInstructions": {
      "bankCode": "kbank",
      "accountNumber": "1234567890",
      "accountName": "Example Company Ltd.",
      "amount": "1000.00",
      "referenceNumber": "ORDER-12345"
    }
  },
  "expiresAt": "2024-01-01T01:00:00.000Z"
}

Use nextAction.type to determine which payment instructions to display:

  • display_qr_code — render the PromptPay QR code from nextAction.qrCode.payload
  • display_bank_transfer_instructions — show bank account details from nextAction.bankTransferInstructions
  • use_payment_app — redirect or embed nextAction.paymentAppUrl (hosted payment page)
  • redirect — redirect the customer to nextAction.redirectUrl

payment.received

Sent when a payment is successfully completed.

{
  "event": "payment.received",
  "paymentId": "550e8400-e29b-41d4-a716-446655440000",
  "referenceId": "order-12345",
  "status": "succeeded",
  "amount": "1000.00",
  "currency": "THB",
  "paymentMethod": "promptpay",
  "completedAt": "2024-01-01T00:05:00.000Z"
}

payment.failed

Sent when a payment fails.

{
  "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"
}

payment.updated

Sent when a payment is updated but not yet completed.

payment.expired

Sent when a payment expires before completion.

payment.refunded

Sent when a payment is refunded.

Withdrawal Events

withdrawal.completed

Sent when a withdrawal is successfully completed.

{
  "event": "withdrawal.completed",
  "withdrawalId": "660e8400-e29b-41d4-a716-446655440001",
  "status": "completed",
  "amount": "1000.00",
  "currency": "THB",
  "referenceNumber": "payout-12345",
  "completedAt": "2024-01-01T00:05:00.000Z"
}

withdrawal.failed

Sent when a withdrawal fails.

withdrawal.expired

Sent when a withdrawal expires.

withdrawal.created

Sent when a withdrawal is created.

Handling Webhooks

Basic Webhook Handler

import crypto from 'crypto';

const WEBHOOK_SECRET = process.env.PAYMENT_WEBHOOK_SECRET!;

// Verify webhook signature
function verifySignature(
  payload: string,
  signature: string,
  timestamp: string,
  secret: string
): boolean {
  // Extract signature hash (format: sha256=<hash>)
  const signatureHash = signature.replace(/^sha256=/, '');

  // Parse timestamp (in milliseconds)
  const timestampMs = parseInt(timestamp, 10);
  if (isNaN(timestampMs) || timestampMs <= 0) {
    return false;
  }

  // Create signed payload: timestamp + "." + payload
  const signedPayload = `${timestampMs}.${payload}`;

  // Compute expected signature
  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(signedPayload)
    .digest('hex');

  // Compare signatures using timing-safe comparison
  return crypto.timingSafeEqual(
    Buffer.from(signatureHash, 'hex'),
    Buffer.from(expectedSignature, 'hex')
  );
}

export async function POST(request: Request) {
  const signature = request.headers.get('x-webhook-signature');
  const timestamp = request.headers.get('x-webhook-timestamp');
  const body = await request.text();

  // Verify signature
  if (!signature || !timestamp || !verifySignature(body, signature, timestamp, WEBHOOK_SECRET)) {
    return new Response('Invalid signature', { status: 401 });
  }

  const event = JSON.parse(body);

  // Handle the event
  switch (event.event) {
    case 'payment.created':
      await handlePaymentCreated(event);
      break;
    case 'payment.received':
      await handlePaymentReceived(event);
      break;
    case 'payment.failed':
      await handlePaymentFailed(event);
      break;
    case 'withdrawal.completed':
      await handleWithdrawalCompleted(event);
      break;
    case 'withdrawal.failed':
      await handleWithdrawalFailed(event);
      break;
    default:
      console.log(`Unhandled event type: ${event.event}`);
  }

  return new Response('OK', { status: 200 });
}

async function handlePaymentCreated(payment: any) {
  console.log('Payment created:', payment.paymentId);

  if (payment.nextAction?.type === 'display_bank_transfer_instructions') {
    // Render QR or bank transfer instructions in your own UI
    await savePaymentInstructions(payment.referenceId, payment.nextAction);
  }

  if (payment.nextAction?.type === 'use_payment_app') {
    await saveHostedPaymentUrl(payment.referenceId, payment.nextAction.paymentAppUrl);
  }
}

async function handlePaymentReceived(payment: any) {
  console.log('Payment received:', payment.paymentId);

  // Update your database
  await updateOrderStatus(payment.referenceId, 'paid');
}

async function handlePaymentFailed(payment: any) {
  console.log('Payment failed:', payment.paymentId);

  // Update your database
  await updateOrderStatus(payment.referenceId, 'failed');
}

async function handleWithdrawalCompleted(withdrawal: any) {
  console.log('Withdrawal completed:', withdrawal.withdrawalId);
  // Update your system
}

async function handleWithdrawalFailed(withdrawal: any) {
  console.log('Withdrawal failed:', withdrawal.withdrawalId);
  // Handle failure
}

Best Practices

Important

Always verify webhook signatures to ensure the request is from One2Pays and hasn't been tampered with.

1. Verify Signatures

Always verify the webhook signature before processing the event. See Signature Verification for details.

2. Handle Idempotency

Webhooks may be delivered more than once. Use the delivery ID from the X-Webhook-Id header to ensure you only process each delivery once.

const processedEvents = new Set();

export async function POST(request: Request) {
  // ... signature verification ...

  const webhookId = request.headers.get('x-webhook-id');
  const event = JSON.parse(body);

  if (!webhookId) {
    return new Response('Missing webhook id', { status: 400 });
  }

  if (processedEvents.has(webhookId)) {
    console.log('Event already processed:', webhookId);
    return new Response('OK', { status: 200 });
  }

  // Process the event
  await handleEvent(event);

  // Mark as processed
  processedEvents.add(webhookId);

  return new Response('OK', { status: 200 });
}

3. Return 200 Status Code

Always return a 200 status code for successfully processed webhooks. Non-2xx responses will trigger retries.

4. Process Quickly

Webhook endpoints should respond within 10 seconds. For longer processing, acknowledge the webhook immediately and process asynchronously.

5. Handle Failures Gracefully

If processing fails, return a 500 status code so One2Pays will retry the webhook.

Webhook Retries

If your endpoint returns a non-2xx status code or times out, One2Pays will retry the webhook:

  • Retry Schedule: 1 minute, 5 minutes, 30 minutes, 2 hours, 12 hours, 24 hours
  • Maximum Retries: 6 attempts over 3 days
  • Timeout: 10 seconds per attempt

Testing Webhooks

Using ngrok for Local Development

# Install ngrok
npm install -g ngrok

# Expose your local server
ngrok http 3000

# Use the HTTPS URL in your webhook settings
# https://abc123.ngrok.io/webhook

On this page