Webhooks

Get an HTTPS request from Signal when records change, and verify that it came from Signal.

Webhooks let Signal tell your systems when something happens, such as a new prospect contact, a won Deal or a Marketing Email click. When a subscribed event happens, Signal sends a POST request with a JSON body to a URL you choose.

Most events fire whether the change was made in Signal or through the REST API. The event catalog notes the exceptions.

Register an endpoint#

  1. In Signal, open Autopilot in the sidebar.
  2. Select the Webhooks & API tab. Webhooks is the first section.
  3. Select Create Webhook and choose the outbound direction ("Send CRM events to external tools").
  4. Enter a name and your endpoint URL, such as https://example.com/signal/webhooks.
  5. Pick the events to send. You can tick whole categories or single events. A sample payload is shown for the events you pick.
  6. Optionally add custom headers, or a bearer token that Signal sends as Authorization: Bearer <token>.
  7. Save. Copy the signing secret now: it's shown only once. You need it to verify signatures.

You need permission to manage integrations to create, edit, test or delete webhooks. People who can view integrations see webhooks read-only. Marketing events are offered only when your plan includes Marketing.

From the webhook's row you can:

  • Test it: Signal sends a webhook.test event and shows the response code.
  • View deliveries: the most recent deliveries with their status, response code, response body (first 2,000 characters), error and duration.
  • Edit it: change the URL, events, headers or token, turn it off, or rotate the signing secret.
  • Delete it, along with its delivery history.

Your endpoint must be reachable from the public internet. Use HTTPS.

Event catalog#

Event Sent when
prospect.created A prospect contact is created, including by POST /prospects when no existing one matches the email.
prospect.updated A prospect contact is updated, including by PATCH /prospects/{id} and by POST /prospects when it matches an existing email.
prospect.deleted A prospect contact is deleted (moved to Recently deleted).
contact.created A Contact is created.
contact.deleted A Contact is deleted (moved to Recently deleted).
deal.created A Deal is created.
deal.won A Deal is won. In Signal, that's when it moves to a won stage or a signed proposal marks it won. Through the API, it's when PATCH /deals/{id} raises probability to 100 or more.
activity.created An activity is logged in Signal. (Activities created through the API don't send it.)
development.created A Property development is created.
development.updated A Property development is updated.
plot.created A Property plot is created.
plot.updated A Property plot is updated.
plot.status_changed A plot's status changes. Sent alongside plot.updated, with previousStatus in data.
marketing.email_opened A recipient opens a Marketing Email.
marketing.email_clicked A recipient clicks a link in a Marketing Email.
marketing.email_unsubscribed A recipient unsubscribes.
marketing.email_bounced A Marketing Email bounces.
marketing.email_complained A recipient marks a Marketing Email as spam.
marketing.email_replied A recipient replies to a Marketing Email.
marketing.campaign_email_sent A Campaign Email is sent.
marketing.nurture_graduated Someone completes a nurture Sequence.
marketing.feedback_received A recipient answers an Email's 0 to 10 feedback question.
marketing.added_to_list Someone is added to one of your Lists.
webhook.test You select Test on the webhook.

Marketing events need a plan that includes Marketing.

The event picker also lists events that Signal doesn't send yet (for example account.created, deal.stage_changed, task.created and the deck.* events). Subscribing to them does no harm, but nothing is delivered for them today.

Payload#

Every delivery is a POST with a JSON body in this envelope:

Field Type Notes
eventId string Unique ID for this event (UUID). Use it to ignore duplicates.
eventType string The event name, such as deal.created.
workspaceId string The workspace the event happened in.
timestamp string When Signal sent the event, ISO 8601 in UTC.
entityType string The kind of record, such as prospect, contact, deal, activity, development or plot. Marketing events name the person: a prospect contact or a Contact.
entityId string The record's ID. Use it with the REST API to fetch the record.
data object A snapshot of the record or event, in camelCase.

data is a snapshot, and its fields depend on the event and on where the change was made (in Signal or through the API). Read the fields you need defensively, and fetch the record from the REST API when you need its full, current state.

Example deal.created (sent by POST /deals):

JSON
{
  "eventId": "0c6f7b2e-9d41-4b8a-a3e5-1f2d4c6b8a90",
  "eventType": "deal.created",
  "workspaceId": "cm1a2b3c40000xy12z3a4b5c6",
  "timestamp": "2026-11-12T09:30:01.214Z",
  "entityType": "deal",
  "entityId": "7c2e9f14-3a6b-4d1e-8f5a-9b0c2d4e6f81",
  "data": {
    "id": "7c2e9f14-3a6b-4d1e-8f5a-9b0c2d4e6f81",
    "name": "Contoso renewal 2027",
    "value": 48500,
    "valuePeriod": "yearly",
    "probability": 50,
    "stageId": "rds_proposal",
    "accountId": "3f6c2a1e-8b4d-4c2a-9f1e-2b7d5a9c4e10",
    "ownerId": null,
    "expectedCloseAt": "2026-12-15T00:00:00.000Z",
    "createdAt": "2026-11-12T09:30:00.000Z"
  }
}

Example prospect.created (sent by POST /prospects):

JSON
{
  "eventId": "5a1e9c3d-7b2f-4e6a-8d0c-2b4f6a8c0e13",
  "eventType": "prospect.created",
  "workspaceId": "cm1a2b3c40000xy12z3a4b5c6",
  "timestamp": "2026-11-12T09:30:01.502Z",
  "entityType": "prospect",
  "entityId": "e4b7c1d9-2f6a-4b3e-9c8d-5a1f7e2b4c63",
  "data": {
    "id": "e4b7c1d9-2f6a-4b3e-9c8d-5a1f7e2b4c63",
    "firstName": "Sam",
    "lastName": "Lee",
    "email": "sam.lee@fabrikam.example",
    "phone": null,
    "jobTitle": "Operations Director",
    "stageId": "rps_new",
    "sourceId": "rls_website",
    "ownerId": null,
    "createdAt": "2026-11-12T09:30:00.000Z",
    "updatedAt": "2026-11-12T09:30:00.000Z",
    "customFields": { "budget_band": "50k-100k" }
  }
}

Marketing events carry the person and Email in data: contactId, prospectId, accountId, ownerId, marketingEmailId, emailName, subject, campaignId, campaignName and occurredAt, plus fields for the event (for example linkUrl and linkIndex on marketing.email_clicked).

Headers#

Header Value
Content-Type application/json
User-Agent Empiraa-Webhooks/1.0
X-Empiraa-Event The event name, such as deal.created.
X-Empiraa-Delivery Unique ID of this delivery.
X-Empiraa-Timestamp When the request was signed, in Unix seconds.
X-Empiraa-Signature sha256= followed by the hex HMAC signature. See below.
Authorization Bearer <token>, if you set a bearer token on the webhook.

Any custom headers you added to the webhook are sent too.

Verify signatures#

Check every request's signature before you trust it. Signal signs each delivery with your webhook's signing secret:

  1. Take the X-Empiraa-Timestamp header and the raw request body, exactly as received.
  2. Join them with a period: <timestamp>.<raw body>.
  3. Compute an HMAC-SHA256 of that string, using the signing secret as the key, and hex-encode it.
  4. Compare it with the value after sha256= in X-Empiraa-Signature, using a constant-time comparison.
  5. Reject requests whose timestamp is more than a few minutes old, to stop replays.

Parse the JSON only after the signature checks out. Re-serializing parsed JSON changes the bytes and breaks the signature.

Node.js example#

JavaScript
import crypto from "node:crypto";
import express from "express";

const app = express();
const SIGNING_SECRET = process.env.SIGNAL_WEBHOOK_SECRET; // the secret Signal showed you
const TOLERANCE_SECONDS = 300;

function verifySignalSignature(rawBody, timestamp, signatureHeader) {
  if (!timestamp || !signatureHeader?.startsWith("sha256=")) return false;

  const age = Math.abs(Date.now() / 1000 - Number(timestamp));
  if (!Number.isFinite(age) || age > TOLERANCE_SECONDS) return false;

  const expected = crypto
    .createHmac("sha256", SIGNING_SECRET)
    .update(`${timestamp}.${rawBody}`)
    .digest("hex");
  const received = signatureHeader.slice("sha256=".length);

  const a = Buffer.from(expected, "hex");
  const b = Buffer.from(received, "hex");
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

// Read the body as raw text so the signature can be checked against the exact bytes.
app.post("/signal/webhooks", express.text({ type: "application/json" }), (req, res) => {
  const ok = verifySignalSignature(
    req.body,
    req.get("X-Empiraa-Timestamp"),
    req.get("X-Empiraa-Signature"),
  );
  if (!ok) return res.status(401).send("Invalid signature");

  const event = JSON.parse(req.body);
  res.status(200).send("ok"); // acknowledge quickly

  // Then process asynchronously, ignoring events you've already handled.
  queueEvent(event); // your own function
});

app.listen(3000);

Rotate the signing secret#

Edit the webhook and rotate the secret. The new secret is shown once, and every delivery after that is signed with it, so update your endpoint's secret straight away. Deliveries signed with the old secret will fail your check until you do.

Delivery, timeouts and retries#

  • Signal sends events shortly after the change, separately from the request or action that caused it.
  • Your endpoint must answer within 10 seconds. Any 2xx status counts as delivered. Any other status, a timeout or a connection error counts as failed.
  • Every attempt is recorded in the webhook's delivery log, with the response code and the first 2,000 characters of the response body.
  • Signal makes one delivery attempt per event. A failed delivery stays in the delivery log and isn't sent again automatically, so reconcile with the REST API (for example, list recently changed records) if your endpoint was down.
  • If the same event fires for the same record several times within about 5 seconds, Signal may send it only once.
  • Events can arrive out of order. Use timestamp, or fetch the record, rather than relying on arrival order.

Best practices#

  • Verify every signature and reject old timestamps.
  • Respond fast. Return 2xx as soon as you've verified and stored the event, then do the work in the background. Slow endpoints hit the 10-second timeout.
  • Be idempotent. Store eventId (or the X-Empiraa-Delivery header) and skip events you've already processed.
  • Treat data as a hint. For anything important, fetch the record from the REST API using entityId.
  • Ignore unknown events and fields. New events and data fields may be added at any time.
  • Use HTTPS, and keep the signing secret and any bearer token in your secrets manager.
  • Watch the delivery log in Signal after a deployment to catch failures early.