> ## Documentation Index
> Fetch the complete documentation index at: https://docs.stairoids.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Configure Outgoing Webhooks to Receive Signal Events

> Register HTTPS endpoints to receive real-time HTTP POST notifications from Stairoids when signal events occur and verify payloads with HMAC-SHA256.

Webhooks give your systems a live feed of everything happening inside Stairoids. Instead of polling the API for new signals or score changes, you register an HTTPS endpoint and Stairoids pushes a structured JSON payload to it the moment a relevant event occurs. This lets you trigger downstream workflows, sync data to your own database, or fan out notifications in real time.

<Note>
  This page covers **outgoing webhooks** — events that Stairoids sends to your endpoint. For **incoming webhooks** (sending events from your own tools into Stairoids), see the [Incoming Automations](/signals/incoming-automations) documentation.
</Note>

## Registering a Webhook Endpoint

<Steps>
  <Step title="Navigate to Webhooks settings">
    Go to **Settings → Webhooks** in the left sidebar of the Stairoids dashboard.
  </Step>

  <Step title="Add a new endpoint">
    Click **Add Endpoint** in the top-right corner.
  </Step>

  <Step title="Enter your endpoint URL">
    Paste the full HTTPS URL of your receiving endpoint (e.g., `https://yourapp.example.com/webhooks/stairoids`). HTTP endpoints are not accepted — your URL must use TLS.
  </Step>

  <Step title="Select event types">
    Choose one or more event types from the checklist. Stairoids only sends events you explicitly subscribe to — subscribing to fewer event types reduces noise and unnecessary processing load on your endpoint.
  </Step>

  <Step title="Save the endpoint">
    Click **Save**. Stairoids generates a unique signing secret for this endpoint and displays it once — copy it immediately and store it securely. You'll use it to verify incoming payloads.
  </Step>
</Steps>

<Tip>
  Use the **Test** button in **Settings → Webhooks** to send a sample payload to your endpoint at any time. This is the fastest way to confirm your endpoint is reachable, parsing the payload correctly, and returning a `2xx` status code.
</Tip>

## Event Types

Subscribe your endpoint to any combination of the following event types.

| Event Type              | Description                                                                  |
| ----------------------- | ---------------------------------------------------------------------------- |
| `signal.created`        | A new signal has been ingested from any source.                              |
| `signal.scored`         | A signal has been processed and an engagement score has been assigned to it. |
| `automation.triggered`  | An outbound automation was triggered by a matching signal.                   |
| `account.score_changed` | An account's aggregate engagement score has changed.                         |
| `contact.score_changed` | A contact's aggregate engagement score has changed.                          |

## Webhook Payload Structure

Every webhook delivery is an HTTP `POST` request with a `Content-Type: application/json` header. The top-level structure is consistent across all event types — only the contents of the `data` object vary by event.

### Example: `signal.created`

```json theme={null}
{
  "event": "signal.created",
  "id": "evt_01HXYZ9876STUVWX",
  "created_at": "2024-06-15T14:32:07Z",
  "workspace_id": "ws_01HABC1234DEFGHI",
  "data": {
    "signal": {
      "id": "sig_01HJKL5678MNOPQR",
      "type": "page_view",
      "source": "segment",
      "created_at": "2024-06-15T14:32:05Z",
      "properties": {
        "url": "https://www.acme.com/pricing",
        "referrer": "https://www.google.com"
      },
      "account": {
        "id": "acc_01HSTU9012VWXYZ1",
        "domain": "acme.com",
        "name": "Acme Corp"
      },
      "contact": {
        "id": "con_01H2345ABCDE6789",
        "email": "jane.doe@acme.com",
        "name": "Jane Doe"
      }
    }
  }
}
```

### Top-Level Fields

<ResponseField name="event" type="string">
  The event type string, e.g. `signal.created` or `account.score_changed`.
</ResponseField>

<ResponseField name="id" type="string">
  A unique identifier for this webhook delivery event. Use this for idempotency — if you receive duplicate deliveries due to retries, this ID lets you deduplicate them.
</ResponseField>

<ResponseField name="created_at" type="string">
  ISO 8601 UTC timestamp of when the event was generated by Stairoids.
</ResponseField>

<ResponseField name="workspace_id" type="string">
  The ID of the workspace that generated the event. Useful when a single endpoint receives events from multiple workspaces.
</ResponseField>

<ResponseField name="data" type="object">
  The event payload. The structure of this object depends on the event type. For `signal.*` events it contains a `signal` object; for `account.*` events it contains an `account` object; and so on.
</ResponseField>

## Securing Your Webhook Endpoint

Stairoids signs every outgoing webhook payload using HMAC-SHA256 so you can verify that the request genuinely originated from Stairoids and has not been tampered with in transit.

The signature is included in the `X-Stairoids-Signature` request header as a hex-encoded digest computed over the raw request body using your endpoint's signing secret.

### Verifying in Node.js

```javascript theme={null}
const crypto = require("crypto");

function verifyWebhookSignature(rawBody, signature, secret) {
  const expectedSignature = crypto
    .createHmac("sha256", secret)
    .update(rawBody) // rawBody must be the raw Buffer, not a parsed object
    .digest("hex");

  // Use timingSafeEqual to prevent timing attacks
  return crypto.timingSafeEqual(
    Buffer.from(signature, "hex"),
    Buffer.from(expectedSignature, "hex")
  );
}

// Express.js example
app.post("/webhooks/stairoids", express.raw({ type: "application/json" }), (req, res) => {
  const signature = req.headers["x-stairoids-signature"];
  const secret = process.env.STAIROIDS_WEBHOOK_SECRET;

  if (!verifyWebhookSignature(req.body, signature, secret)) {
    return res.status(401).send("Invalid signature");
  }

  const event = JSON.parse(req.body);
  // Handle the event asynchronously...
  res.status(200).send("OK");
});
```

### Verifying in Python

```python theme={null}
import hashlib
import hmac
import os
from flask import Flask, request, abort

app = Flask(__name__)
WEBHOOK_SECRET = os.environ["STAIROIDS_WEBHOOK_SECRET"]

@app.route("/webhooks/stairoids", methods=["POST"])
def handle_webhook():
    raw_body = request.get_data()  # Raw bytes — do NOT call request.json yet
    signature = request.headers.get("X-Stairoids-Signature", "")

    expected_signature = hmac.new(
        WEBHOOK_SECRET.encode("utf-8"),
        raw_body,
        hashlib.sha256
    ).hexdigest()

    if not hmac.compare_digest(signature, expected_signature):
        abort(401)

    event = request.get_json()
    # Handle the event asynchronously...
    return "", 200
```

<Warning>
  Always verify the `X-Stairoids-Signature` header before processing a webhook payload. Without signature verification, any party who discovers your endpoint URL could send forged events.
</Warning>

## Retry Policy

Stairoids considers a webhook delivery successful when your endpoint returns any `2xx` HTTP status code within the response timeout window. If your endpoint returns a non-`2xx` response — or fails to respond at all — Stairoids automatically retries the delivery with exponential backoff.

| Attempt   | Delay After Previous Failure |
| --------- | ---------------------------- |
| 1st retry | 1 minute                     |
| 2nd retry | 5 minutes                    |
| 3rd retry | 30 minutes                   |
| 4th retry | 2 hours                      |
| 5th retry | 8 hours                      |

After 5 failed retries (6 attempts total), Stairoids marks the delivery as **failed** and stops retrying. You can view failed deliveries and manually re-trigger them from **Settings → Webhooks → \[Endpoint Name] → Delivery Log**.

## Responding to Webhooks

<Warning>
  Your endpoint must respond within **10 seconds**. Stairoids counts the clock from the moment the TCP connection is established. If your response takes longer, the delivery is treated as failed and enters the retry queue — even if your server eventually processes the event correctly.
</Warning>

To avoid timeouts, follow this pattern:

1. **Respond immediately with `200 OK`** as soon as you receive the request and have verified the signature.
2. **Enqueue the event payload** to a message queue (e.g., SQS, RabbitMQ, Redis) or background job system (e.g., Sidekiq, BullMQ, Celery).
3. **Process the event asynchronously** in a worker that consumes from the queue.

```javascript theme={null}
// Correct pattern: acknowledge immediately, process later
app.post("/webhooks/stairoids", express.raw({ type: "application/json" }), async (req, res) => {
  // 1. Verify signature
  if (!verifyWebhookSignature(req.body, req.headers["x-stairoids-signature"], secret)) {
    return res.status(401).send("Invalid signature");
  }

  // 2. Acknowledge immediately
  res.status(200).send("Accepted");

  // 3. Process asynchronously
  const event = JSON.parse(req.body);
  await queue.enqueue("process_stairoids_event", event);
});
```

<Tip>
  Use the webhook event's `id` field to implement idempotency in your queue consumer. If Stairoids retries a delivery that your server already processed (but responded slowly to), your consumer can detect the duplicate `id` and skip reprocessing.
</Tip>
