Skip to main content
Superlog sends webhooks to your server when something happens to an incident in a project. They’re designed as messages to relay to an outgoing integration — Telegram, email, SMS, a status page, PagerDuty, your data warehouse, or any internal tooling — so every payload carries both render-ready text and the full structured state.

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:
For the simplest possible integration, subscribe to both events and forward message.title / message.body (see The message object) verbatim. You never have to parse the rest of the schema.
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.
Expand an endpoint to change its subscribed events later. You can disable, rotate the secret, or delete an endpoint at any time from Settings → Webhooks.
  • 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 POST with Content-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-Delivery UUID — 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 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.

Request headers

Every webhook request includes the following headers:

Verifying signatures

Every request is signed with HMAC-SHA256 using the whsec_ 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:
The signed string is <t>.<rawRequestBody>. Here is a complete TypeScript verification helper:
Always verify against the raw request body before JSON-parsing it. Frameworks that re-serialize JSON (reordering keys, stripping whitespace) will produce a different byte sequence and a different hash, causing every signature check to fail.

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.
Use 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 incident and agentRun objects follow the shared shapes in Common envelope above (that’s the source of truth); the example shows representative values, and its incident block is abbreviated relative to the full field list.
  • agentRun.state is always "complete" for this change. A failed run sends change.kind = "agent_failed"; a run whose incident was merged into another sends change.kind = "merged" instead.
  • agentRun.failureReason is always null here for the same reason.
  • incident.status is one of "open" | "resolved" | "autoresolved_noise" | "merged". Note that agent_completed can fire while the incident is still "open" — the run finished, but no one has resolved the incident yet.
  • incident.severity and agentRun.result.severity are "SEV-1" | "SEV-2" | "SEV-3" | null. incident.suggestedSeverity mirrors the agent’s pick for this run; incident.severity is the value a user or automated worker settled on.
  • incident.rootCauseText / rootCauseConfidence / estimatedImpactText / estimatedImpactConfidence are flattened from the latest successful run’s result.rootCause / result.estimatedImpact so the incident row is the single source of truth for “what did we learn”. The run’s raw result jsonb is preserved on agentRun.result for audit.
  • agentRun.result.rootCauseConfidence is a coarse "high" | "medium" | "low" | null rating. agentRun.result.rootCause and agentRun.result.estimatedImpact each carry a free-text text plus a numeric confidence on a 0–10 scale (10 = backed by verbatim code/log/trace/ticket evidence; 0 = speculation).
  • agentRun.result.pr is present when a fix PR was opened. It is mutually exclusive with agentRun.result.noiseClassification and agentRun.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.linearTicket is present when Linear is connected and a ticket was filed.
  • pullRequests and linearTickets reflect 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.result as 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, then change.kind. incident.created → start a new thread keyed on incident.id; incident.updated → look up that thread and post a reply (or edit), branching on change.kind. For the simplest integration, just forward message.title / message.body and 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.