Webhooks

Signal can send a signed POST request to your own address when a rule fires, for example when a visitor becomes a hot lead. Use it to create a deal in your CRM, post in Slack through Zapier or Make, or start anything on your own server.

Set it up

  1. Settings → Integrations: name it (e.g. crm), choose Webhook, enter your https:// address and press Generate for a signing secret. Copy the secret; it's shown once.
  2. In your configuration, add an action and a rule:
"actions": {
  "toCrm": { "type": "webhook", "integration": "crm", "maxPerVisitor": 1 }
},
"rules": [
  {
    "id": "hot-lead",
    "when": { "type": "signal_equals", "key": "leadTemperature", "value": "hot", "minConfidence": 0.8 },
    "then": ["toCrm"]
  }
]

maxPerVisitor and cooldown (default 1d) keep one visitor from triggering it again and again. In shadow mode, no webhooks are sent.

The request

POST /your/endpoint HTTP/1.1
Content-Type: application/json
X-Signal-Delivery: act_01J…            (unique per delivery; use it to ignore duplicates)
X-Signal-Timestamp: 1759567200          (Unix seconds)
X-Signal-Signature: sha256=5f2b…        (see below)

{
  "event": "signal.action",
  "projectId": "prj_…",
  "visitor": { "id": "vis_…", "name": "Robin Baker", "company": "Bakery Robin" },
  "action": { "id": "toCrm", "ruleId": "hot-lead" },
  "signals": {
    "leadTemperature": { "value": "hot", "confidence": 0.86 }
  }
}
  • visitor.name and visitor.company are included when the visitor gave them. Email addresses and phone numbers are never sent in webhooks.
  • signals holds the estimates the rule looked at.

Verify the signature

The signature is an HMAC-SHA256 of "{timestamp}.{raw body}" with your signing secret, hex-encoded and prefixed with sha256=. Check it against the raw request body, and reject requests older than five minutes.

// Node.js (Express): use the raw body, not parsed JSON.
import { createHmac, timingSafeEqual } from "node:crypto";

app.post("/signal", express.raw({ type: "application/json" }), (req, res) => {
  const timestamp = req.get("x-signal-timestamp");
  const signature = req.get("x-signal-signature") ?? "";
  const expected = "sha256=" + createHmac("sha256", process.env.SIGNAL_WEBHOOK_SECRET)
    .update(`${timestamp}.${req.body}`).digest("hex");
  const fresh = Math.abs(Date.now() / 1000 - Number(timestamp)) < 300;
  const valid = signature.length === expected.length && timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
  if (!fresh || !valid) return res.status(401).end();

  const event = JSON.parse(req.body);
  // … create the deal, notify the team …
  res.status(200).end();
});
// PHP
$body = file_get_contents('php://input');
$timestamp = $_SERVER['HTTP_X_SIGNAL_TIMESTAMP'] ?? '';
$expected = 'sha256=' . hash_hmac('sha256', $timestamp . '.' . $body, getenv('SIGNAL_WEBHOOK_SECRET'));
if (abs(time() - (int) $timestamp) > 300 || !hash_equals($expected, $_SERVER['HTTP_X_SIGNAL_SIGNATURE'] ?? '')) {
  http_response_code(401); exit;
}
$event = json_decode($body, true);

Zapier ("Catch Hook") and Make ("Custom webhook") accept the request as it is; checking the signature there is optional.

Delivery and retries

  • Answer with any 2xx status within 10 seconds.
  • Anything else is retried up to 4 more times, with growing waits between attempts. Every attempt is recorded.
  • Signal never posts to private or internal network addresses and doesn't follow redirects.

Email alerts instead

Don't need a webhook? notifications in your configuration emails your team when someone is ready, with the whole visit. See Configuration → Notifications.