Every TraceCrowd alert can be delivered as a signed JSON POST to any HTTPS endpoint you control. That makes the webhook integration the universal adapter — it’s how you wire TraceCrowd into PagerDuty, Opsgenie, Discord, Zapier, or your own services.
X-TraceCrowd-Signature header computed over the raw body. Leave it blank and the header is omitted.Every alert is an HTTPS POST with a JSON body and these headers:
Content-Type: application/json
User-Agent: TraceCrowd/1.0
X-TraceCrowd-Event: down | up | test
X-TraceCrowd-Delivery: <uuid, fresh per POST>
X-TraceCrowd-Signature: sha256=<hex> (only when a secret is configured)
Your endpoint should respond with a 2xx status code. Any 3xx/4xx/5xx response is treated as a failed delivery and logged against the incident. We do not currently retry failed deliveries — if your receiver can’t process a POST, the incident timeline will show an alert_failed entry.
Dedupe. The X-TraceCrowd-Delivery UUID is fresh on every POST. If your receiver is at-least-once by design (queues, multi-worker), key idempotent work on this id.
All three event types share the same top-level shape. type tells you which variant it is; down and up carry extra fields.
down — an incident opened{
"type": "down",
"at": "2026-04-21T10:22:47.311Z",
"monitor": {
"id": "mon_01HX...",
"name": "api-checkout",
"url": "https://api.acme.com/health"
},
"incident": {
"id": "inc_01HX...",
"links": {
"incident": "https://app.tracecrowd.com/r/...",
"monitor": "https://app.tracecrowd.com/r/..."
}
},
"cause": "connection timed out",
"statusCode": null
}
cause is a human-readable failure reason (HTTP status mismatch, keyword missing, TLS handshake error, connection timeout, …). statusCode is the HTTP code returned by the target, or null for transport-level failures.
up — the incident recovered{
"type": "up",
"at": "2026-04-21T10:29:02.108Z",
"monitor": { "id": "mon_01HX...", "name": "api-checkout", "url": "https://api.acme.com/health" },
"incident": {
"id": "inc_01HX...",
"links": {
"incident": "https://app.tracecrowd.com/r/...",
"monitor": "https://app.tracecrowd.com/r/..."
}
},
"startedAt": "2026-04-21T10:22:47.311Z"
}
The incident.id matches the earlier down event for the same incident. startedAt is when the incident was first opened — useful for computing downtime without looking anything up.
test — from the “Send test” button{
"type": "test",
"at": "2026-04-21T10:20:00.000Z",
"monitor": { "id": "test-monitor", "name": "<your integration name>", "url": "https://example.com" },
"incident": {
"id": "test-incident",
"links": {
"incident": "https://app.tracecrowd.com/incidents/test-incident",
"monitor": "https://app.tracecrowd.com/monitors/test-monitor"
}
}
}
Test envelopes carry placeholder monitor/incident ids. If your receiver treats type: "test" specially (e.g. acks a smoke-test channel), key off that instead of the ids.
linksThe incident and monitor URLs route through TraceCrowd’s attribution layer (/r/<token>) before redirecting to the real page, so we can count clicks per channel. Treat them as opaque — do not parse or rewrite them. They expire only when the underlying incident is deleted.
If you set a signing secret on the integration, every POST includes:
X-TraceCrowd-Signature: sha256=<hex lowercase>
The signature is the HMAC-SHA256 of the raw request body, using your configured secret as the key. Compare it with a constant-time equality check.
import { createHmac, timingSafeEqual } from "node:crypto";
export function verifyTraceCrowd(rawBody, header, secret) {
if (!header?.startsWith("sha256=")) return false;
const expected = createHmac("sha256", secret).update(rawBody).digest("hex");
const got = header.slice("sha256=".length);
const a = Buffer.from(expected, "hex");
const b = Buffer.from(got, "hex");
return a.length === b.length && timingSafeEqual(a, b);
}
Pass the raw request body — if your framework parses the JSON before you see it, the re-serialized bytes will not match. In Express, reach for express.raw({ type: "application/json" }) on the webhook route. In Fastify, use rawBody: true on the route options.
import hmac, hashlib
def verify_tracecrowd(raw_body: bytes, header: str | None, secret: str) -> bool:
if not header or not header.startswith("sha256="):
return False
expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, header[len("sha256="):])
Run a tiny transform service in front of PagerDuty’s Events API. Map type: "down" to event_action: "trigger", type: "up" to event_action: "resolve", and use incident.id as the dedup_key so trigger/resolve match up. A 20-line Cloudflare Worker or AWS Lambda is plenty.
Point the URL at a Discord channel webhook. Because TraceCrowd’s envelope isn’t Discord-shaped, run a small transform first — map the fields into Discord’s content and embeds.
Use each tool’s “Webhook” trigger and paste its receive URL into the integration. The envelope is stable, so subsequent steps (Slack, Jira, Notion, …) can reference specific fields directly.
Ship an /internal/tracecrowd HTTPS endpoint, verify the signature, and branch on type. Keep the handler fast — acknowledge with 2xx and queue any heavy work for a background job.
X-TraceCrowd-Delivery. If your deduping is per-incident (e.g. one active PagerDuty incident per monitor), use incident.id.