We use optional, privacy-safe analytics to understand which pages help visitors. Nothing optional runs until you choose. Privacy policy

Skip to content
Developer Tools

Webhook Debugger

Inspect SendiMessage webhook events and validate your integration before handling them in production.

Payloads are processed locally in your browser and are not submitted to SendiMessage. JSON, secrets, and signatures you enter here are kept only in this page's memory — nothing is written to storage, logs, or analytics, and everything clears when you leave the page or press Clear Data.

Use sanitized webhook examples when possible. Webhook payloads may contain customer data.

Validation

Paste or load an event, then press Validate Event.

Signature Debugger
Verify the signature using the exact raw request body before JSON parsing — a re-serialized copy of the JSON will not match, even if the content is identical.

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.

Node.js
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 parsing
process.env.WEBHOOK_SECRET,
);
if (!ok) return res.sendStatus(401);
const event = JSON.parse(req.body);
res.sendStatus(200); // respond fast; process asynchronously
});
Python
import hashlib
import hmac
import time
def 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 False
signed_payload = f"{timestamp}.".encode() + raw_body
expected = 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"],
# )
PHP
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'));
Go
package main
import (
"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

CustomerManaged LineSendiMessageWebhookYour Application

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.

queuedroutingsendingsentdeliveredfailedundeliveredreceivedcancelled

SendiMessage does not currently support read receipts — there is no "read" status.