Webhook Debugger
Inspect SendiMessage webhook events and validate your integration before handling them in production.
Use sanitized webhook examples when possible. Webhook payloads may contain customer data.
Paste or load an event, then press Validate Event.
The raw request body used for verification is the exact text in the JSON box above.
Verified signature-verification code
Derived directly from the real signing algorithm (WebhookSigner.php) — never a real secret, always the WEBHOOK_SECRET placeholder.
import crypto from "crypto";import express from "express";function verifySendiMessageSignature(header, rawBody, secret, toleranceSeconds = 300) {const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));const timestamp = Number(parts.t);if (!timestamp || Math.abs(Date.now() / 1000 - timestamp) > toleranceSeconds) return false;const expected = crypto.createHmac("sha256", secret).update(`${timestamp}.${rawBody}`).digest("hex");return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1 ?? ""));}const app = express();app.post("/webhooks/sendimessage", express.raw({ type: "application/json" }), (req, res) => {const ok = verifySendiMessageSignature(req.get("SendiMessage-Signature") ?? "",req.body, // raw Buffer — verify before parsingprocess.env.WEBHOOK_SECRET,);if (!ok) return res.sendStatus(401);const event = JSON.parse(req.body);res.sendStatus(200); // respond fast; process asynchronously});
import hashlibimport hmacimport timedef verify_sendimessage_signature(header: str, raw_body: bytes, secret: str, tolerance_seconds: int = 300) -> bool:parts = dict(p.split("=", 1) for p in header.split(",") if "=" in p)timestamp = int(parts.get("t", 0))if not timestamp or abs(time.time() - timestamp) > tolerance_seconds:return Falsesigned_payload = f"{timestamp}.".encode() + raw_bodyexpected = hmac.new(secret.encode(), signed_payload, hashlib.sha256).hexdigest()return hmac.compare_digest(expected, parts.get("v1", ""))# Flask example# ok = verify_sendimessage_signature(# request.headers.get("SendiMessage-Signature", ""),# request.get_data(), # exact raw bytes — do not re-serialize# os.environ["WEBHOOK_SECRET"],# )
function verify_sendimessage_signature(string $header, string $rawBody, string $secret, int $toleranceSeconds = 300): bool{$parts = [];foreach (explode(',', $header) as $piece) {[$k, $v] = array_pad(explode('=', trim($piece), 2), 2, null);$parts[$k] = $v;}$timestamp = isset($parts['t']) ? (int) $parts['t'] : 0;if ($timestamp <= 0 || abs(time() - $timestamp) > $toleranceSeconds) {return false;}$expected = hash_hmac('sha256', $timestamp.'.'.$rawBody, $secret);return hash_equals($expected, $parts['v1'] ?? '');}// $rawBody = file_get_contents('php://input'); // exact raw bytes// $ok = verify_sendimessage_signature($_SERVER['HTTP_SENDIMESSAGE_SIGNATURE'] ?? '', $rawBody, getenv('WEBHOOK_SECRET'));
package mainimport ("crypto/hmac""crypto/sha256""encoding/hex""fmt""math""strconv""strings""time")func verifySendiMessageSignature(header string, rawBody []byte, secret string, toleranceSeconds int64) bool {parts := map[string]string{}for _, piece := range strings.Split(header, ",") {kv := strings.SplitN(strings.TrimSpace(piece), "=", 2)if len(kv) == 2 {parts[kv[0]] = kv[1]}}ts, err := strconv.ParseInt(parts["t"], 10, 64)if err != nil || ts <= 0 {return false}if math.Abs(float64(time.Now().Unix()-ts)) > float64(toleranceSeconds) {return false}mac := hmac.New(sha256.New, []byte(secret))mac.Write([]byte(fmt.Sprintf("%d.%s", ts, rawBody)))expected := hex.EncodeToString(mac.Sum(nil))return hmac.Equal([]byte(expected), []byte(parts["v1"]))}// ok := verifySendiMessageSignature(r.Header.Get("SendiMessage-Signature"), rawBody, os.Getenv("WEBHOOK_SECRET"), 300)
How an incoming message reaches your app
What your application typically does with a receive event:
- Verify the signature before trusting the payload.
- Identify the conversation — e.g. by the sender's number and the line it arrived on.
- Store or process the message in your own system of record.
- Update whatever workflow depends on it — a CRM timeline, a support ticket, an agent loop.
Delivery / status event lifecycle
An outbound event fires once a sent message reaches a terminal state. The event's data.message.status carries the raw internal pipeline status — not the simplified public enum GET /status returns.
SendiMessage does not currently support read receipts — there is no "read" status.