Examples
Handle Webhook Example
Complete example of handling webhook events from One2Pays.
Overview
This example shows how to:
- Receive webhook requests
- Verify webhook signatures
- Handle idempotency
- 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 createdpayment.processing- Payment is being processedpayment.succeeded- Payment completed successfullypayment.failed- Payment failedpayment.canceled- Payment was canceledpayment.expired- Payment expired
Withdraw Events
withdraw.paid- Withdraw completed successfullywithdraw.failed- Withdraw failedwithdraw.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
- Always verify signatures - Never process webhooks without signature verification
- Implement idempotency - Use event IDs to prevent duplicate processing
- Respond quickly - Return 200 OK within 5 seconds
- Handle errors gracefully - Log errors but return 200 OK to prevent retries
- Test webhooks - Use the test endpoint to verify your webhook handler