The model: two events
There are exactly two events, and they map directly onto how a messaging integration thinks:
Everything that used to be its own event — resolve, reopen, merge, an investigation starting / finishing / failing / asking for input — is now an
incident.updated distinguished by a change.kind field. The incident is the “thread”; an agent run (investigation) is just activity on it.
change.kind is one of:
Each endpoint chooses whether it receives
incident.created, incident.updated, or both (both by default).
Configuring an endpoint
1
Open Webhook Settings
Navigate to Settings → Webhooks for the project that should send events.
2
Add your endpoint URL
Paste your HTTPS endpoint URL (e.g.
https://your-app.example.com/hooks/superlog) into the URL field.3
Pick the events
Tick the events this endpoint should receive. Both are selected by default.
4
Save and copy the signing secret
Click Add endpoint. Superlog immediately generates a signing secret prefixed
whsec_ and shows it to you once. Copy it to your secrets manager — it is never displayed again.5
Verify transport with Send test
Use the Send test button to fire a stub payload to your endpoint. The test delivery appears in the live delivery log under the endpoint so you can confirm the request arrived and your server returned a 2xx.
- Disable: suspends delivery without deleting history. Re-enable at any time.
- Rotate secret: generates a new
whsec_secret immediately. Update your handler before rotating to avoid a gap. - Delete: permanently removes the endpoint and all its delivery history.
The Send test payload has the shape
{ event, eventId, occurredAt, test: true, message: { title, body }, project } and does not include incident, change, agentRun, events, pullRequests, or linearTickets. Use it to verify transport and signature only.Delivery semantics
- Events are delivered as
POSTwithContent-Type: application/json. - The request timeout is 10 seconds. Any response that takes longer is treated as a failure.
- Non-2xx responses, connection errors, and timeouts are all retried with the following backoff schedule:
After all 8 attempts fail (~8 hours elapsed), the delivery is marked
failed and Superlog stops retrying.
- Automatic retries reuse the same
Superlog-DeliveryUUID — use it as your idempotency key. - Manual redeliver from Settings enqueues a brand-new delivery row with a new UUID. If you redeliver something that was already delivered successfully, your receiver will see it twice.
- Disabling an endpoint stops new deliveries from being enqueued; any deliveries still pending when the endpoint is disabled are marked
failedwithlastError = "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.
Request headers
Every webhook request includes the following headers:Verifying signatures
Every request is signed with HMAC-SHA256 using thewhsec_ secret you saved at endpoint creation. You must verify this signature on every request before processing the payload.
The Superlog-Signature header has the format:
<t>.<rawRequestBody>. Here is a complete TypeScript verification helper:
Common envelope
Both events share the same top-level shape:incident.updated additionally carries a change object (always), and — for agent-related changes — an agentRun object (and, on agent_completed, the events / pullRequests / linearTickets arrays). Treat any unknown top-level keys as additive.
The message object
The single most useful field for a relay: a pre-rendered, channel-neutral { title, body } you can forward to Telegram/email/SMS without understanding the rest of the schema. title is the incident title (the “thread subject”); body is a one-line description of what just happened — e.g. "Resolved: looks fine now", "Investigation complete: <summary>. Opened PR: <url>", or "Reopened — the underlying issue regressed." For anything richer, read the structured fields below.
The incident object
Present on every event. A curated projection of the incident (Slack anchors, billing cooldowns, and other operational columns are intentionally omitted):
The agentRun object
Present on incident.updated when change.kind is one of the agent_* kinds:
incident.created
Just the common envelope — { event, eventId, occurredAt, project, message, incident } with incident.status = "open". Relay it as a new message / open a new thread keyed on incident.id.
incident.updated
The common envelope plus a change object. change.kind tells you what happened; switch on it to decide how to render the reply. Some kinds add fields to change; agent kinds add an agentRun (and agent_completed adds the events / pullRequests / linearTickets arrays).
change.kind = "resolved"
Fires for every path that closes an incident, including auto-close as noise.
change.resolution.status to tell a real resolve ("resolved") from a noise auto-close ("autoresolved_noise"). On the noise path, resolution.kind / reasonCode / resolvedAt are filled from the incident’s noise-close metadata.
change.kind = "reopened"
change.kind = "merged"
The incident is the source (now status: "merged"); change.mergedInto is the survivor it folded into.
change.kind = "agent_started"
change is just { "kind": "agent_started" }; the payload adds agentRun (state running, result: null).
change.kind = "agent_failed"
change is just { "kind": "agent_failed" }; the payload adds agentRun with state: "failed", failureReason set, and failureCategory derived from it.
change.kind = "agent_awaiting_input"
change.kind = "agent_completed"
The richest update — see the full example below.
agent_completed payload (full example)
When change.kind = "agent_completed", the incident.updated payload — in addition to agentRun — embeds the run’s chronological events, opened pullRequests, and filed linearTickets.
Field notes
- The
incidentandagentRunobjects follow the shared shapes in Common envelope above (that’s the source of truth); the example shows representative values, and itsincidentblock is abbreviated relative to the full field list. agentRun.stateis always"complete"for this change. A failed run sendschange.kind = "agent_failed"; a run whose incident was merged into another sendschange.kind = "merged"instead.agentRun.failureReasonis alwaysnullhere for the same reason.incident.statusis one of"open" | "resolved" | "autoresolved_noise" | "merged". Note thatagent_completedcan fire while the incident is still"open"— the run finished, but no one has resolved the incident yet.incident.severityandagentRun.result.severityare"SEV-1" | "SEV-2" | "SEV-3" | null.incident.suggestedSeveritymirrors the agent’s pick for this run;incident.severityis the value a user or automated worker settled on.incident.rootCauseText/rootCauseConfidence/estimatedImpactText/estimatedImpactConfidenceare flattened from the latest successful run’sresult.rootCause/result.estimatedImpactso the incident row is the single source of truth for “what did we learn”. The run’s rawresultjsonb is preserved onagentRun.resultfor audit.agentRun.result.rootCauseConfidenceis a coarse"high" | "medium" | "low" | nullrating.agentRun.result.rootCauseandagentRun.result.estimatedImpacteach carry a free-texttextplus a numericconfidenceon a 0–10 scale (10 = backed by verbatim code/log/trace/ticket evidence; 0 = speculation).agentRun.result.pris present when a fix PR was opened. It is mutually exclusive withagentRun.result.noiseClassificationandagentRun.result.resolutionClassification, which are set when the agent decided not to ship a fix (the issue is noise, already fixed in current code, or PR policy = never).agentRun.result.linearTicketis present when Linear is connected and a ticket was filed.pullRequestsandlinearTicketsreflect the persistent state Superlog tracks at the moment the webhook fires. A PR that was"open"at dispatch time may be merged or closed later — Superlog does not send a second event when that happens.- Treat unknown fields in
agentRun.resultas additive. The agent result shape evolves over time.
Best practices
Following these practices protects your integration against retries, replay attacks, and spoofed requests.
- Route on
event, thenchange.kind.incident.created→ start a new thread keyed onincident.id;incident.updated→ look up that thread and post a reply (or edit), branching onchange.kind. For the simplest integration, just forwardmessage.title/message.bodyand ignore the rest. - Respond fast. Return a 2xx within 10 seconds and do the real work asynchronously. Slow handlers cause retries that your de-duplication logic must handle.
- De-duplicate on
Superlog-Delivery. Superlog may retry a delivery your server already processed — for example, if your server returned 200 but the connection dropped before Superlog saw it. Store the delivery UUID on receipt and skip processing if you have seen it before. - Verify the signature on every request. Reject requests where the timestamp drifts more than 5 minutes from your clock. This protects against replay attacks using captured payloads.
- Don’t rely on the URL for authenticity. Anyone who discovers your webhook URL could POST to it. The HMAC signature is your only cryptographic proof that the payload came from Superlog.