Outgoing webhooks
Payload shapes and signature verification for webhooks SigSentry sends to your notification channels
This page describes the webhooks SigSentry sends to you — the payload format your webhook endpoint receives when an analysis fires. This is the contract you build against when implementing a webhook notification channel.
For configuring webhook channels themselves, see Webhook channels.
Delivery
When an analysis result meets a webhook channel's severity threshold,
SigSentry sends a POST to your URL with:
POST <your-url>
Content-Type: application/json
X-Sigsentry-Event: analysis.complete
X-Sigsentry-Signature: sha256=...
X-Sigsentry-Delivery: <unique-id>| Header | Notes |
|---|---|
X-Sigsentry-Event | Event type — see the table below |
X-Sigsentry-Signature | HMAC-SHA256 of the raw body, keyed with the channel's signing secret |
X-Sigsentry-Delivery | Unique delivery id; safe to use for dedup |
Event types
| Event | When sent |
|---|---|
analysis.complete | An analysis finished and its severity met the channel's threshold |
analysis.partial | An analysis returned partial results (one or more log sources unreachable) |
watchdog.fired | A Watchdog rule with notify_only action fired (no analysis follows) |
Your endpoint should respond 2xx quickly — long-running work should
be enqueued in your handler and processed asynchronously. Non-2xx
responses are retried with exponential backoff for a while; if all
retries fail the delivery is marked Failed in the channel's
activity.
analysis.complete payload
{
"event": "analysis.complete",
"deliveryId": "del_a1b2c3...",
"analysisId": "ana_x1y2z3...",
"projectId": "proj_p1q2r3...",
"projectSlug": "checkout-api",
"createdAt": "2026-04-25T14:30:12Z",
"summary": "Connection pool exhaustion in checkout-api...",
"severity": "high",
"confidence": 0.87,
"rootCause": {
"description": "...",
"service": "checkout-api",
"errorType": "ConnectionPoolExhausted",
"category": "database"
},
"affectedServices": [
{ "serviceName": "checkout-api", "role": "origin", "errorCount": 234, "firstSeen": "...", "lastSeen": "..." }
],
"suggestedActions": [
{ "priority": 1, "action": "Increase the database connection pool ceiling", "rationale": "...", "type": "fix" }
],
"codeCorrelation": {
"available": true,
"suspectedCode": { "repo": "checkout-api", "filePath": "src/db.ts", "functionName": "getConnection", "lineRange": [42, 58] },
"causalPR": { "id": "...", "title": "...", "url": "...", "confidence": 0.91 }
},
"dashboardUrl": "https://dashboard.sigsentry.com/..."
}The shape mirrors the API's AnalysisResult — see
Reading the result for field-level
detail.
analysis.partial payload
Same shape as analysis.complete plus a partialReasons field:
{
"event": "analysis.partial",
"...": "...",
"partialReasons": [
{ "source": "datadog-prod", "reason": "rate-limited" }
]
}watchdog.fired payload
Sent for notify_only rules. No analysis runs.
{
"event": "watchdog.fired",
"deliveryId": "del_...",
"ruleId": "rule_...",
"ruleName": "checkout-api error rate",
"projectId": "proj_...",
"projectSlug": "checkout-api",
"firedAt": "2026-04-25T14:30:00Z",
"trigger": {
"type": "error_rate",
"value": 0.082,
"threshold": 0.05,
"errorCount": 234,
"totalCount": 2854,
"lookbackMinutes": 30
},
"sampleLogs": [
{ "timestamp": "...", "service": "...", "level": "error", "message": "..." }
],
"dashboardUrl": "https://dashboard.sigsentry.com/..."
}Signature verification
Every delivery is signed with HMAC-SHA256 using the channel's signing
secret. The secret is supplied by you when you create the
webhook channel — generate a strong random value (e.g.
openssl rand -hex 32) and store it on both ends. It's not
auto-generated server-side, so you control where it lives.
The signature is computed over the raw request body, before any parsing or modification. Verify with this Node.js example:
import { createHmac, timingSafeEqual } from 'crypto';
function verifySignature(rawBody: Buffer, headerValue: string, secret: string): boolean {
// headerValue looks like "sha256=<hex>"
const [scheme, sig] = headerValue.split('=');
if (scheme !== 'sha256' || !sig) return false;
const expected = createHmac('sha256', secret).update(rawBody).digest('hex');
const received = Buffer.from(sig, 'hex');
const expectedBuf = Buffer.from(expected, 'hex');
return received.length === expectedBuf.length
&& timingSafeEqual(received, expectedBuf);
}Verify against the raw bytes, not a re-serialized JSON body. Most web frameworks parse JSON before exposing the body — you'll need to configure your framework to retain the raw body for verification.
| Framework | Pattern |
|---|---|
| Express | app.use(express.json({ verify: (req, res, buf) => req.rawBody = buf })) then verify against req.rawBody |
| Fastify | Use the addContentTypeParser to capture the raw body before parsing |
| Next.js (Pages Router) | export const config = { api: { bodyParser: false } }, then read the raw stream |
| Next.js (App Router) | await request.text() gives the raw body as a string; convert to Buffer for verification |
Dedup with deliveryId
Each delivery has a unique deliveryId. If your endpoint sometimes
receives duplicates (network retries, your handler fails after
processing), dedup on this field rather than analysis id — multiple
deliveries can be triggered for the same analysis if your endpoint
returns non-2xx and SigSentry retries.
Manual replay
The dashboard has a Replay button on each delivery in Project → Channels → [your channel] → Activity. It re-sends the same payload with the same signature so you can debug your handler without triggering a new analysis.
