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
- Settings → Integrations: name it (e.g.
crm), choose Webhook, enter yourhttps://address and press Generate for a signing secret. Copy the secret; it's shown once. - 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.nameandvisitor.companyare included when the visitor gave them. Email addresses and phone numbers are never sent in webhooks.signalsholds 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
2xxstatus 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.