TraceCrowd beta
Docs · Integrations

Webhook integration

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.

Stable payload HMAC-SHA256 signatures Delivery ids for dedupe

Setting one up

  1. In the app, go to Integrations and click Add new integration.
  2. Pick Webhook under the “Webhooks” category.
  3. Paste the HTTPS endpoint that should receive alerts. It must be reachable from the public internet — TraceCrowd runs a probe POST before saving and will refuse URLs that 404 or don’t resolve.
  4. (Optional) Set a signing secret. If you do, every POST will include an X-TraceCrowd-Signature header computed over the raw body. Leave it blank and the header is omitted.
  5. After saving, attach the integration to any monitor’s alert routing. Use Send test from the integration card to verify end-to-end delivery.

Delivery

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.

Payload envelope

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.

The 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.

Signature verification

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.

Verify in Node.js

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.

Verify in Python

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="):])

Wiring it into common tools

PagerDuty (Events API v2)

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.

Discord

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.

Zapier / Make / n8n

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.

Your own service

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.

Troubleshooting

  • Integration won’t save. The probe POST must reach your endpoint. 404 and DNS failures are rejected; 2xx/3xx/4xx (other than 404) are accepted, so an endpoint that returns 401/403 to unsigned pings is fine.
  • Signature doesn’t verify. You’re almost certainly hashing the wrong bytes. Use the raw request body exactly as received; don’t parse+re-serialize JSON first.
  • Duplicate notifications downstream. De-dupe on X-TraceCrowd-Delivery. If your deduping is per-incident (e.g. one active PagerDuty incident per monitor), use incident.id.
  • Delivery logged as failed. Check the incident timeline in TraceCrowd — we record the HTTP status and a snippet of the response body for every failed POST.

Deliberately not in scope

  • No retries. If your receiver is down, the alert is logged as failed on the incident and not re-sent.
  • No per-monitor payload templates. The envelope is stable across all integrations so your transforms don’t break on a UI change.
  • No batch deliveries. Each down/up event is its own POST.
Something unclear?
Email alerts@tracecrowd.com with the part that tripped you up.
Open Integrations →