Webhooks
Ringkasan Webhook
Terima notifikasi real-time saat terjadi event di Api.co.id.
Saat sebuah event terjadi (misalnya menerima pesan), kami akan mengirim HTTP POST request ke URL yang Anda konfigurasi beserta data event-nya.
Cara Kerja
- Konfigurasikan URL webhook di dashboard Anda
- Pilih event yang ingin Anda terima
- Kami mengirim POST request ke URL Anda saat event terjadi
- Server Anda memproses event dan merespons dengan 200 OK
Event Webhook
Event webhook yang bisa Anda subscribe.
| Event | Keterangan |
|---|---|
message.received | Pesan masuk baru |
message.sent | Pesan berhasil dikirim |
message.delivered | Pesan sampai ke penerima |
message.read | Pesan dibaca penerima |
message.failed | Pengiriman pesan gagal |
Format Payload
{
"event": "message.received",
"timestamp": "2025-01-07T10:30:00Z",
"data": {
"message_id": "msg_xyz789",
"customer_id": "clxxx1234567890",
"customer_phone": "628123456789",
"channel": "whatsapp",
"direction": "inbound",
"message_type": "text",
"content": "Hello, I need help!",
"media_url": null
}
}
Untuk pesan media (image/video/audio/document/sticker), payload juga
menyertakan field media_status:
"ok" (media_url adalah
URL re-host yang bisa dipakai), "download_failed"
(kami gagal me-re-host media), atau "unsupported"
(Meta tidak mengirim media yang bisa diunduh, mis. pesan view-once). Hanya percayai
media_url jika media_status bernilai "ok". Field ini tidak ada untuk pesan non-media.
Keamanan Webhook
Verifikasi signature webhook untuk memastikan request benar-benar berasal dari Api.co.id.
Semua request webhook menyertakan signature HMAC-SHA256 di
header X-Webhook-Signature.
Verifikasi signature ini di server Anda untuk memastikan request benar-benar datang dari Api.co.id.
Penting: Selalu verifikasi signature webhook sebelum memproses event. Tanpa verifikasi, siapa pun bisa mengirim payload webhook palsu ke server Anda.
Cara Kerja Signature
- Saat Anda membuat endpoint webhook, kami membuat
webhook_secretyang unik - Untuk setiap event, kami menghitung:
HMAC-SHA256(payload, webhook_secret) - Signature dalam format hex dikirim di header
X-Webhook-Signature - Server Anda menghitung ulang signature dengan secret yang sama lalu membandingkannya
- Jika cocok, request tersebut asli — proses event-nya
- Jika tidak cocok, kembalikan
403 Forbidden
Di Mana Menemukan Webhook Secret
Buka Developers → Webhooks
lalu klik endpoint webhook Anda. secret hanya ditampilkan sekali saat Anda membuat webhook — simpan dengan aman di environment variable (mis. WEBHOOK_SECRET).
Contoh Verifikasi
- Node.js (Express)
- Python (Flask)
- PHP (Laravel)
- Go
const crypto = require('crypto');
function verifyWebhookSignature(rawBody, signature, secret) {
if (!signature) return false;
const expected = crypto
.createHmac('sha256', secret)
.update(rawBody)
.digest('hex');
// Use timingSafeEqual to prevent timing attacks
try {
return crypto.timingSafeEqual(
Buffer.from(expected, 'hex'),
Buffer.from(signature, 'hex')
);
} catch {
return false;
}
}
// IMPORTANT: use express.raw() to get the unparsed body
app.post(
'/webhook',
express.raw({ type: 'application/json' }),
(req, res) => {
const signature = req.headers['x-webhook-signature'];
const rawBody = req.body.toString('utf8');
if (!verifyWebhookSignature(rawBody, signature, process.env.WEBHOOK_SECRET)) {
return res.status(403).json({ error: 'Invalid signature' });
}
const event = JSON.parse(rawBody);
// Process the event based on event.event_type
console.log('Received event:', event.event_type);
// Always return 200 quickly to avoid retries
res.status(200).send('OK');
}
);
import hmac
import hashlib
from flask import Flask, request, abort
import os
app = Flask(__name__)
WEBHOOK_SECRET = os.environ['WEBHOOK_SECRET']
def verify_signature(raw_body: bytes, signature: str, secret: str) -> bool:
if not signature:
return False
expected = hmac.new(
secret.encode('utf-8'),
raw_body,
hashlib.sha256
).hexdigest()
# Use compare_digest to prevent timing attacks
return hmac.compare_digest(expected, signature)
@app.route('/webhook', methods=['POST'])
def webhook():
signature = request.headers.get('X-Webhook-Signature', '')
raw_body = request.get_data() # IMPORTANT: get raw bytes, not parsed JSON
if not verify_signature(raw_body, signature, WEBHOOK_SECRET):
abort(403, description='Invalid signature')
event = request.get_json()
# Process the event
print(f"Received event: {event['event_type']}")
return 'OK', 200
<?php
// In a controller method or middleware
public function handleWebhook(\Illuminate\Http\Request $request)
{
$signature = $request->header('X-Webhook-Signature');
$rawBody = $request->getContent(); // IMPORTANT: raw body, not parsed
$secret = env('WEBHOOK_SECRET');
if (!$this->verifySignature($rawBody, $signature, $secret)) {
return response()->json(['error' => 'Invalid signature'], 403);
}
$event = json_decode($rawBody, true);
// Process event based on $event['event_type']
return response('OK', 200);
}
private function verifySignature(string $body, ?string $signature, string $secret): bool
{
if (empty($signature)) return false;
$expected = hash_hmac('sha256', $body, $secret);
// Use hash_equals to prevent timing attacks
return hash_equals($expected, $signature);
}
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"io"
"net/http"
"os"
)
func verifySignature(body []byte, signature, secret string) bool {
if signature == "" {
return false
}
mac := hmac.New(sha256.New, []byte(secret))
mac.Write(body)
expected := hex.EncodeToString(mac.Sum(nil))
// hmac.Equal uses constant-time comparison
return hmac.Equal([]byte(expected), []byte(signature))
}
func webhookHandler(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "Failed to read body", http.StatusBadRequest)
return
}
defer r.Body.Close()
signature := r.Header.Get("X-Webhook-Signature")
secret := os.Getenv("WEBHOOK_SECRET")
if !verifySignature(body, signature, secret) {
http.Error(w, "Invalid signature", http.StatusForbidden)
return
}
// Parse and process event
// ...
w.WriteHeader(http.StatusOK)
w.Write([]byte("OK"))
}
func main() {
http.HandleFunc("/webhook", webhookHandler)
http.ListenAndServe(":8080", nil)
}
Kesalahan Umum
❌ Membandingkan JSON yang sudah di-parse, bukan raw body
Signature dihitung dari raw bytes PERSIS seperti yang kami kirim. Jika Anda mem-parse body ke JSON lalu men-stringify ulang, perbedaan format sekecil apa pun (whitespace, urutan key) akan membuat signature tidak cocok. Selalu gunakan raw body.
❌ Memakai == atau === untuk membandingkan signature
Gunakan perbandingan constant-time (timingSafeEqual,
hmac.compare_digest,
hash_equals,
hmac.Equal) untuk mencegah timing attack.
❌ Menyimpan webhook secret di kode
Selalu simpan webhook secret di environment variable, jangan pernah meng-commit-nya ke git.
❌ Merespons terlalu lambat
Kembalikan 200 OK dalam 5 detik.
Untuk pemrosesan berat, masukkan event ke antrean dan proses secara asinkron — jika tidak, kami akan mengirim ulang (retry).
Uji Verifikasi Anda
Setelah verifikasi signature diimplementasikan:
- Buka Developers → Webhooks
- Klik endpoint webhook Anda
- Klik tombol "Test Webhook"
- Kami akan mengirim event uji dengan signature yang valid — server Anda harus mengembalikan
200 OK
Daftar Webhook
Lihat daftar endpoint webhook Anda beserta statusnya. Gunakan ini untuk mengecek apakah sebuah webhook dinonaktifkan otomatis setelah gagal kirim berulang kali.
GET /api/v1/public/webhooks
Webhook otomatis dinonaktifkan setelah 10 kali gagal kirim berturut-turut (satu
pengiriman sukses akan me-reset hitungannya). Saat dinonaktifkan,
is_active bernilai
false dan
disabled_at /
disable_reason terisi. Aktifkan kembali
lewat endpoint Aktifkan Webhook.
Contoh Request
- cURL
- JavaScript
- Python
curl -X GET "https://chat.api.co.id/api/v1/public/webhooks" \
-H "Authorization: Bearer YOUR_API_KEY"
const response = await fetch("https://chat.api.co.id/api/v1/public/webhooks", {
headers: { "Authorization": "Bearer YOUR_API_KEY" }
});
const { data } = await response.json();
const disabled = data.filter(w => !w.is_active);
import requests
res = requests.get(
"https://chat.api.co.id/api/v1/public/webhooks",
headers={"Authorization": "Bearer YOUR_API_KEY"},
)
webhooks = res.json()["data"]
Response
{
"success": true,
"data": [
{
"id": "cmxxx1234567890",
"name": "n8n production",
"url": "https://example.com/webhook",
"events": ["message.received"],
"channels": ["whatsapp"],
"is_active": false,
"status": "disabled",
"failure_count": 10,
"last_failed_at": "2026-06-24T03:00:00.000Z",
"disabled_at": "2026-06-24T03:05:00.000Z",
"disable_reason": "Auto-disabled after 10 consecutive delivery failures",
"created_at": "2026-06-01T00:00:00.000Z",
"updated_at": "2026-06-24T03:05:00.000Z"
}
]
}
Detail Webhook
Ambil status satu endpoint webhook berdasarkan ID.
GET /api/v1/public/webhooks/:id
URL Parameter
| Parameter | Tipe | Wajib | Keterangan |
|---|---|---|---|
id | string | ya | ID endpoint webhook (dari Daftar Webhook). |
Contoh Request
curl -X GET "https://chat.api.co.id/api/v1/public/webhooks/WEBHOOK_ID" \
-H "Authorization: Bearer YOUR_API_KEY"
Aktifkan Webhook
Aktifkan kembali endpoint webhook yang dinonaktifkan (mis. dinonaktifkan otomatis setelah gagal kirim berulang kali). Sekaligus me-reset failure_count dan mengosongkan disabled_at / disable_reason.
POST /api/v1/public/webhooks/:id/enable
URL Parameter
| Parameter | Tipe | Wajib | Keterangan |
|---|---|---|---|
id | string | ya | ID endpoint webhook yang akan diaktifkan. |
Contoh Request
- cURL
- JavaScript
- Python
curl -X POST "https://chat.api.co.id/api/v1/public/webhooks/WEBHOOK_ID/enable" \
-H "Authorization: Bearer YOUR_API_KEY"
const response = await fetch(
"https://chat.api.co.id/api/v1/public/webhooks/" + webhookId + "/enable",
{ method: "POST", headers: { "Authorization": "Bearer YOUR_API_KEY" } }
);
const { data } = await response.json();
console.log(data.is_active); // true
import requests
res = requests.post(
f"https://chat.api.co.id/api/v1/public/webhooks/{webhook_id}/enable",
headers={"Authorization": "Bearer YOUR_API_KEY"},
)
print(res.json())
Response
{
"success": true,
"data": {
"id": "cmxxx1234567890",
"name": "n8n production",
"url": "https://example.com/webhook",
"events": ["message.received"],
"channels": ["whatsapp"],
"is_active": true,
"status": "active",
"failure_count": 0,
"last_failed_at": "2026-06-24T03:00:00.000Z",
"disabled_at": null,
"disable_reason": null,
"created_at": "2026-06-01T00:00:00.000Z",
"updated_at": "2026-06-24T04:00:00.000Z"
},
"message": "Webhook endpoint enabled. Failure count has been reset."
}
Coba langsung
Tekan Try it lalu tempel API key Anda (kc_live_...) ke kolom API key (Bearer). Dapatkan key dari API Keys. Request dijalankan ke akun live Anda, jadi endpoint yang mengirim, membuat, atau menghapus akan memengaruhi data sungguhan.
- GET/api/v1/public/webhooksList Webhooks
- GET/api/v1/public/webhooks/{id}Get Webhook
- POST/api/v1/public/webhooks/{id}/enableEnable Webhook