Skip to content

Webhook

Terima notifikasi real-time dari gateway: pesan masuk, status terkirim, dan status dibaca.

Cara Kerja

Gateway melakukan HTTP POST ke URL endpoint Anda setiap kali ada event dari WhatsApp. Request berisi payload JSON beserta signature HMAC-SHA256 untuk memverifikasi keasliannya.

Event WhatsApp → [Gateway] → POST ke endpoint Anda → Anda proses event

Jenis Event

Semua event dikirim ke webhook yang sama. Bedakan jenis event dari field kind:

kindKapan dikirim
messageAda pesan masuk ke nomor WhatsApp Anda
receiptPesan yang Anda kirim sudah diterima atau dibaca penerima

Mendaftarkan Endpoint

bash
curl -X POST https://api.example.com/v1/webhooks \
  -H "Authorization: Bearer wag_xxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://yourserver.com/webhook/wa"}'

Response (201 Created):

json
{
  "id": "wh_1a2b3c...",
  "url": "https://yourserver.com/webhook/wa",
  "secret": "whsec_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  "active": true,
  "created_at": "2026-06-25T10:00:00Z"
}

Simpan secret dengan aman

secret hanya ditampilkan sekali saat pendaftaran. Simpan di environment variable atau secret manager — Anda akan membutuhkannya untuk verifikasi signature setiap request yang masuk.


Format Payload

Event: Pesan Masuk (kind: "message")

json
{
  "kind": "message",
  "message_id": "msg_7f3a...",
  "session_id": "6281200000000@s.whatsapp.net",
  "sender_jid": "628111111111@s.whatsapp.net",
  "chat_jid": "628111111111@s.whatsapp.net",
  "is_group": false,
  "type": "text",
  "text": "Halo, saya ingin bertanya...",
  "timestamp_unix": 1719340800
}
FieldTipeKeterangan
kindstringSelalu "message" untuk pesan masuk
message_idstringID unik pesan, gunakan untuk dedup di sisi Anda
session_idstringJID nomor WhatsApp Anda yang menerima pesan
sender_jidstringJID pengirim
chat_jidstringJID chat (sama dengan sender_jid untuk DM, berbeda untuk grup)
is_groupbooltrue jika pesan dari grup WhatsApp
typestringTipe pesan: text atau other (media, stiker, dll)
textstringIsi pesan teks (kosong jika type bukan text)
timestamp_unixintWaktu pesan dalam Unix timestamp (detik)

Event: Status Pengiriman (kind: "receipt")

Dikirim saat penerima menerima atau membaca pesan yang Anda kirim lewat gateway.

json
{
  "kind": "receipt",
  "session_id": "6281200000000@s.whatsapp.net",
  "request_id": "order-12345-notif-1",
  "whatsapp_message_id": "3EB0A1B2C3D4E5F6",
  "recipient_jid": "628111111111@s.whatsapp.net",
  "receipt_type": "read",
  "timestamp_unix": 1719340900
}
FieldTipeKeterangan
kindstringSelalu "receipt"
session_idstringJID nomor WhatsApp yang mengirim pesan asli
request_idstringIdempotency-Key dari request kirim pesan (kosong jika worker sempat restart)
whatsapp_message_idstringID pesan di WhatsApp
recipient_jidstringJID penerima yang membaca/menerima pesan
receipt_typestringdelivered (centang dua ✓✓), read (centang biru ✓✓), atau played (media view-once)
timestamp_unixintWaktu receipt dalam Unix timestamp (detik)

Contoh implementasi — handle dua jenis event:

javascript
app.post('/webhook/wa', (req, res) => {
  // ... verifikasi signature dulu (lihat bagian Verifikasi di bawah) ...

  const payload = JSON.parse(req.body)

  if (payload.kind === 'receipt') {
    const { request_id, recipient_jid, receipt_type } = payload
    console.log(`Pesan ${request_id} → ${receipt_type} oleh ${recipient_jid}`)
    // Update status di database Anda
  } else {
    // kind === 'message' (atau tidak ada kind untuk payload lama)
    console.log(`Pesan dari ${payload.sender_jid}: ${payload.text}`)
  }

  res.json({ ok: true })
})

Header Request

Setiap request webhook menyertakan header berikut:

HeaderContoh Nilai
Content-Typeapplication/json
X-Webhook-IDwh_1a2b3c...
X-Message-IDmsg_7f3a...
X-Timestamp1719340800
X-Webhook-Signaturet=1719340800,v1=abc123...

Verifikasi Signature

Selalu verifikasi signature sebelum memproses payload. Ini memastikan request benar-benar dari gateway kami dan payload tidak dimodifikasi di tengah jalan.

Format Signature

Header X-Webhook-Signature berformat:

t=<unix_timestamp>,v1=<hex_hmac_sha256>

Cara Verifikasi

  1. Ambil timestamp t dari header
  2. Buat string: <t>.<raw_body> (timestamp + titik + body request mentah)
  3. Hitung HMAC-SHA256 dari string tersebut menggunakan secret
  4. Bandingkan hasilnya dengan nilai v1 secara constant-time
  5. Tolak jika selisih waktu antara t dan waktu sekarang lebih dari 5 menit (mencegah replay attack)

Jangan skip verifikasi

Tanpa verifikasi, siapapun bisa mengirim request palsu ke endpoint Anda dan menyuntikkan data berbahaya ke sistem Anda.


Contoh Implementasi

php
<?php

function verifyWebhookSignature(string $secret, string $rawBody, string $signatureHeader): bool
{
    // Parse "t=1234,v1=abc..."
    $parts = [];
    foreach (explode(',', $signatureHeader) as $part) {
        [$k, $v] = explode('=', $part, 2);
        $parts[$k] = $v;
    }

    if (empty($parts['t']) || empty($parts['v1'])) {
        return false;
    }

    // Tolak jika timestamp terlalu lama (>5 menit)
    if (abs(time() - (int)$parts['t']) > 300) {
        return false;
    }

    // Hitung HMAC
    $expected = hash_hmac('sha256', $parts['t'] . '.' . $rawBody, $secret);

    // Bandingkan constant-time untuk mencegah timing attack
    return hash_equals($expected, $parts['v1']);
}

// --- Handler endpoint webhook ---

$secret  = getenv('WEBHOOK_SECRET'); // whsec_xxx dari saat pendaftaran
$rawBody = file_get_contents('php://input');
$sigHeader = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '';

if (!verifyWebhookSignature($secret, $rawBody, $sigHeader)) {
    http_response_code(401);
    exit('Signature tidak valid');
}

$payload = json_decode($rawBody, true);

// Proses pesan
if ($payload['type'] === 'text') {
    $pengirim = $payload['sender_jid'];
    $pesan    = $payload['text'];
    error_log("Pesan dari {$pengirim}: {$pesan}");
}

// Selalu balas 200 OK agar tidak di-retry
http_response_code(200);
echo json_encode(['ok' => true]);
js
import crypto from 'crypto'
import express from 'express'

const app = express()

// Penting: gunakan raw body untuk verifikasi signature
app.use('/webhook/wa', express.raw({ type: 'application/json' }))

function verifySignature(secret, rawBody, signatureHeader) {
  const parts = Object.fromEntries(
    signatureHeader.split(',').map(p => p.split('='))
  )

  if (!parts.t || !parts.v1) return false

  // Tolak jika timestamp terlalu lama (>5 menit)
  if (Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) return false

  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${parts.t}.${rawBody}`)
    .digest('hex')

  // Bandingkan constant-time
  return crypto.timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(parts.v1)
  )
}

app.post('/webhook/wa', (req, res) => {
  const secret    = process.env.WEBHOOK_SECRET
  const rawBody   = req.body
  const sigHeader = req.headers['x-webhook-signature'] ?? ''

  if (!verifySignature(secret, rawBody, sigHeader)) {
    return res.status(401).json({ error: 'Signature tidak valid' })
  }

  const payload = JSON.parse(rawBody)

  if (payload.type === 'text') {
    console.log(`Pesan dari ${payload.sender_jid}: ${payload.text}`)
  }

  // Selalu balas 200 OK agar tidak di-retry
  res.json({ ok: true })
})

app.listen(3000)
python
import hashlib
import hmac
import time
import os
from flask import Flask, request, jsonify, abort

app = Flask(__name__)

def verify_signature(secret: str, raw_body: bytes, signature_header: str) -> bool:
    parts = dict(p.split('=', 1) for p in signature_header.split(','))

    if 't' not in parts or 'v1' not in parts:
        return False

    # Tolak jika timestamp terlalu lama (>5 menit)
    if abs(time.time() - int(parts['t'])) > 300:
        return False

    payload = f"{parts['t']}.".encode() + raw_body
    expected = hmac.new(secret.encode(), payload, hashlib.sha256).hexdigest()

    # Bandingkan constant-time
    return hmac.compare_digest(expected, parts['v1'])

@app.route('/webhook/wa', methods=['POST'])
def webhook():
    secret      = os.environ['WEBHOOK_SECRET']
    raw_body    = request.get_data()
    sig_header  = request.headers.get('X-Webhook-Signature', '')

    if not verify_signature(secret, raw_body, sig_header):
        abort(401, 'Signature tidak valid')

    payload = request.get_json(force=True)

    if payload.get('type') == 'text':
        print(f"Pesan dari {payload['sender_jid']}: {payload['text']}")

    # Selalu balas 200 OK agar tidak di-retry
    return jsonify({'ok': True})

Retry & Dead Letter

Jika endpoint Anda merespons dengan status non-2xx atau koneksi timeout, sistem akan mencoba kirim ulang secara otomatis dengan interval yang semakin panjang (exponential backoff):

PercobaanJeda sebelum retry
1 (asli)
2~30 detik
3~5 menit
4~30 menit

Setelah semua percobaan gagal, pesan dipindah ke dead letter queue untuk investigasi manual.

Rekomendasi:

  • Selalu balas 200 OK sesegera mungkin, lalu proses payload secara asinkron di background job.
  • Implementasikan dedup di sisi Anda menggunakan message_id — pesan yang sama bisa diterima lebih dari sekali saat ada retry.

Kelola Webhook

Lihat daftar webhook:

bash
curl https://api.example.com/v1/webhooks \
  -H "Authorization: Bearer wag_xxxxxxxxxxxx"

Hapus webhook:

bash
curl -X DELETE https://api.example.com/v1/webhooks/wh_1a2b3c... \
  -H "Authorization: Bearer wag_xxxxxxxxxxxx"

Dokumentasi WhatsApp Gateway