Logo
Examples

Handle Webhook Example

Complete example of handling webhook events from One2Pays.

Overview

This example shows how to:

  1. Receive webhook requests
  2. Verify webhook signatures
  3. Handle idempotency
  4. Process different event types

Complete Example

// app/api/webhooks/payment/route.ts
import crypto from 'crypto';
import { NextRequest, NextResponse } from 'next/server';

const WEBHOOK_SECRET = process.env.PAYMENT_WEBHOOK_SECRET!;

// Verify webhook signature
function verifySignature(
  payload: string,
  signature: string,
  timestamp: 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', WEBHOOK_SECRET)
    .update(signedPayload)
    .digest('hex');

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

// Check if event was already processed (idempotency)
async function isEventProcessed(eventId: string): Promise<boolean> {
  // Implement your idempotency check (e.g., database lookup)
  // This is a simplified example
  return false;
}

// Mark event as processed
async function markEventProcessed(eventId: string): Promise<void> {
  // Implement your idempotency storage (e.g., database insert)
}

// Handle payment succeeded event
async function handlePaymentSucceeded(payment: any) {
  // Update order status in your database
  console.log('Payment succeeded:', payment.id);

  // Send confirmation email, update inventory, etc.
  // await updateOrderStatus(payment.referenceId, 'paid');
  // await sendConfirmationEmail(payment.referenceId);
}

// Handle payment failed event
async function handlePaymentFailed(payment: any) {
  console.log('Payment failed:', payment.id);

  // Notify customer, update order status, etc.
  // await updateOrderStatus(payment.referenceId, 'failed');
  // await notifyCustomer(payment.referenceId);
}

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

    if (!signature || !timestamp) {
      return NextResponse.json(
        { error: 'Missing signature or timestamp' },
        { status: 400 }
      );
    }

    // Verify signature
    if (!verifySignature(payload, signature, timestamp)) {
      return NextResponse.json(
        { error: 'Invalid signature' },
        { status: 401 }
      );
    }

    const event = JSON.parse(payload);

    // Handle idempotency
    if (await isEventProcessed(eventId || event.id)) {
      return NextResponse.json({ received: true });
    }

    // Process event based on type
    switch (event.type) {
      case 'payment.succeeded':
        await handlePaymentSucceeded(event.data);
        break;
      case 'payment.failed':
        await handlePaymentFailed(event.data);
        break;
      case 'payment.canceled':
        console.log('Payment canceled:', event.data.id);
        break;
      case 'withdraw.paid':
        console.log('Withdraw completed:', event.data.id);
        break;
      case 'withdraw.failed':
        console.log('Withdraw failed:', event.data.id);
        break;
      default:
        console.log('Unhandled event type:', event.type);
    }

    // Mark event as processed
    if (eventId) {
      await markEventProcessed(eventId);
    }

    return NextResponse.json({ received: true });
  } catch (error) {
    console.error('Webhook error:', error);
    return NextResponse.json(
      { error: 'Internal server error' },
      { status: 500 }
    );
  }
}

Event Types

Payment Events

  • payment.created - 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 - Withdraw completed successfully
  • withdraw.failed - Withdraw failed
  • withdraw.canceled - Withdraw was canceled

Webhook Payload Structure

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

Best Practices

  1. Always verify signatures - Never process webhooks without signature verification
  2. Implement idempotency - Use event IDs to prevent duplicate processing
  3. Respond quickly - Return 200 OK within 5 seconds
  4. Handle errors gracefully - Log errors but return 200 OK to prevent retries
  5. Test webhooks - Use the test endpoint to verify your webhook handler

On this page