SignEnvoy docs

Verifying webhook signatures

Every delivery carries X-SignEnvoy-Signature, an HMAC over the exact bytes of the request body using the endpoint's secret (the sewh_... value shown once when the endpoint was created or its secret was rotated).

The scheme

X-SignEnvoy-Signature: t=<unix>,v1=hex(hmac_sha256(secret, "{t}.{body}"))
X-SignEnvoy-Signature: t=1756000000,v1=<new secret>,v1=<previous secret>

Verify

  1. Read the raw request body as bytes. Do not parse and re-serialize it first: the body is compact JSON with sorted keys, and the signature covers those exact bytes.
  2. Split the header on ,; take t and every v1.
  3. Compute HMAC-SHA256(secret, t + "." + body) and compare it to each v1 with a constant-time comparison. Accept on the first match.
  4. Treat id in the body as the deduplication key: the same event is redelivered with the same id and a new signature.
import hashlib, hmac

def verify(secret: str, header: str, body: bytes) -> bool:
    parts = [p.split("=", 1) for p in header.split(",")]
    t = next(v for k, v in parts if k == "t")
    expected = hmac.new(secret.encode(), f"{t}.".encode() + body,
                        hashlib.sha256).hexdigest()
    return any(hmac.compare_digest(expected, v) for k, v in parts if k == "v1")

Replays and the timestamp

SignEnvoy does not enforce a maximum age on t; a redelivery requested from the dashboard or the API days later is signed with the time it is sent. If your handler rejects deliveries whose t is older than a window of your choosing, deduplicate on id as well so a late retry of a delivery you already processed is harmless. Retries happen after 30 s, 5 min, 30 min, with a 10-second timeout per attempt.

Rotating the secret

POST /v1/webhooks/{id}/rotate-secret (or "Rotate secret" in the dashboard) returns a new secret, shown once. For the next 24 hours every delivery to that endpoint is signed with both the new and the previous secret, so you can switch your handler at any point in that window without a failed verification. After the window the previous secret is discarded and the header carries one v1 again. GET /v1/webhooks shows previous_secret_expires_at while the window is open. Rotating again inside the window retires the older secret immediately: at most two secrets are ever live.