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.
| Header | Value |
|---|---|
Content-Type | application/json |
User-Agent | Logthread-Alerts/1 |
X-Logthread-Event | open (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-Signature | sha256=<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:
| Field | Type | Meaning |
|---|---|---|
version | number | Payload version. Currently 1 |
workspace.id, .name, .time_zone | string | The workspace, and its time zone name (default UTC) |
rule.id, .name | string | The alert rule |
rule.type | string | new_pattern, error_spike, scheduled_query, metric_threshold, span_latency, absence or query_value (a computed value or a percentage) |
rule.severity | string | info, warning or critical |
incident.id | string | Stable for the incident, and the same in open, repeat and resolve. Use it to deduplicate |
incident.state | string | firing or resolved |
incident.started_at, .resolved_at | string or null | RFC 3339 UTC. resolved_at is null while it fires |
incident.value, .threshold | number or null | The measured value and the rule's limit. Null for rules without one, such as a new pattern |
incident.service | string or null | The service, when the rule is per service |
incident.pattern | string or null | The new log pattern, for new_pattern rules |
incident.group | string or null | The group (label value, service or operation) for metric, trace, absence and computed-value rules |
incident.summary | string | A readable sentence such as "p95(attr.gw.duration_ms) is 6230 (> 5000) over 10 minutes". Present for the same rule types as group |
story_url | string | The matching window in Explore |
incident_url | string | The incident in the app |
test | boolean | Present 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
2xxstatus 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.idtogether withX-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.