Skip to content

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 ​

EventKapandata
conversation.createdPengunjung memulai percakapan{ conversation }
conversation.assignedPercakapan diberikan atau dialihkan{ conversation }
conversation.closedPercakapan berakhir (oleh agen, lewat API, atau setelah 30 menit tidak ada aktivitas){ conversation }
message.createdPengunjung, agen, atau integrasi mengirim pesan (catatan internal tidak dikirim){ message, conversation }
message.updatedAgen mengedit balasan yang dikirimnya (agen bisa mengedit atau menarik balasan dalam 5 menit){ message, conversation }
message.deletedAgen menarik balasan yang dikirimnya: hapus salinan Anda{ message: { id, conversation_id }, conversation }
offline_message.createdPengunjung meninggalkan pesan saat tidak ada yang tersedia{ offline_message }
visitor.identifiedNama, email, atau nomor telepon pengunjung menjadi diketahui{ visitor }
lead.updatedStatus, penanggung jawab, waktu tindak lanjut, atau nilai sebuah prospek berubah{ conversation, lead }
visitor.deletedPemilik 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:

json
{
  "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-Signaturet=<unix seconds>,v1=<signature> (lihat di bawah)
X-SealReply-EventTipe event
X-SealReply-Event-Idid event, sama pada percobaan ulang dan pengiriman ulang
X-SealReply-DeliveryID 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:

js
// 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);
}
py
"""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
<?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
//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 id event, dan gunakan created_at serta status percakapan, 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.