Early access is open. Free plan during early access. Start free
Logthread

Guide

Alert webhooks

A Webhook channel sends each alert notification to an HTTPS URL of yours as JSON. Use it to open a ticket, post to your own chat bot, trigger a runbook or feed an incident tool. This page is the contract: the request, the payload, the signature, and what happens when your endpoint fails. To create the channel and test it, see Alerts.

The request#

Logthread sends POST with a JSON body to the URL you set on the channel.

HeaderValue
Content-Typeapplication/json
User-AgentLogthread-Alerts/1
X-Logthread-Eventopen (the incident just fired), repeat (still firing, reminder), resolve (it recovered). A Send test is delivered as open with "test": true in the body
X-Logthread-Signaturesha256=<hex>, only when you set a signing secret on the channel

The URL must be https://, with no username or password, and must resolve to a public address. Private, loopback, link-local and cloud metadata addresses are refused, both when you save the channel and again at every send. Redirects are never followed, and a request that does not answer within 10 seconds counts as failed.

The payload#

{
  "version": 1,
  "workspace": { "id": "ws_9f3k2", "name": "Acme", "time_zone": "UTC" },
  "rule": {
    "id": "alr_4d8x1",
    "name": "Gateway 5xx",
    "type": "scheduled_query",
    "severity": "critical"
  },
  "incident": {
    "id": "inc_7c2m9",
    "state": "firing",
    "started_at": "2026-10-30T09:41:12Z",
    "resolved_at": null,
    "value": 31,
    "threshold": 20,
    "service": "gateway",
    "pattern": null
  },
  "story_url": "https://logthread.com/explore?story=1",
  "incident_url": "https://logthread.com/alerts/incidents/inc_7c2m9"
}

The values above are an illustration. The fields are:

FieldTypeMeaning
versionnumberPayload version. Currently 1
workspace.id, .name, .time_zonestringThe workspace, and its time zone name (default UTC)
rule.id, .namestringThe alert rule
rule.typestringnew_pattern, error_spike, scheduled_query, metric_threshold, span_latency, absence or query_value (a computed value or a percentage)
rule.severitystringinfo, warning or critical
incident.idstringStable for the incident, and the same in open, repeat and resolve. Use it to deduplicate
incident.statestringfiring or resolved
incident.started_at, .resolved_atstring or nullRFC 3339 UTC. resolved_at is null while it fires
incident.value, .thresholdnumber or nullThe measured value and the rule's limit. Null for rules without one, such as a new pattern
incident.servicestring or nullThe service, when the rule is per service
incident.patternstring or nullThe new log pattern, for new_pattern rules
incident.groupstring or nullThe group (label value, service or operation) for metric, trace, absence and computed-value rules
incident.summarystringA readable sentence such as "p95(attr.gw.duration_ms) is 6230 (> 5000) over 10 minutes". Present for the same rule types as group
story_urlstringThe matching window in Explore
incident_urlstringThe incident in the app
testbooleanPresent and true only on a Send test

New-pattern rules never send resolve; their incidents close on their own after 24 hours. Treat unknown fields as additions and ignore them.

Verify the signature#

Set a Signing secret on the channel. Logthread then computes an HMAC with SHA-256 over the exact bytes of the request body, using your secret as the key, and sends sha256= plus the lowercase hex digest. Compute it over the raw body you received, before you parse the JSON: re-serialising can change the bytes. The body is compact JSON with sorted keys.

Python:

import hmac, hashlib

def verify(secret: str, raw_body: bytes, header: str) -> bool:
    expected = "sha256=" + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, header or "")

Node.js:

import { createHmac, timingSafeEqual } from "node:crypto";

export function verify(secret, rawBody, header = "") {
  const expected = "sha256=" + createHmac("sha256", secret).update(rawBody).digest("hex");
  const a = Buffer.from(expected);
  const b = Buffer.from(header);
  return a.length === b.length && timingSafeEqual(a, b);
}

Reject the request with a 401 when verify returns false. A channel without a secret sends no signature header, so set one for any endpoint that is reachable from the internet.

Reply and retries#

  • Answer with any 2xx status within 10 seconds. The body is ignored; the first 300 characters are kept in the channel's Delivery log to help you debug.
  • Any other answer, a redirect, a timeout or a connection error is a failure. Logthread retries after 30 seconds, then 1, 2, 4 and so on up to 30 minutes between attempts, and gives up after 8 attempts. The log then shows Failed with the last error.
  • Delivery is at least once, so your endpoint can see the same notification twice. Deduplicate on incident.id together with X-Logthread-Event.
  • Do your slow work after you answer: respond 200 first, then process.

Test it#

On the channel, choose Send test. The test arrives as an open event for an example service with "test": true, signed like a real one. A refused address or a failing endpoint is reported immediately and a test is not retried.

Next: put the alert's query on a dashboard.