Use SignalOps webhooks to push commerce events into your backend.
Merchant developers can receive signed events when SignalOps accepts an import, completes analysis, or syncs recommendations. Use those events to update internal dashboards, trigger warehouse checks, open support workflows, or notify the right operator without polling.
Why merchant teams use webhooks
The Commerce Ingestion API gets order data into SignalOps. Webhooks tell the merchant system what happened after the data arrived. That matters when the merchant has its own admin, ERP, Slack automation, support queue, warehouse workflow, or business intelligence layer that needs to react to SignalOps output.
A webhook keeps the merchant backend in control. SignalOps delivers a signed POST request, the merchant verifies it, stores the event, and chooses what should happen next.
Event flow
- A merchant backend sends orders to
POST /api/v1/ingest/orderswith a server-side API key. - SignalOps validates the payload and stores the import run.
- If
run_analysisis enabled, SignalOps runs revenue leak analysis and prepares recovery recommendations. - For each subscribed event, SignalOps posts a JSON payload to the merchant webhook URL.
- The merchant backend verifies the signature, stores the event, and returns a
2xxresponse.
Webhook events
import.accepted
Sent after SignalOps accepts and stores an ingestion batch. Use it to mark an internal sync job as complete or link a merchant import ID to the SignalOps import run.
analysis.completed
Sent when an analysis run exists for the accepted import. Use it to refresh internal reporting, notify an operator, or fetch analysis details from the authenticated dashboard/API path used by your integration.
recommendations.synced
Sent when SignalOps has refreshed the recovery recommendation queue from a new analysis. Use it to open merchant approval workflows, notify support, or hand off recommended actions to an internal system.
Create a webhook endpoint
- Open the SignalOps Developer console.
- Add a webhook name, such as
Production ERP receiver. - Enter an absolute
https://endpoint URL controlled by your backend. - Select one or more event types.
- Copy the signing secret immediately. SignalOps only shows it once.
The endpoint should respond quickly. Do slow work in a background job after you verify and store the event. SignalOps records the latest delivery status, status code, and delivery time in the Developer console.
Payload and headers
SignalOps sends compact JSON with the event name, creation time, and import or analysis identifiers.
POST /signalops/webhook
Content-Type: application/json
User-Agent: SignalOps-Webhooks/1.0
X-SignalOps-Event: analysis.completed
X-SignalOps-Timestamp: 1789794000
X-SignalOps-Signature: sha256=8f2f...
{
"event": "analysis.completed",
"created_at": "2026-09-19T10:20:00Z",
"data": {
"import_id": 42,
"source": "Acme backend",
"source_type": "api",
"order_count": 1842,
"linked_analysis_run_id": 77,
"run_id": 77
}
}
| Header | Use |
|---|---|
X-SignalOps-Event | The event type being delivered. |
X-SignalOps-Timestamp | Unix timestamp used in the HMAC signed message. Reject stale timestamps to reduce replay risk. |
X-SignalOps-Signature | HMAC SHA-256 signature in sha256=... format. |
Verify the signature
Always verify against the raw request body before parsing or transforming JSON. Build the signed message as:
{timestamp}.{raw_body}
Then compute HMAC SHA-256 using the webhook signing secret copied from SignalOps.
expected = "sha256=" + hmac_sha256(signing_secret, timestamp + "." + raw_body)
Compare signatures with a constant-time comparison. Also reject requests with missing headers, invalid timestamps, or timestamps outside your replay window.
Example: Node and Express receiver
import crypto from "node:crypto";
import express from "express";
const app = express();
const signingSecret = process.env.SIGNALOPS_WEBHOOK_SECRET;
app.post(
"/signalops/webhook",
express.raw({ type: "application/json" }),
(req, res) => {
const timestamp = req.header("X-SignalOps-Timestamp");
const signature = req.header("X-SignalOps-Signature") || "";
if (!timestamp || !signingSecret) {
return res.sendStatus(401);
}
const ageSeconds = Math.abs(Date.now() / 1000 - Number(timestamp));
if (!Number.isFinite(ageSeconds) || ageSeconds > 300) {
return res.sendStatus(401);
}
const body = req.body;
const expected =
"sha256=" +
crypto
.createHmac("sha256", signingSecret)
.update(Buffer.concat([Buffer.from(`${timestamp}.`), body]))
.digest("hex");
const expectedBuffer = Buffer.from(expected);
const receivedBuffer = Buffer.from(signature);
if (
expectedBuffer.length !== receivedBuffer.length ||
!crypto.timingSafeEqual(expectedBuffer, receivedBuffer)
) {
return res.sendStatus(401);
}
const event = JSON.parse(body.toString("utf8"));
// Store first, then handle in a job queue.
// Use event.event + event.data.import_id + event.data.run_id for dedupe.
console.log("SignalOps event", event.event, event.data);
return res.sendStatus(204);
}
);
app.listen(3000);
Example: Python and FastAPI receiver
import hashlib
import hmac
import os
import time
from fastapi import FastAPI, Header, HTTPException, Request
app = FastAPI()
SIGNING_SECRET = os.environ["SIGNALOPS_WEBHOOK_SECRET"]
@app.post("/signalops/webhook")
async def signalops_webhook(
request: Request,
x_signalops_timestamp: str | None = Header(default=None),
x_signalops_signature: str | None = Header(default=None),
):
if not x_signalops_timestamp or not x_signalops_signature:
raise HTTPException(status_code=401, detail="Missing webhook signature")
try:
timestamp = int(x_signalops_timestamp)
except ValueError as exc:
raise HTTPException(status_code=401, detail="Invalid timestamp") from exc
if abs(int(time.time()) - timestamp) > 300:
raise HTTPException(status_code=401, detail="Stale webhook")
raw_body = await request.body()
signed_message = f"{x_signalops_timestamp}.".encode("utf-8") + raw_body
expected = "sha256=" + hmac.new(
SIGNING_SECRET.encode("utf-8"),
signed_message,
hashlib.sha256,
).hexdigest()
if not hmac.compare_digest(expected, x_signalops_signature):
raise HTTPException(status_code=401, detail="Invalid signature")
event = await request.json()
# Store event, dedupe it, then enqueue internal work.
return {"ok": True}
What to do after receiving events
| Merchant system | Practical use |
|---|---|
| Custom admin | Show import and analysis progress next to the merchant's own sync job. |
| ERP or OMS | Attach the SignalOps import ID to the originating order batch for auditability. |
| Support platform | Create a review task when analysis points to refund pressure or product quality risk. |
| Marketing automation | Start an approval workflow when recommendations are ready for lifecycle or retention action. |
| Data warehouse | Store event metadata so BI dashboards can show when SignalOps ran and which import triggered it. |
Reliability checklist
- Return
2xxonly after the signature is valid and the event is stored. - Respond quickly and move slow work to a queue.
- Dedupe events by
event,data.import_id,data.run_id, andcreated_at. - Reject stale timestamps, such as requests older than five minutes.
- Keep the signing secret in a secret manager or encrypted environment variable.
- Rotate the webhook by creating a new endpoint, deploying the new secret, then revoking the old endpoint.
- Watch the Developer console delivery status after deploys and endpoint changes.