Menerima webhook
Webhook memberi tahu server Anda saat sesuatu terjadi, jadi Anda tidak perlu melakukan polling. Webhook memerlukan paket yang menyertakan webhook (Starter ke atas).
Tambah endpoint
Di konsol, buka Developer → Webhook → Tambah endpoint:
- URL: HTTPS di internet publik. Alamat di jaringan privat,
localhost, dan sejenisnya ditolak, dan alamat hasil resolusi nama domain diperiksa ulang di setiap pengiriman. - Peristiwa: event yang Anda inginkan (lihat di bawah).
- Salin rahasia penandatanganan (
whsec_…). Rahasia hanya ditampilkan sekali; Rahasia baru menggantinya (rahasia lama langsung berhenti berfungsi).
Maksimal 10 endpoint per ruang kerja. Kirim peristiwa uji mengirim event ping, apa pun event yang dilanggan endpoint tersebut.
Event
| Event | Kapan | data |
|---|---|---|
conversation.created | Pengunjung memulai percakapan | { conversation } |
conversation.assigned | Percakapan diberikan atau dialihkan | { conversation } |
conversation.closed | Percakapan berakhir (oleh agen, lewat API, atau setelah 30 menit tidak ada aktivitas) | { conversation } |
message.created | Pengunjung, agen, atau integrasi mengirim pesan (catatan internal tidak dikirim) | { message, conversation } |
message.updated | Agen mengedit balasan yang dikirimnya (agen bisa mengedit atau menarik balasan dalam 5 menit) | { message, conversation } |
message.deleted | Agen menarik balasan yang dikirimnya: hapus salinan Anda | { message: { id, conversation_id }, conversation } |
offline_message.created | Pengunjung meninggalkan pesan saat tidak ada yang tersedia | { offline_message } |
visitor.identified | Nama, email, atau nomor telepon pengunjung menjadi diketahui | { visitor } |
lead.updated | Status, penanggung jawab, waktu tindak lanjut, atau nilai sebuah prospek berubah | { conversation, lead } |
visitor.deleted | Pemilik atau admin menghapus semua data tentang seorang pengunjung (permintaan data); hapus juga salinan Anda | { visitor: { id, site_id } } |
Setiap pengiriman adalah POST dengan body JSON seperti:
{
"id": "0192f2c1-7d3a-7c1e-9b0e-4f5a6b7c8d9e",
"type": "message.created",
"created_at": "2026-10-25T12:00:00Z",
"data": {
"message": { "id": "0192f2c1-…", "sender": { "type": "visitor" }, "content_type": "text", "text": "Do you ship to Spain?", "created_at": "…" },
"conversation": { "id": "0192f2b8-…", "site_id": "…", "visitor_id": "…", "status": "waiting", "assigned_member_id": null, "started_at": "…" }
}
}dan header berikut:
| Header | |
|---|---|
X-Signature | t=<unix seconds>,v1=<signature> (lihat di bawah) |
X-SealReply-Event | Tipe event |
X-SealReply-Event-Id | id event, sama pada percobaan ulang dan pengiriman ulang |
X-SealReply-Delivery | ID percobaan pengiriman ini (untuk dukungan) |
Verifikasi tanda tangan
v1 adalah HMAC-SHA256 dalam format hex dari "<t>.<raw body>" dengan rahasia Anda. Periksa terhadap body mentah, sebelum mem-parsing JSON, dan tolak timestamp yang selisihnya lebih dari 5 menit:
// Verify a SealReply webhook in Node.js (18+), with no dependencies.
import crypto from "node:crypto";
const TOLERANCE_SECONDS = 5 * 60;
/**
* header: the X-Signature request header ("t=1761390000,v1=5f2b…").
* rawBody: the request body exactly as received (a Buffer or string), not re-encoded JSON.
*/
export function verifySignature(secret, header, rawBody, now = Date.now() / 1000) {
const parts = Object.fromEntries((header ?? "").split(",").map((p) => p.split("=", 2)));
const t = Number(parts.t);
if (!parts.v1 || !Number.isInteger(t) || Math.abs(now - t) > TOLERANCE_SECONDS) return false;
const expected = crypto.createHmac("sha256", secret).update(`${parts.t}.`).update(rawBody).digest("hex");
const a = Buffer.from(expected);
const b = Buffer.from(parts.v1);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
// Try it: SEALREPLY_WEBHOOK_SECRET=whsec_… SIGNATURE='t=…,v1=…' node verify-webhook.mjs < body.json
if (import.meta.url === `file://${process.argv[1]}`) {
const chunks = [];
for await (const chunk of process.stdin) chunks.push(chunk);
const ok = verifySignature(process.env.SEALREPLY_WEBHOOK_SECRET, process.env.SIGNATURE, Buffer.concat(chunks));
console.log(ok ? "ok" : "invalid");
process.exit(ok ? 0 : 1);
}"""Verify a SealReply webhook in Python 3.10+, with the standard library only."""
import hashlib
import hmac
import os
import sys
import time
TOLERANCE_SECONDS = 5 * 60
def verify_signature(secret: str, header: str, raw_body: bytes, now: float | None = None) -> bool:
"""header: the X-Signature request header; raw_body: the request body as received."""
parts = dict(p.split("=", 1) for p in (header or "").split(",") if "=" in p)
try:
t = int(parts["t"])
signature = parts["v1"]
except (KeyError, ValueError):
return False
if abs((now or time.time()) - t) > TOLERANCE_SECONDS:
return False
expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature)
# Try it: SEALREPLY_WEBHOOK_SECRET=whsec_… SIGNATURE='t=…,v1=…' python3 verify_webhook.py < body.json
if __name__ == "__main__":
ok = verify_signature(os.environ["SEALREPLY_WEBHOOK_SECRET"], os.environ["SIGNATURE"], sys.stdin.buffer.read())
print("ok" if ok else "invalid")
sys.exit(0 if ok else 1)<?php
// Verify a SealReply webhook in PHP 7.4+, with no extensions beyond the defaults.
const SEALREPLY_TOLERANCE_SECONDS = 300;
/**
* $header: the X-Signature request header ($_SERVER['HTTP_X_SIGNATURE']).
* $rawBody: the request body as received (file_get_contents('php://input')).
*/
function sealreply_verify_signature(string $secret, string $header, string $rawBody, ?int $now = null): bool
{
$parts = [];
foreach (explode(',', $header) as $part) {
$pair = explode('=', $part, 2);
if (count($pair) === 2) {
$parts[$pair[0]] = $pair[1];
}
}
if (!isset($parts['t'], $parts['v1']) || !ctype_digit($parts['t'])) {
return false;
}
if (abs(($now ?? time()) - (int) $parts['t']) > SEALREPLY_TOLERANCE_SECONDS) {
return false;
}
$expected = hash_hmac('sha256', $parts['t'] . '.' . $rawBody, $secret);
return hash_equals($expected, $parts['v1']);
}
// Try it: SEALREPLY_WEBHOOK_SECRET=whsec_… SIGNATURE='t=…,v1=…' php verify-webhook.php < body.json
if (PHP_SAPI === 'cli' && realpath($argv[0] ?? '') === __FILE__) {
$ok = sealreply_verify_signature(getenv('SEALREPLY_WEBHOOK_SECRET'), getenv('SIGNATURE'), stream_get_contents(STDIN));
echo $ok ? "ok\n" : "invalid\n";
exit($ok ? 0 : 1);
}//go:build ignore
// Verify a SealReply webhook in Go, with the standard library only.
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"os"
"strconv"
"strings"
"time"
)
const tolerance = 5 * time.Minute
// VerifySignature checks the X-Signature header against the raw request body.
func VerifySignature(secret, header string, rawBody []byte, now time.Time) bool {
parts := map[string]string{}
for _, p := range strings.Split(header, ",") {
if k, v, ok := strings.Cut(p, "="); ok {
parts[k] = v
}
}
t, err := strconv.ParseInt(parts["t"], 10, 64)
if err != nil || parts["v1"] == "" {
return false
}
if d := now.Sub(time.Unix(t, 0)); d > tolerance || d < -tolerance {
return false
}
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(parts["t"] + "."))
mac.Write(rawBody)
return hmac.Equal([]byte(hex.EncodeToString(mac.Sum(nil))), []byte(parts["v1"]))
}
// Try it: SEALREPLY_WEBHOOK_SECRET=whsec_… SIGNATURE='t=…,v1=…' go run verify_webhook.go < body.json
func main() {
body, _ := io.ReadAll(os.Stdin)
if VerifySignature(os.Getenv("SEALREPLY_WEBHOOK_SECRET"), os.Getenv("SIGNATURE"), body, time.Now()) {
fmt.Println("ok")
return
}
fmt.Println("invalid")
os.Exit(1)
}Rangkaian pengujian kami menandatangani body persis seperti pengiriman ditandatangani dan menjalankan setiap contoh ini, jadi semuanya berfungsi persis seperti yang tertulis.
Jawab dengan cepat, tangani pengulangan
- Kembalikan 2xx apa pun dalam 10 detik. Selain itu dihitung sebagai kegagalan. Kerjakan proses yang lambat setelah menjawab (masukkan event ke antrean).
- Kegagalan dicoba ulang setelah 1 menit, 5 menit, 30 menit, 2 jam, 6 jam, dan 24 jam. Endpoint yang gagal selama 3 hari berturut-turut dinonaktifkan; aktifkan lagi di konsol setelah diperbaiki. Endpoint juga dinonaktifkan saat rekan tim yang menambahkannya keluar dari ruang kerja, dinonaktifkan, atau diubah menjadi agen.
- Sesekali, event yang sama datang dua kali, dan event bisa datang tidak berurutan. Hilangkan duplikat berdasarkan
idevent, dan gunakancreated_atsertastatuspercakapan, bukan urutan kedatangan.
Log pengiriman
Developer → Webhook → Log pengiriman menampilkan setiap pengiriman selama 30 hari: status, kode respons atau error, durasi, percobaan ulang berikutnya, dan body yang dikirim. Kirim ulang mengirim sebuah event lagi sekarang, dengan id yang sama.