Pengenalan
Contoh implementasi webhook lengkap dalam berbagai bahasa pemrograman dan framework.Pilih contoh yang sesuai dengan technology stack Anda.
Daftar Isi
Node.js/Express
Implementasi Lengkap
Copy
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
Copy
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
Copy
<?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
Copy
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
Copy
# 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
-
Buat request baru
- Method: POST
- URL:
https://your-domain.com/webhook
-
Tambahkan header
Authorization:your_secret_keyContent-Type:application/json
-
Tambahkan body (raw JSON)
Copy
{ "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" } } } -
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