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
Navigate to Webhooks settings
Go to Settings → Webhooks in the left sidebar of the Stairoids dashboard.
Add a new endpoint
Click Add Endpoint in the top-right corner.
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.
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.
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 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
{
"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
The event type string, e.g. signal.created or account.score_changed.
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.
ISO 8601 UTC timestamp of when the event was generated by Stairoids.
The ID of the workspace that generated the event. Useful when a single endpoint receives events from multiple workspaces.
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.
| 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
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:
- Respond immediately with
200 OK as soon as you receive the request and have verified the signature.
- Enqueue the event payload to a message queue (e.g., SQS, RabbitMQ, Redis) or background job system (e.g., Sidekiq, BullMQ, Celery).
- 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.