Logo
Guides

Webhooks

Webhooks allow you to receive real-time notifications about payment events. Instead of polling the API for status updates, One2Pays will send HTTP POST requests to your webhook endpoint when events occur.

How Webhooks Work

  1. Register your webhook endpoint with One2Pays
  2. One2Pays sends events to your endpoint when payments change status
  3. Verify the signature to ensure the request is from One2Pays
  4. Process the event and update your system accordingly

Webhook Events

One2Pays sends webhooks for the following events:

Payment Events

  • payment.created - A new payment was created
  • payment.processing - Payment is being processed
  • payment.succeeded - Payment completed successfully
  • payment.failed - Payment failed
  • payment.canceled - Payment was canceled
  • payment.expired - Payment expired

Withdraw Events

  • withdraw.paid - A withdraw was successfully completed
  • withdraw.failed - Withdraw failed
  • withdraw.canceled - Withdraw was canceled

Registering a Webhook Endpoint

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 Payload

Webhook requests include the following payload:

{
  "id": "evt_1234567890abcdef",
  "type": "payment.succeeded",
  "data": {
    "id": "pay_1234567890abcdef",
    "amount": "1000.00",
    "currency": "THB",
    "status": "succeeded",
    "referenceId": "order-12345"
  },
  "createdAt": "2024-01-01T00:00:00Z"
}

Verifying Webhook Signatures

Always verify webhook signatures to ensure requests are from One2Pays:

import crypto from 'crypto';

function verifyWebhookSignature(
  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')
  );
}

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

  if (!signature || !timestamp || !verifyWebhookSignature(payload, signature, timestamp, WEBHOOK_SECRET)) {
    return new Response('Invalid signature', { status: 401 });
  }

  const event = JSON.parse(payload);
  // Process event...

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

Webhook Headers

One2Pays includes these headers in webhook requests:

HeaderDescription
X-Webhook-SignatureHMAC signature of the payload (format: sha256=<hex_signature>)
X-Webhook-TimestampTimestamp in milliseconds used for signature
X-Webhook-EventEvent type (e.g., payment.succeeded)
X-Webhook-IdUnique event ID
Content-Typeapplication/json

Handling Webhooks

Best Practices

  1. Verify signatures - Always verify webhook signatures
  2. Idempotency - Handle duplicate events gracefully using event IDs
  3. Respond quickly - Return 200 OK within 5 seconds
  4. Log events - Log all webhook events for debugging
  5. Handle failures - Implement retry logic for failed webhook processing

Example: Complete Webhook Handler

import crypto from 'crypto';

const WEBHOOK_SECRET = process.env.PAYMENT_WEBHOOK_SECRET!;

export async function POST(request: Request) {
  try {
    const payload = await request.text();
    const signature = request.headers.get('x-webhook-signature');
    const eventId = request.headers.get('x-webhook-event-id');

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

    const event = JSON.parse(payload);

    // Handle idempotency (check if event already processed)
    if (await isEventProcessed(eventId)) {
      return new Response('OK', { status: 200 });
    }

    // Process event
    switch (event.type) {
      case 'payment.succeeded':
        await handlePaymentSucceeded(event.data);
        break;
      case 'payment.failed':
        await handlePaymentFailed(event.data);
        break;
      case 'withdraw.paid':
        await handleWithdrawPaid(event.data);
        break;
      case 'withdraw.failed':
        await handleWithdrawFailed(event.data);
        break;
      default:
        console.log('Unhandled event type:', event.type);
    }

    // Mark event as processed
    await markEventProcessed(eventId);

    return new Response('OK', { status: 200 });
  } catch (error) {
    console.error('Webhook error:', error);
    return new Response('Error', { status: 500 });
  }
}

async function handlePaymentSucceeded(payment: any) {
  // Update order status, send confirmation email, etc.
  console.log('Payment succeeded:', payment.id);
}

async function handlePaymentFailed(payment: any) {
  console.log('Payment failed:', payment.id);
  // Notify customer, update order status, etc.
}

async function handlePaymentCanceled(payment: any) {
  console.log('Payment canceled:', payment.id);
  // Update order status, etc.
}

async function handleWithdrawPaid(withdraw: any) {
  console.log('Withdraw completed:', withdraw.id);
  // Update your system
}

async function handleWithdrawFailed(withdraw: any) {
  console.log('Withdraw failed:', withdraw.id);
  // Handle failure
}

Webhook Retries

One2Pays automatically retries failed webhook deliveries:

  • Initial attempt: Immediate
  • Retry 1: After 1 minute
  • Retry 2: After 5 minutes
  • Retry 3: After 15 minutes
  • Retry 4: After 1 hour
  • Retry 5: After 6 hours
  • Retry 6: After 24 hours

Webhooks are considered failed if:

  • Your endpoint returns a non-2xx status code
  • Your endpoint times out (>30 seconds)
  • Network error occurs

Testing Webhooks

You can test webhooks using the test webhook feature in your merchant dashboard, or by using a tool like ngrok for local development.

Security Best Practices

Always verify webhook signatures

Never process webhooks without signature verification.

  1. Use HTTPS - Always use HTTPS for webhook endpoints
  2. Verify signatures - Always verify HMAC signatures
  3. Check event IDs - Implement idempotency using event IDs
  4. Validate payloads - Validate webhook payload structure
  5. Rate limiting - Implement rate limiting on webhook endpoints
  6. Logging - Log all webhook events for audit trails

On this page