Webhooks Deep Dive

Advanced webhook handling and best practices

Webhooks Deep Dive

Advanced guide to implementing production-ready webhook handling.

Overview

Webhooks allow Koywe to send real-time notifications about order status changes to your server.

Why Use Webhooks?

Benefits:

  • Real-time order status updates
  • No polling required
  • Scalable architecture
  • Reliable delivery with retries

Webhook Signature Verification

Critical: Always verify webhook signatures to ensure authenticity.

Verification Process

1const crypto = require('crypto');
2const express = require('express');
3
4app.post('/webhooks/koywe',
5 express.raw({ type: 'application/json' }), // MUST use raw body
6 (req, res) => {
7 // 1. Get signature from header
8 const signature = req.headers['koywe-signature'];
9 const secret = process.env.KOYWE_WEBHOOK_SECRET;
10
11 // 2. Calculate expected signature
12 const expectedSignature = crypto
13 .createHmac('sha256', secret)
14 .update(req.body) // Raw body (Buffer)
15 .digest('hex');
16
17 // 3. Compare signatures
18 if (signature !== expectedSignature) {
19 console.error('Invalid webhook signature');
20 return res.status(401).send('Invalid signature');
21 }
22
23 // 4. Signature valid, parse event
24 const event = JSON.parse(req.body);
25
26 // 5. Process event
27 await processWebhook(event);
28
29 // 6. Respond quickly
30 res.status(200).send('OK');
31 }
32);

Common Mistakes:

  • Using express.json() instead of express.raw() - this modifies the body
  • Using parsed JSON for signature calculation - must use raw body
  • Wrong secret - verify youโ€™re using webhook secret, not API secret

Idempotency

Handle duplicate webhook deliveries:

Idempotent Processing
1const processedEvents = new Set(); // In production, use Redis/database
2
3async function processWebhook(event) {
4 const eventId = event.id;
5
6 // Check if already processed
7 if (processedEvents.has(eventId)) {
8 console.log(`Event ${eventId} already processed, skipping`);
9 return;
10 }
11
12 try {
13 // Process event
14 await handleEvent(event);
15
16 // Mark as processed
17 processedEvents.add(eventId);
18
19 // In production, persist to database
20 await db.saveProcessedEvent(eventId, new Date());
21
22 } catch (error) {
23 console.error(`Error processing event ${eventId}:`, error);
24 // Don't mark as processed - allow retry
25 throw error;
26 }
27}

Event Types

Order Events

Event TypeDescriptionWhen Fired
order.createdOrder createdOrder creation
order.pendingAwaiting paymentAfter creation
order.processingPayment being processedCustomer initiated payment
order.paidPayment confirmedPayment provider confirms
order.completedFunds settledFunds credited/debited
order.failedOrder failedPayment or transfer failed
order.expiredOrder expiredPassed due date
order.cancelledOrder cancelledUser or system cancellation

Event Payload Structure

1{
2 "id": "evt_abc123xyz",
3 "type": "order.completed",
4 "version": "v1",
5 "occurred_at": "2025-11-13T15:30:00Z",
6 "source": "koywe.api",
7 "environment": "production",
8 "organization_id": "org_xyz789",
9 "merchant_id": "mrc_abc123",
10 "data": {
11 "orderId": "ord_123456",
12 "type": "PAYIN",
13 "status": "COMPLETED",
14 "amountIn": 50000,
15 "originCurrencySymbol": "COP",
16 "destinationCurrencySymbol": "COP",
17 "externalId": "order-12345",
18 "contactId": "cnt_customer1",
19 "dates": {
20 "confirmationDate": "2025-11-13T15:29:00Z",
21 "paymentDate": "2025-11-13T15:29:30Z",
22 "deliveryDate": "2025-11-13T15:30:00Z"
23 }
24 }
25}

Event Handling

Complete Event Handler

Production Handler
1async function handleEvent(event) {
2 const { type, data } = event;
3
4 switch (type) {
5 case 'order.created':
6 await onOrderCreated(data);
7 break;
8
9 case 'order.pending':
10 await onOrderPending(data);
11 break;
12
13 case 'order.processing':
14 await onOrderProcessing(data);
15 break;
16
17 case 'order.paid':
18 await onOrderPaid(data);
19 break;
20
21 case 'order.completed':
22 await onOrderCompleted(data);
23 break;
24
25 case 'order.failed':
26 await onOrderFailed(data);
27 break;
28
29 case 'order.expired':
30 await onOrderExpired(data);
31 break;
32
33 case 'order.cancelled':
34 await onOrderCancelled(data);
35 break;
36
37 default:
38 console.warn(`Unknown event type: ${type}`);
39 }
40}
41
42async function onOrderCompleted(data) {
43 // CRITICAL: This is where you fulfill orders
44
45 if (data.type === 'PAYIN') {
46 // Customer paid, fulfill order
47 await fulfillOrder(data.externalId);
48 await sendConfirmationEmail(data.externalId);
49 await updateInventory(data.externalId);
50 } else if (data.type === 'PAYOUT') {
51 // Payout completed, mark as paid
52 await markInvoiceAsPaid(data.externalId);
53 await notifyProvider(data.externalId);
54 }
55}
56
57async function onOrderFailed(data) {
58 // Handle failure
59 await markOrderFailed(data.externalId);
60 await sendFailureNotification(data.externalId);
61
62 // Log for investigation
63 console.error('Order failed:', {
64 orderId: data.orderId,
65 externalId: data.externalId,
66 type: data.type
67 });
68}

Response Times

Respond to webhooks quickly (< 5 seconds):

Async Processing
1app.post('/webhooks/koywe', express.raw({ type: 'application/json' }), async (req, res) => {
2 // Convert raw body to string once
3 const rawBody = req.body.toString();
4
5 // 1. Verify signature (fast)
6 if (!verifySignature(rawBody, req.headers['koywe-signature'])) {
7 return res.status(401).send('Invalid signature');
8 }
9
10 // Parse JSON from string
11 const event = JSON.parse(rawBody);
12
13 // 2. Check idempotency (fast)
14 if (await isEventProcessed(event.id)) {
15 return res.status(200).send('Already processed');
16 }
17
18 // 3. Respond immediately
19 res.status(200).send('OK');
20
21 // 4. Process asynchronously (AFTER responding)
22 setImmediate(async () => {
23 try {
24 await processWebhook(event);
25 await markEventProcessed(event.id);
26 } catch (error) {
27 console.error('Webhook processing error:', error);
28 await logWebhookError(event, error);
29 }
30 });
31});

Best Practice: Respond immediately (200 OK) and process the webhook asynchronously. This prevents timeouts and ensures reliable delivery.


Retry Mechanism

Koywe automatically retries failed webhooks:

  • Retry Schedule: 1 min, 5 min, 15 min, 1 hour, 6 hours, 24 hours
  • Max Retries: 6 attempts over 24 hours
  • Retry Condition: Non-200 response or timeout

Handling Retries

Idempotent Handler
1async function processWebhook(event) {
2 const eventId = event.id;
3
4 // Use database for persistence (not in-memory)
5 const alreadyProcessed = await db.isEventProcessed(eventId);
6
7 if (alreadyProcessed) {
8 console.log(`Event ${eventId} already processed`);
9 return; // Return success without re-processing
10 }
11
12 // Process event
13 await handleEvent(event);
14
15 // Mark as processed AFTER successful handling
16 await db.markEventProcessed(eventId, {
17 processedAt: new Date(),
18 eventType: event.type,
19 orderId: event.data.orderId
20 });
21}

Testing Webhooks

Local Testing with ngrok

1

Install ngrok

$npm install -g ngrok
2

Start your local server

$node server.js
># Server running on http://localhost:3000
3

Create ngrok tunnel

$ngrok http 3000
4

Use ngrok URL

Forwarding: https://abc123.ngrok.io -> http://localhost:3000
5

Configure webhook

Use https://abc123.ngrok.io/webhooks/koywe as your webhook URL

6

Test

Create orders and watch webhooks arrive in real-time

Using webhook.site

For quick testing without code:

  1. Visit https://webhook.site
  2. Copy your unique URL
  3. Use as webhook URL in API
  4. View incoming webhooks in browser

Production Checklist

Before going live:

  • Signature verification implemented
  • Idempotency handling in place
  • Quick response times (< 5 seconds)
  • Async processing implemented
  • Error logging configured
  • Event persistence to database
  • Monitoring and alerts setup
  • Tested with ngrok/webhook.site
  • Webhook endpoint is HTTPS
  • Firewall allows Koywe IPs

Monitoring and Debugging

Webhook Logs

Query webhook delivery attempts:

Node.js
1async function getWebhookDeliveries(token, orgId, orderId) {
2 const response = await axios.get(
3 `https://api.koywe.com/api/v1/organizations/${orgId}/webhook-events`,
4 {
5 params: { orderId },
6 headers: { 'Authorization': `Bearer ${token}` }
7 }
8 );
9
10 return response.data;
11}
12
13// Usage
14const deliveries = await getWebhookDeliveries(token, orgId, 'ord_123');
15deliveries.forEach(d => {
16 console.log(`Attempt ${d.attempt}: ${d.status} (${d.responseCode})`);
17 console.log(` Delivered at: ${d.deliveredAt}`);
18 console.log(` Response time: ${d.responseTime}ms`);
19});

Replay Webhooks

Replay a webhook manually:

Replay Event
1async function replayWebhook(token, orgId, eventId) {
2 const response = await axios.post(
3 `https://api.koywe.com/api/v1/organizations/${orgId}/webhook-events/{eventId}/replay`,
4 {},
5 {
6 headers: { 'Authorization': `Bearer ${token}` }
7 }
8 );
9
10 console.log('Webhook replayed');
11}

Security Best Practices

Critical Security Measures:

  1. Always verify signatures - Never skip this step
  2. Use HTTPS - Webhooks over HTTP are insecure
  3. Validate event structure - Check required fields exist
  4. Use webhook secret - Donโ€™t use API secret
  5. Log all webhooks - For audit trail
  6. Rate limit your endpoint - Protect against attacks

Next Steps