File size: 10,362 Bytes
0964fe7 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 | # Webhooks
Superlog sends webhooks to your server when interesting things happen in a
project. Today we ship exactly one event:
| Event | Fires when |
| ---------------------- | ------------------------------------------------------------------------------------------- |
| `agent_run.completed` | An agent run on an incident finishes with findings β either a PR was opened, or the agent reached a no-PR conclusion (noise / already resolved / PR policy = never). Not fired when an agent run is closed because its incident was merged into a duplicate. |
> **Breaking change (May 2026):** the event name was renamed from
> `investigation.completed` to `agent_run.completed` as part of merging the
> incident and investigation concepts. Existing endpoints' `enabled_events`
> are migrated automatically; update any client-side switch / filter on the
> event name to match.
## Configuring an endpoint
Go to **Settings β Webhooks** for the project that should send the event.
1. Paste your endpoint URL (`https://...`).
2. Click **Add endpoint**. Superlog generates a signing secret prefixed `whsec_`
and shows it to you once. Copy it β we never display it again.
3. Use **Send test** to fire a sample payload right away. The delivery shows up
in the live log under the endpoint.
You can rotate the secret at any time, disable an endpoint (deliveries are
suspended but state is preserved), or delete it (deletes all delivery history).
## Delivery semantics
- Each event is sent as `POST` with `Content-Type: application/json`.
- Request timeout is **10 seconds**. Anything slower is treated as a failure
and retried.
- Non-2xx responses and connection errors / timeouts are retried with backoff
before attempts 2-8: **30s β 1m β 2m β 5m β 15m β 1h β 6h** (no wait before
attempt 1). After 8 failed attempts (~8h elapsed) the delivery is marked
`failed` and we stop trying.
- Automatic retries reuse the same `Superlog-Delivery` id β use it as your
idempotency key. A manual **redeliver** from Settings enqueues a *new*
delivery row with a *new* id; if you redeliver something we already
delivered, your receiver will see it twice.
- Disabling an endpoint stops new deliveries from being enqueued, and any
deliveries still pending at the time the endpoint is disabled are marked
`failed` with `lastError = "endpoint disabled"` on the next worker tick.
- Deliveries appear under the endpoint in Settings with status, last HTTP code,
response body (truncated to 2 KiB), next attempt, and any error.
- The **Send test** button posts a stub payload of the form
`{ event, eventId, occurredAt, test: true, message, project }` β it shares
transport, signature, and retry behavior with real events but does not
include `agentRun`, `incident`, `events`, `pullRequests`, or
`linearTickets`. Use it to verify transport and signature only.
## Signing
Every request has a `Superlog-Signature` header in Stripe-style format:
```
Superlog-Signature: t=1715450000,v1=<hex>
```
`<hex>` is `HMAC_SHA256(secret, "<t>.<rawRequestBody>")`.
Verify in your handler:
```ts
import { createHmac, timingSafeEqual } from "node:crypto";
function verify(secret: string, header: string, rawBody: string): boolean {
const parts = Object.fromEntries(
header.split(",").map((p) => {
const i = p.indexOf("=");
return [p.slice(0, i), p.slice(i + 1)];
}),
);
const ts = Number(parts.t);
const v1 = parts.v1;
if (!Number.isFinite(ts) || !v1) return false;
// Reject anything older than 5 minutes to block replay attacks.
if (Math.abs(Math.floor(Date.now() / 1000) - ts) > 300) return false;
const expected = createHmac("sha256", secret).update(`${ts}.${rawBody}`).digest("hex");
const a = Buffer.from(expected);
const b = Buffer.from(v1);
return a.length === b.length && timingSafeEqual(a, b);
}
```
Important: verify against the **raw body**, before JSON-parsing. Frameworks
that re-stringify JSON will produce a different hash.
## Headers
| Header | Value |
| ----------------------- | ------------------------------------------------------ |
| `Superlog-Signature` | `t=<unix-ts>,v1=<hex>` HMAC-SHA256 over `<ts>.<body>` |
| `Superlog-Event` | Event type, e.g. `agent_run.completed` |
| `Superlog-Delivery` | UUID of this delivery β stable across retries |
| `User-Agent` | `Superlog-Webhooks/1.0` |
## `agent_run.completed` payload
```json
{
"event": "agent_run.completed",
"eventId": "5f0a6b6e-...",
"occurredAt": "2026-05-11T12:34:56.000Z",
"project": { "id": "uuid", "name": "Default", "slug": "default" },
"agentRun": {
"id": "uuid",
"state": "complete",
"runtime": "anthropic",
"completedAt": "2026-05-11T12:34:56.000Z",
"startedAt": "2026-05-11T12:20:00.000Z",
"cumulativeRuntimeMinutes": 14,
"resumeCount": 0,
"failureReason": null,
"result": {
"state": "complete",
"summary": "Root cause: missing null check in orders.ts:42",
"rootCauseConfidence": "high",
"rootCause": {
"text": "orders.ts:42 dereferences `customer` without checking for null.",
"confidence": 9
},
"estimatedImpact": {
"text": "~3% of /api/orders requests since deploy at 11:14 UTC.",
"confidence": 7
},
"severity": "SEV-2",
"pr": {
"selectedRepoFullName": "acme/orders",
"branchName": "superlog/fix-orders-typeerror",
"baseBranch": "main",
"openStatus": "opened",
"url": "https://github.com/acme/orders/pull/4271",
"patch": "diff --git a/orders.ts ...",
"validationPassed": true
},
"linearTicket": {
"id": "...",
"url": "https://linear.app/acme/issue/ENG-1234",
"createdByAgent": true
},
"noiseClassification": null,
"resolutionClassification": null
}
},
"incident": {
"id": "uuid",
"title": "TypeError in /api/orders",
"codename": "squishy-narwhal",
"status": "open",
"severity": "SEV-2",
"service": "orders",
"firstSeen": "2026-05-11T11:00:00.000Z",
"lastSeen": "2026-05-11T12:30:00.000Z",
"issueCount": 14,
"rootCauseText": "orders.ts:42 dereferences `customer` without checking for null.",
"rootCauseConfidence": 9,
"estimatedImpactText": "~3% of /api/orders requests since deploy at 11:14 UTC.",
"estimatedImpactConfidence": 7,
"suggestedSeverity": "SEV-2",
"noiseClassification": null,
"resolutionClassification": null,
"findingsAgentRunId": "uuid"
},
"events": [
{
"id": "uuid",
"kind": "agent_run_started",
"summary": "...",
"detail": { },
"createdAt": "2026-05-11T12:20:00.000Z"
}
],
"pullRequests": [
{
"id": "uuid",
"repoFullName": "acme/orders",
"prNumber": 4271,
"url": "https://github.com/acme/orders/pull/4271",
"branchName": "superlog/fix-orders-typeerror",
"baseBranch": "main",
"state": "open",
"title": "[superlog] Fix TypeError in /api/orders",
"mergedAt": null,
"closedAt": null
}
],
"linearTickets": [
{
"id": "uuid",
"workspaceId": "...",
"ticketId": "...",
"ticketIdentifier": "ENG-1234",
"url": "https://linear.app/acme/issue/ENG-1234",
"title": "Fix TypeError in /api/orders",
"state": "In Progress"
}
]
}
```
Field notes:
- `agentRun.state` is always `"complete"` for this event. Failed agent runs
and runs whose incident was merged into another do not fire a webhook.
- `agentRun.failureReason` is always `null` here for the same reason.
- `incident.status` is one of `"open" | "resolved" | "autoresolved_noise" | "merged"`.
Note that an `agent_run.completed` can fire while the incident is still
`"open"` β the agent run finished, but no one has resolved the incident
yet.
- `incident.severity` and `agentRun.result.severity` are
`"SEV-1" | "SEV-2" | "SEV-3" | null`. The incident's `suggestedSeverity`
mirrors the agent's pick for this run; `severity` is the value the user
(or worker) settled on.
- `incident.rootCauseText` / `rootCauseConfidence` / `estimatedImpactText` /
`estimatedImpactConfidence` are flattened from the latest successful
agent run's `result.rootCause` / `result.estimatedImpact` so the incident
row is the single source of truth for "what did we learn". The agent
run's raw `result` jsonb is preserved on `agentRun.result` for audit.
- `agentRun.result.rootCauseConfidence` is the coarse `"high" | "medium" | "low" | null`
rating. `agentRun.result.rootCause` and `agentRun.result.estimatedImpact`
are separate objects carrying a free-text `text` plus a numeric `confidence`
on a 0-10 scale (10 = backed by verbatim code/log/trace/ticket evidence;
0 = pure speculation).
- `agentRun.result.pr` is present when a PR was opened. Mutually-exclusive
with the no-PR conclusion fields `agentRun.result.noiseClassification` /
`agentRun.result.resolutionClassification`, which are set when the agent
decided not to ship a fix (the issue is noise, or already fixed in the
current code, or PR policy = never).
- `agentRun.result.linearTicket` is present when Linear is connected and a
ticket was filed.
- `pullRequests` and `linearTickets` mirror the same underlying records but
as the persistent state we track (PR state may change after the webhook
fires β e.g. a PR opened at agent-run-completion time might be merged
later; that mid-flight state is what's in the payload).
- The `agentRun.result` shape evolves with the agent runtime. Treat unknown
fields as additive.
## Best practices
- **Respond fast.** Return 2xx within 10 seconds. Do the actual work async.
- **De-dupe** on `Superlog-Delivery`. We may retry a delivery you already
processed (e.g. your server returned 200 but the connection dropped before we
saw it).
- **Verify the signature** on every request. Reject if the timestamp drifts
more than ~5 minutes from your clock.
- **Don't trust the URL host** for routing β anyone with the URL could try to
POST to it. The HMAC is your only proof.
|