WhatsApp API dengan Webhook

Panduan setup webhook WhatsApp API. Terima pesan real-time, handle events, secure webhook. Tutorial lengkap untuk developer!

WhatsApp API dengan Webhook
WhatsApp API dengan Webhook

Webhook = Real-time communication!

Webhook memungkinkan server kamu menerima notifikasi real-time setiap kali ada pesan masuk, status berubah, atau event lainnya di WhatsApp.


Apa itu Webhook?

📡 WEBHOOK EXPLAINED:

TANPA WEBHOOK (Polling):
- Server kamu tanya terus: "Ada pesan baru?"
- Boros resource
- Delay response
- Tidak efisien

DENGAN WEBHOOK:
- WhatsApp yang notify: "Hey, ada pesan!"
- Real-time
- Efisien
- Event-driven

Webhook Events

📋 EVENT TYPES:

MESSAGES:
- messages - Pesan masuk
- message_status - Status delivered/read

ACCOUNT:
- account_update - Perubahan akun
- phone_number_quality - Quality rating

BUSINESS:
- template_status - Template approval
- account_alerts - Peringatan akun

MEDIA:
- media - Media upload status

Setup Webhook

1. Buat Endpoint:

javascript

// server.js (Express)
const express = require('express');
const app = express();

app.use(express.json());

// Webhook verification (GET)
app.get('/webhook', (req, res) => {
    const mode = req.query['hub.mode'];
    const token = req.query['hub.verify_token'];
    const challenge = req.query['hub.challenge'];
    
    // Token yang kamu set di Meta Dashboard
    const VERIFY_TOKEN = process.env.WEBHOOK_VERIFY_TOKEN;
    
    if (mode === 'subscribe' && token === VERIFY_TOKEN) {
        console.log('Webhook verified!');
        res.status(200).send(challenge);
    } else {
        res.status(403).send('Forbidden');
    }
});

// Webhook handler (POST)
app.post('/webhook', (req, res) => {
    const body = req.body;
    
    console.log('Webhook received:', JSON.stringify(body, null, 2));
    
    // Harus respond 200 dalam 20 detik!
    res.status(200).send('OK');
    
    // Process async
    processWebhook(body);
});

app.listen(3000, () => {
    console.log('Webhook server running on port 3000');
});

2. Register di Meta Dashboard:

📝 LANGKAH REGISTER:

1. Buka Meta Developer Dashboard
2. Pilih App kamu
3. WhatsApp > Configuration
4. Webhook > Edit
5. Masukkan:
   • Callback URL: https://yourdomain.com/webhook
   • Verify Token: token_rahasia_kamu
6. Klik Verify and Save
7. Subscribe ke events yang diinginkan

Handle Incoming Messages

Message Structure:

json

{
  "object": "whatsapp_business_account",
  "entry": [{
    "id": "WHATSAPP_BUSINESS_ACCOUNT_ID",
    "changes": [{
      "value": {
        "messaging_product": "whatsapp",
        "metadata": {
          "display_phone_number": "628123456789",
          "phone_number_id": "PHONE_NUMBER_ID"
        },
        "contacts": [{
          "profile": { "name": "John Doe" },
          "wa_id": "628987654321"
        }],
        "messages": [{
          "from": "628987654321",
          "id": "wamid.xxx",
          "timestamp": "1234567890",
          "type": "text",
          "text": { "body": "Halo!" }
        }]
      },
      "field": "messages"
    }]
  }]
}

Process Handler:

javascript

async function processWebhook(body) {
    if (body.object !== 'whatsapp_business_account') return;
    
    for (const entry of body.entry) {
        for (const change of entry.changes) {
            const value = change.value;
            
            if (change.field === 'messages') {
                // Handle incoming messages
                if (value.messages) {
                    for (const message of value.messages) {
                        await handleIncomingMessage(value, message);
                    }
                }
                
                // Handle status updates
                if (value.statuses) {
                    for (const status of value.statuses) {
                        await handleStatusUpdate(status);
                    }
                }
            }
        }
    }
}

async function handleIncomingMessage(value, message) {
    const from = message.from;
    const name = value.contacts[0]?.profile?.name || 'Customer';
    const phoneNumberId = value.metadata.phone_number_id;
    
    console.log(`Pesan dari ${name} (${from})`);
    
    switch (message.type) {
        case 'text':
            await handleTextMessage(phoneNumberId, from, message.text.body);
            break;
        case 'image':
            await handleImageMessage(phoneNumberId, from, message.image);
            break;
        case 'interactive':
            await handleInteractiveMessage(phoneNumberId, from, message.interactive);
            break;
        default:
            console.log(`Tipe pesan tidak dihandle: ${message.type}`);
    }
}

async function handleStatusUpdate(status) {
    const messageId = status.id;
    const recipientId = status.recipient_id;
    const statusType = status.status; // sent, delivered, read, failed
    
    console.log(`Status ${statusType} untuk ${messageId}`);
    
    // Update di database
    await db.messages.updateOne(
        { messageId },
        { $set: { status: statusType, updatedAt: new Date() } }
    );
}

Tipe Pesan

Text Message:

javascript

// message.type === 'text'
{
    "type": "text",
    "text": {
        "body": "Halo, mau tanya produk"
    }
}

// Handler
async function handleTextMessage(phoneNumberId, from, text) {
    console.log(`Text: ${text}`);
    
    // Auto-reply
    await sendMessage(phoneNumberId, from, {
        type: 'text',
        text: { body: 'Hai! Ada yang bisa dibantu?' }
    });
}

Image Message:

javascript

// message.type === 'image'
{
    "type": "image",
    "image": {
        "id": "MEDIA_ID",
        "mime_type": "image/jpeg",
        "sha256": "xxx",
        "caption": "Ini foto produk"
    }
}

// Handler
async function handleImageMessage(phoneNumberId, from, image) {
    // Download image
    const mediaUrl = await getMediaUrl(image.id);
    const imageBuffer = await downloadMedia(mediaUrl);
    
    // Process (OCR, save, etc)
    await processImage(imageBuffer);
}

Interactive Response:

javascript

// message.type === 'interactive'
{
    "type": "interactive",
    "interactive": {
        "type": "button_reply",
        "button_reply": {
            "id": "btn_order",
            "title": "Order Sekarang"
        }
    }
}

// atau list_reply
{
    "type": "interactive",
    "interactive": {
        "type": "list_reply",
        "list_reply": {
            "id": "product_001",
            "title": "Produk A",
            "description": "Rp 100.000"
        }
    }
}

Webhook Security

Verify Signature:

javascript

const crypto = require('crypto');

function verifyWebhookSignature(req) {
    const signature = req.headers['x-hub-signature-256'];
    
    if (!signature) return false;
    
    const expectedSignature = crypto
        .createHmac('sha256', process.env.APP_SECRET)
        .update(JSON.stringify(req.body))
        .digest('hex');
    
    const trusted = Buffer.from(`sha256=${expectedSignature}`, 'utf8');
    const received = Buffer.from(signature, 'utf8');
    
    return crypto.timingSafeEqual(trusted, received);
}

// Middleware
app.post('/webhook', (req, res) => {
    if (!verifyWebhookSignature(req)) {
        console.log('Invalid signature!');
        return res.status(401).send('Invalid signature');
    }
    
    // Process...
    res.status(200).send('OK');
});

IP Whitelist:

javascript

const ALLOWED_IPS = [
    // Meta IP ranges
    '157.240.0.0/16',
    '31.13.24.0/21',
    // ... more IPs
];

function isAllowedIP(ip) {
    // Implement IP check
    return ALLOWED_IPS.some(range => ipInRange(ip, range));
}

Error Handling

Retry Logic:

javascript

// Meta akan retry jika tidak dapat 200
// Retry schedule: 15s, 1m, 5m, 30m

// Pastikan respond cepat!
app.post('/webhook', async (req, res) => {
    // RESPOND IMMEDIATELY
    res.status(200).send('OK');
    
    // Process di background
    try {
        await processWebhook(req.body);
    } catch (error) {
        console.error('Webhook processing error:', error);
        // Log untuk debug, tapi jangan fail response
    }
});

Queue Processing:

javascript

const Queue = require('bull');
const webhookQueue = new Queue('webhook-processing');

app.post('/webhook', (req, res) => {
    // Respond immediately
    res.status(200).send('OK');
    
    // Add to queue
    webhookQueue.add(req.body);
});

// Process dari queue
webhookQueue.process(async (job) => {
    await processWebhook(job.data);
});

Testing Webhook

Ngrok untuk Local:

bash

# Install ngrok
npm install -g ngrok

# Jalankan server lokal
node server.js

# Expose ke internet
ngrok http 3000

# Dapat URL seperti: https://abc123.ngrok.io
# Gunakan URL ini untuk webhook di Meta Dashboard

Test dengan curl:

bash

curl -X POST https://yourdomain.com/webhook \
  -H "Content-Type: application/json" \
  -d '{
    "object": "whatsapp_business_account",
    "entry": [{
      "changes": [{
        "value": {
          "messages": [{
            "from": "628123456789",
            "type": "text",
            "text": {"body": "Test message"}
          }]
        },
        "field": "messages"
      }]
    }]
  }'

Best Practices

DO ✅

- Respond 200 dalam 5 detik
- Verify signature
- Process async (queue)
- Log semua events
- Handle all message types
- Idempotent processing

DON'T ❌

- Block response untuk processing
- Skip signature verification
- Ignore unknown events
- No error handling
- Process synchronously
- Trust all requests

FAQ

Kenapa webhook tidak diterima?

Cek: URL accessible, SSL valid, respond 200, verify token benar.

Berapa lama timeout webhook?

20 detik. Lebih dari itu dianggap failed dan akan retry.

Perlu handle retry/duplicate?

Ya! Simpan message ID dan skip jika sudah diproses.


Kesimpulan

Webhook = Real-time & efisien!

PollingWebhook
Tanya terusEvent-driven
Boros resourceEfisien
DelayReal-time
ComplexSimple

Setup Webhook Sekarang →


Artikel Terkait