Skip to main content

Pengenalan

Contoh implementasi webhook lengkap dalam berbagai bahasa pemrograman dan framework.
Pilih contoh yang sesuai dengan technology stack Anda.

Daftar Isi

  1. Node.js/Express
  2. Python/Flask
  3. PHP
  4. Go
  5. Ruby

Node.js/Express

Implementasi Lengkap

const express = require('express');
const bodyParser = require('body-parser');
const app = express();

// Konfigurasi
const SECRET_KEY = process.env.WEBHOOK_SECRET_KEY || 'your_secret_key';
const PORT = process.env.PORT || 3000;

// Middleware
app.use(bodyParser.json());

// Simpan event yang sudah diproses (di produksi, gunakan database)
const processedEvents = new Set();

// Bersihkan event yang diproses secara berkala (simpan 10,000 terakhir)
setInterval(() => {
  if (processedEvents.size > 10000) {
    const events = Array.from(processedEvents);
    processedEvents.clear();
    events.slice(5000).forEach(id => processedEvents.add(id));
  }
}, 3600000); // Setiap jam

// Endpoint webhook
app.post('/webhook', async (req, res) => {
  try {
    // Validasi autentikasi
    const authHeader = req.headers.authorization;
    if (authHeader !== SECRET_KEY) {
      console.error('Secret webhook tidak valid');
      return res.status(401).json({ error: 'Unauthorized' });
    }
    
    const { event, timestamp, conversation_id, message } = req.body;
    
    // Deduplikasi event
    const eventId = `${event}_${message.id}`;
    if (processedEvents.has(eventId)) {
      console.log('Event duplikat, melewati');
      return res.status(200).send('OK');
    }
    
    // Proses berdasarkan jenis event
    switch (event) {
      case 'channel.message_in':
        await handleIncomingMessage(message, conversation_id);
        break;
      case 'agent.message_out':
        await handleAgentMessage(message, conversation_id);
        break;
      case 'ai.message_generated':
        await handleAIMessage(message, conversation_id);
        break;
      case 'device.connected':
        await handleDeviceConnected(req.body.device);
        break;
      case 'device.disconnected':
        await handleDeviceDisconnected(req.body.device);
        break;
      default:
        console.log('Jenis event tidak dikenal:', event);
    }
    
    // Tandai sebagai sudah diproses
    processedEvents.add(eventId);
    
    // Respons segera
    res.status(200).send('OK');
  } catch (error) {
    console.error('Error webhook:', error);
    res.status(500).send('Error');
  }
});

// Handler event
async function handleIncomingMessage(message, conversationId) {
  console.log('Pesan baru dari:', message.sender_name);
  console.log('Konten:', message.content.text);
  console.log('Channel:', message.channel.name);
  
  // Logika bisnis Anda di sini
  // Contoh: Simpan ke database
  // await db.messages.create({ ...message, conversationId });
}

async function handleAgentMessage(message, conversationId) {
  console.log('Agen', message.agent.name, 'mengirim pesan');
  
  // Logika bisnis Anda di sini
}

async function handleAIMessage(message, conversationId) {
  console.log('AI', message.ai.name, 'menghasilkan respons');
  
  // Logika bisnis Anda di sini
}

async function handleDeviceConnected(device) {
  console.log('Perangkat', device.name, 'sekarang terhubung');
  
  // Logika bisnis Anda di sini
}

async function handleDeviceDisconnected(device) {
  console.log('Perangkat', device.name, 'sekarang terputus');
  
  // Logika bisnis Anda di sini
}

// Health check
app.get('/health', (req, res) => {
  res.status(200).json({ status: 'healthy', processedEvents: processedEvents.size });
});

// Jalankan server
app.listen(PORT, () => {
  console.log(`Server webhook berjalan di port ${PORT}`);
});

Python/Flask

Implementasi Lengkap

from flask import Flask, request, jsonify
from collections import deque
import os
from datetime import datetime
import threading
import time

# Konfigurasi
app = Flask(__name__)
SECRET_KEY = os.getenv('WEBHOOK_SECRET_KEY', 'your_secret_key')
PORT = os.getenv('PORT', 3000)

# Simpan event yang sudah diproses (di produksi, gunakan database)
processed_events = deque(maxlen=10000)

def cleanup_processed_events():
    """Pembersihan berkala (tidak diperlukan dengan deque maxlen)"""
    pass

# Jalankan thread pembersihan
cleanup_thread = threading.Thread(target=cleanup_processed_events, daemon=True)
cleanup_thread.start()

@app.route('/webhook', methods=['POST'])
def webhook():
    try:
        # Validasi autentikasi
        auth_header = request.headers.get('Authorization')
        if auth_header != SECRET_KEY:
            print('Secret webhook tidak valid')
            return jsonify({'error': 'Unauthorized'}), 401
        
        data = request.get_json()
        event = data.get('event')
        timestamp = data.get('timestamp')
        conversation_id = data.get('conversation_id')
        message = data.get('message')
        
        # Deduplikasi event
        event_id = f"{event}_{message['id']}"
        if event_id in processed_events:
            print(f'Event duplikat: {event_id}')
            return 'OK', 200
        
        # Proses berdasarkan jenis event
        if event == 'channel.message_in':
            handle_incoming_message(message, conversation_id)
        elif event == 'agent.message_out':
            handle_agent_message(message, conversation_id)
        elif event == 'ai.message_generated':
            handle_ai_message(message, conversation_id)
        elif event == 'device.connected':
            handle_device_connected(data['device'])
        elif event == 'device.disconnected':
            handle_device_disconnected(data['device'])
        else:
            print(f'Jenis event tidak dikenal: {event}')
        
        # Tandai sebagai sudah diproses
        processed_events.append(event_id)
        
        return 'OK', 200
    except Exception as e:
        print(f'Error webhook: {str(e)}')
        return 'Error', 500

def handle_incoming_message(message, conversation_id):
    print(f"Pesan baru dari: {message['sender_name']}")
    print(f"Konten: {message['content']['text']}")
    print(f"Channel: {message['channel']['name']}")
    # Logika bisnis Anda di sini

def handle_agent_message(message, conversation_id):
    print(f"Agen {message['agent']['name']} mengirim pesan")
    # Logika bisnis Anda di sini

def handle_ai_message(message, conversation_id):
    print(f"AI {message['ai']['name']} menghasilkan respons")
    # Logika bisnis Anda di sini

def handle_device_connected(device):
    print(f"Perangkat {device['name']} sekarang terhubung")
    # Logika bisnis Anda di sini

def handle_device_disconnected(device):
    print(f"Perangkat {device['name']} sekarang terputus")
    # Logika bisnis Anda di sini

@app.route('/health', methods=['GET'])
def health():
    return jsonify({
        'status': 'healthy',
        'processed_events': len(processed_events)
    })

if __name__ == '__main__':
    app.run(port=PORT, debug=True)

PHP

Implementasi Lengkap

<?php
$SECRET_KEY = getenv('WEBHOOK_SECRET_KEY') ?: 'your_secret_key';
$processedEvents = [];

function webhookHandler() {
    global $SECRET_KEY, $processedEvents;
    
    // Validasi autentikasi
    $authHeader = $_SERVER['HTTP_AUTHORIZATION'] ?? '';
    if ($authHeader !== $SECRET_KEY) {
        error_log('Secret webhook tidak valid');
        http_response_code(401);
        echo json_encode(['error' => 'Unauthorized']);
        return;
    }
    
    // Dapatkan data POST
    $data = json_decode(file_get_contents('php://input'), true);
    
    if (!isset($data['event'], $data['message'])) {
        error_log('Struktur payload tidak valid');
        http_response_code(400);
        echo json_encode(['error' => 'Invalid payload']);
        return;
    }
    
    $event = $data['event'];
    $message = $data['message'];
    $conversationId = $data['conversation_id'] ?? '';
    
    // Deduplikasi event
    $eventId = $event . '_' . ($message['id'] ?? '');
    if (in_array($eventId, $processedEvents)) {
        error_log("Event duplikat: $eventId");
        http_response_code(200);
        echo 'OK';
        return;
    }
    
    // Proses berdasarkan jenis event
    switch ($event) {
        case 'channel.message_in':
            handleIncomingMessage($message, $conversationId);
            break;
        case 'agent.message_out':
            handleAgentMessage($message, $conversationId);
            break;
        case 'ai.message_generated':
            handleAIMessage($message, $conversationId);
            break;
        case 'device.connected':
            handleDeviceConnected($data['device']);
            break;
        case 'device.disconnected':
            handleDeviceDisconnected($data['device']);
            break;
        default:
            error_log("Jenis event tidak dikenal: $event");
    }
    
    // Tandai sebagai sudah diproses
    $processedEvents[] = $eventId;
    
    // Batasi array event yang diproses
    if (count($processedEvents) > 10000) {
        array_shift($processedEvents);
    }
    
    http_response_code(200);
    echo 'OK';
}

function handleIncomingMessage($message, $conversationId) {
    error_log("Pesan baru dari: {$message['sender_name']}");
    error_log("Konten: {$message['content']['text']}");
    error_log("Channel: {$message['channel']['name']}");
    // Logika bisnis Anda di sini
}

function handleAgentMessage($message, $conversationId) {
    error_log("Agen {$message['agent']['name']} mengirim pesan");
    // Logika bisnis Anda di sini
}

function handleAIMessage($message, $conversationId) {
    error_log("AI {$message['ai']['name']} menghasilkan respons");
    // Logika bisnis Anda di sini
}

function handleDeviceConnected($device) {
    error_log("Perangkat {$device['name']} sekarang terhubung");
    // Logika bisnis Anda di sini
}

function handleDeviceDisconnected($device) {
    error_log("Perangkat {$device['name']} sekarang terputus");
    // Logika bisnis Anda di sini
}

// Health check
function healthCheck() {
    http_response_code(200);
    echo json_encode([
        'status' => 'healthy',
        'processed_events' => count($processedEvents)
    ]);
}

// Penanganan route
$requestUri = $_SERVER['REQUEST_URI'] ?? '';

if (strpos($requestUri, '/webhook') !== false) {
    webhookHandler();
} elseif (strpos($requestUri, '/health') !== false) {
    healthCheck();
}

// Jalankan handler
webhookHandler();
?>

Go

Implementasi Lengkap

package main

import (
    "encoding/json"
    "fmt"
    "log"
    "net/http"
    "os"
    "sync"
    "time"
)

// Konfigurasi
const (
    SECRET_KEY = os.Getenv("WEBHOOK_SECRET_KEY")
    PORT      = os.Getenv("PORT")
)

var (
    processedEvents = make(map[string]bool)
    mu            sync.RWMutex
)

// Struktur event
type WebhookPayload struct {
    Event          string      `json:"event"`
    Timestamp      int64       `json:"timestamp"`
    ConversationID string      `json:"conversation_id"`
    Message        Message     `json:"message"`
    Device         Device      `json:"device,omitempty"`
}

type Message struct {
    ID         string       `json:"id"`
    SenderName string       `json:"sender_name"`
    Channel    Channel      `json:"channel"`
    Bisnis     Bisnis      `json:"bisnis"`
    User       User         `json:"user"`
    Content    Content      `json:"content"`
    Agent      *Agent       `json:"agent,omitempty"`
    AI         *AI          `json:"ai,omitempty"`
}

type Channel struct {
    ID     string `json:"id"`
    Name   string `json:"name"`
    Engine string `json:"engine"`
}

type Content struct {
    Type         string       `json:"type"`
    Text         string       `json:"text,omitempty"`
    Attachments  []Attachment  `json:"attachments,omitempty"`
}

// ... definisi struct lainnya

func webhookHandler(w http.ResponseWriter, r *http.Request) {
    // Validasi method
    if r.Method != http.MethodPost {
        http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
        return
    }
    
    // Validasi autentikasi
    authHeader := r.Header.Get("Authorization")
    if authHeader != SECRET_KEY {
        log.Println("Secret webhook tidak valid")
        http.Error(w, "Unauthorized", http.StatusUnauthorized)
        return
    }
    
    // Parse payload
    var payload WebhookPayload
    err := json.NewDecoder(r.Body).Decode(&payload)
    if err != nil {
        log.Printf("Error decoding payload: %v", err)
        http.Error(w, "Invalid payload", http.StatusBadRequest)
        return
    }
    
    // Deduplikasi event
    eventID := fmt.Sprintf("%s_%s", payload.Event, payload.Message.ID)
    mu.RLock()
    _, exists := processedEvents[eventID]
    mu.RUnlock()
    
    if exists {
        log.Printf("Event duplikat: %s", eventID)
        w.WriteHeader(http.StatusOK)
        fmt.Fprintf(w, "OK")
        return
    }
    
    // Proses berdasarkan jenis event
    switch payload.Event {
    case "channel.message_in":
        handleIncomingMessage(payload.Message, payload.ConversationID)
    case "agent.message_out":
        handleAgentMessage(payload.Message, payload.ConversationID)
    case "ai.message_generated":
        handleAIMessage(payload.Message, payload.ConversationID)
    case "device.connected":
        handleDeviceConnected(payload.Device)
    case "device.disconnected":
        handleDeviceDisconnected(payload.Device)
    default:
        log.Printf("Jenis event tidak dikenal: %s", payload.Event)
    }
    
    // Tandai sebagai sudah diproses
    mu.Lock()
    processedEvents[eventID] = true
    mu.Unlock()
    
    // Respons segera
    w.WriteHeader(http.StatusOK)
    fmt.Fprintf(w, "OK")
}

func handleIncomingMessage(message Message, conversationID string) {
    log.Printf("Pesan baru dari: %s", message.SenderName)
    log.Printf("Konten: %s", message.Content.Text)
    log.Printf("Channel: %s", message.Channel.Name)
    // Logika bisnis Anda di sini
}

func handleAgentMessage(message Message, conversationID string) {
    log.Printf("Agen %s mengirim pesan", message.Agent.Name)
    // Logika bisnis Anda di sini
}

func handleAIMessage(message Message, conversationID string) {
    log.Printf("AI %s menghasilkan respons", message.AI.Name)
    // Logika bisnis Anda di sini
}

func handleDeviceConnected(device Device) {
    log.Printf("Perangkat %s sekarang terhubung", device.Name)
    // Logika bisnis Anda di sini
}

func handleDeviceDisconnected(device Device) {
    log.Printf("Perangkat %s sekarang terputus", device.Name)
    // Logika bisnis Anda di sini
}

func healthHandler(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(map[string]interface{}{
        "status":           "healthy",
        "processed_events": len(processedEvents),
    })
}

func main() {
    // Bersihkan event yang diproses secara berkala
    go func() {
        for {
            time.Sleep(time.Hour)
            mu.Lock()
            if len(processedEvents) > 10000 {
                // Hapus 5000 event tertua
                count := 0
                for key := range processedEvents {
                    if count >= 5000 {
                        delete(processedEvents, key)
                    }
                    count++
                }
            }
            mu.Unlock()
        }
    }()
    
    // Setup route
    http.HandleFunc("/webhook", webhookHandler)
    http.HandleFunc("/health", healthHandler)
    
    // Jalankan server
    port := PORT
    if port == "" {
        port = "3000"
    }
    log.Printf("Server webhook berjalan di port %s", port)
    log.Fatal(http.ListenAndServe(":"+port, nil))
}

Menguji Webhook Anda

Uji dengan curl

# Uji webhook channel.message_in
curl -X POST https://your-domain.com/webhook \
  -H "Authorization: your_secret_key" \
  -H "Content-Type: application/json" \
  -d '{
    "event": "channel.message_in",
    "timestamp": 1738056000,
    "conversation_id": "conv_test123",
    "conversation_label": "Test User",
    "need_human": 0,
    "message": {
      "id": "msg_test456",
      "sender_name": "Test User",
      "channel": {
        "id": "channel_test",
        "name": "WhatsApp Business",
        "engine": "wa"
      },
      "bisnis": {
        "id": "biz_test",
        "name": "Test Business"
      },
      "user": {
        "id": "merchant_test",
        "name": "Test Merchant"
      },
      "content": {
        "type": "text",
        "text": "Halo, saya butuh bantuan"
      }
    }
  }'

Uji dengan Postman

  1. Buat request baru
    • Method: POST
    • URL: https://your-domain.com/webhook
  2. Tambahkan header
    • Authorization: your_secret_key
    • Content-Type: application/json
  3. Tambahkan body (raw JSON)
    {
      "event": "channel.message_in",
      "timestamp": 1738056000,
      "conversation_id": "conv_test123",
      "message": {
        "id": "msg_test456",
        "sender_name": "Test User",
        "channel": {
          "name": "WhatsApp Business",
          "engine": "wa"
        },
        "content": {
          "type": "text",
          "text": "Pesan test"
        }
      }
    }
    
  4. Kirim request dan verifikasi Anda menerima 200 OK
Selalu uji dengan payload real sebelum deploy ke produksi.

Checklist Produksi

Sebelum go live, pastikan Anda telah menyelesaikan langkah-langkah ini.
  • URL Webhook dapat diakses publik
  • HTTPS diaktifkan (sertifikat SSL valid)
  • Autentikasi diimplementasikan dan diuji
  • Idempotensi diimplementasikan
  • Penanganan error sudah ada
  • Logging dikonfigurasi
  • Monitoring disiapkan
  • Rate limiting dikonfigurasi
  • Indexing database dioptimalkan
  • Load testing telah dilakukan
  • Tim sudah dilatih langkah-langkah troubleshooting

Langkah Selanjutnya