All articles
Integration & Analytics

Webhook signatures

Deliveries from Signalist are signed with a secret that only you and Signalist know. Your receiver can use the signature to check that a request came from Signalist and that nobody changed it on the way. New webhooks are signed from the start. A webhook that was created before 14 October 2026 keeps delivering unsigned, as before, until you switch signing on for it (see "Your signing secret"). This article explains how to get your secret, how to check a delivery, and how to rotate the secret. For creating a webhook and for payload formats, see Use API keys and webhooks safely.

What a signed delivery carries

Each signed request has these headers:

  • X-Signalist-Signature: t=<unix seconds>,v1=<hex>: the signature.
  • X-Signalist-Webhook-Id: the webhook that sent it.
  • X-Signalist-Delivery-Id: one id per lead and event. A retry repeats the id and carries a fresh t.
  • Content-Type: application/json

An older webhook that has not been switched over sends none of the X-Signalist- headers.

Your signing secret

The secret starts with whsec_. Signalist shows it once, in the Webhook created: copy your signing secret dialog right after you create the webhook. Copy it into your receiver. The dialog can only be closed with I have copied the secret.

  • You lost the secret. Open Integrations → Webhooks, open the row menu (More actions) and choose Rotate secret. Confirm with Rotate. A new secret is shown once.

  • Your webhook is older than signing. Webhooks created before signing was introduced show Secret not yet viewed in the Signing column. Their deliveries are not signed yet: they are sent exactly as before, without the signature headers, and they stay that way until you switch signing on. You switch it on in one of three ways:

    • choose Reveal secret once in the row menu to see the secret. It can be shown only once. A second attempt is refused with "This secret was already shown once. Use "Rotate secret" to get a new one.", and the menu item is gone after the page refreshes;
    • choose Rotate secret in the row menu;
    • change the webhook's URL with Replace URL. Choose Reveal secret once afterwards to see the secret.

    From then on every delivery to that webhook is signed, goes only to a public address and follows no redirect, like a delivery of a new webhook. It does not go back to unsigned delivery. If your current URL redirects to another address, replace it with the address it redirects to before you switch signing on.

How to verify a delivery

  1. Read the raw request body exactly as received, before any JSON parsing.
  2. Split the signature header at the commas. Take t and every v1.
  3. Compute HMAC-SHA256 with the whole secret (including the whsec_ prefix) as the key, over the string <t>.<raw body>, as lowercase hex.
  4. Compare the result with each v1 in constant time (for example crypto.timingSafeEqual in Node or hmac.compare_digest in Python). Accept the request if one matches.
  5. Reject the request if t is more than 5 minutes away from your clock. This stops replays.
  6. Store X-Signalist-Delivery-Id and ignore a delivery you already processed. A retry repeats it.

Answer with a 2xx status to accept a delivery.

A Node example. It accepts a real delivery and rejects a changed body, a wrong secret and a timestamp older than 5 minutes:

import crypto from "node:crypto";

function verify(header, rawBody, secret, now = Date.now() / 1000) {
  const parts = header.split(",").map((p) => p.split("="));
  const t = parts.find(([k]) => k === "t")?.[1];
  const signatures = parts.filter(([k]) => k === "v1").map(([, v]) => v);
  if (!t || Math.abs(now - Number(t)) > 300) return false;
  const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
  return signatures.some(
    (s) => s.length === expected.length && crypto.timingSafeEqual(Buffer.from(s), Buffer.from(expected)),
  );
}

Rotate the secret

Choose Rotate secret in the row menu. The old secret keeps working for 24 hours. During that time each request carries two v1 values, the new one first and the old one second, so you can update your receiver at your own pace: accept the request if either value matches. The Signing column shows "Rotating: old secret valid until" and the date.

If you rotate again within those 24 hours, the secret from the first rotation stops working at once.

Rotating the secret of an older webhook also switches it to signed delivery: after rotating, every delivery to it is signed, goes only to a public address and follows no redirect.

Send a test event

In the row menu or in the edit dialog, choose Send test event. Signalist sends a signed sample, marked "event": "test", to your URL. No real lead is sent and nothing is written to your lists. The dialog shows Delivered. or Not delivered., what was sent, and your receiver's answer (the first 500 characters). A test event does not count toward the webhook's delivery status in the table.

  • You can send one test event per webhook every 15 seconds. Sooner, you see "A test event was just sent. Wait a few seconds and try again."
  • Signalist waits at most 10 seconds for an answer and reads at most 64 KB of it.
  • Redirects are not followed. A 301 or 302 shows as Not delivered, and the target of the redirect is not called.
  • A URL on a private network address cannot receive webhooks. See "A delivery shows Not sent" in Use API keys and webhooks safely.
  • A test event is always signed and always goes through these checks, also for an older webhook. Sending one does not switch the webhook over to signed delivery.

Troubleshooting

ProblemWhat to do
The signature never matchesCompute it over the raw request body, byte for byte, not over re-serialised JSON. Use the whole secret including whsec_, and the string <t>.<raw body>.
The signature matched until todayThe secret may have been rotated. During the 24 hours after a rotation a request carries two v1 values. Accept it if either matches, and switch to the new secret.
The deliveries of an older webhook carry no signatureThe webhook shows Secret not yet viewed and still delivers as before. Choose Reveal secret once or Rotate secret in the row menu, or replace its URL. From then on its deliveries are signed.
I lost the secretChoose Rotate secret in the row menu.
Reveal secret once is missingThe secret of a webhook created with signing was shown once, at creation. Choose Rotate secret to get a new one.
Send test event says Not deliveredRead your receiver's answer in the dialog. If your receiver checks the signature, make sure it uses the current secret and the raw request body.

Frequently asked questions

Do I have to verify the signature?

No. Your receiver keeps working without a check. The deliveries of new webhooks, and of older webhooks that you have switched over, are signed anyway, so you can add the check whenever you are ready.

Why does an older webhook deliver without a signature?

So that a receiver built before signing existed keeps working unchanged. Signing, the check for private addresses and the end of redirect following are switched on for such a webhook only when you reveal or rotate its secret or change its URL.

Can I go back to unsigned delivery after switching signing on?

No. Once a webhook is signed, it stays signed, goes only to public addresses and follows no redirect.

Is the signature the same for Version 1 and Version 2 webhooks?

The signature works the same way. It is computed over the body of the delivery, whichever payload format the webhook uses.

Why do I see two v1 values?

You rotated the secret less than 24 hours ago. The first value is made with the new secret, the second with the old one.