Skip to main content
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.
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 documentation.

Registering a Webhook Endpoint

1

Navigate to Webhooks settings

Go to Settings → Webhooks in the left sidebar of the Stairoids dashboard.
2

Add a new endpoint

Click Add Endpoint in the top-right corner.
3

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.
4

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.
5

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.
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.

Event Types

Subscribe your endpoint to any combination of the following event types.
Event TypeDescription
signal.createdA new signal has been ingested from any source.
signal.scoredA signal has been processed and an engagement score has been assigned to it.
automation.triggeredAn outbound automation was triggered by a matching signal.
account.score_changedAn account’s aggregate engagement score has changed.
contact.score_changedA 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

{
  "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

event
string
The event type string, e.g. signal.created or account.score_changed.
id
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.
created_at
string
ISO 8601 UTC timestamp of when the event was generated by Stairoids.
workspace_id
string
The ID of the workspace that generated the event. Useful when a single endpoint receives events from multiple workspaces.
data
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.

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

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

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
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.

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.
AttemptDelay After Previous Failure
1st retry1 minute
2nd retry5 minutes
3rd retry30 minutes
4th retry2 hours
5th retry8 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

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.
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.
// 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);
});
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.