Skip to content
TraceItX Docs
Documentation

Webhooks

Each report is delivered to your subscriber URL as a signed HTTP POST. Verify it, return 2xx, and the full bug context is in your hands.

The delivery request

TraceItX sends a POST to your subscriber URL with the envelope as the JSON body and these headers:

HeaderValue
X-TraceItX-Signaturet=<unix-seconds>,v1=<hmac-hex>
X-TraceItX-Eventreport.received
X-TraceItX-Delivery-IdUnique per attempt (fresh on retry)
X-TraceItX-Attempt1-indexed attempt number
X-TraceItX-Subscriber-IdYour subscriber’s id
Content-Typeapplication/json
User-AgentTraceItX-Webhook/1.0

Who filed the report

The body wraps the envelope as data.report, alongside data.reporter — TraceItX’s own answer about who filed it, resolved server-side:

FieldValue
data.reporter.tierverified, self_declared, or anonymous
data.reporter.identity{ id, label, verified }, or null when the tier is anonymous
Use data.reporter, not data.report.reporter.user. The latter is whatever the app passed to setUser — an unauthenticated claim in every case, even when a valid identity token was also presented. Only tier: "verified" is proof. And anonymous is not “unverified”: it means no person was resolved at all.

Verifying the signature

The signature is an HMAC-SHA256 over <timestamp>.<raw-request-body>, keyed with your subscriber’s signing secret. Compute it over the raw bytes of the body (before any JSON parsing) and compare in constant time. This is the Stripe-style scheme, so existing helpers translate directly.

import crypto from 'node:crypto';

function verifyTraceItX(rawBody, header, secret, toleranceSec = 300) {
  const parts = Object.fromEntries(header.split(',').map(p => p.split('=')));
  const t = Number(parts.t);

  // 1. Reject stale timestamps (replay guard)
  if (Math.abs(Date.now() / 1000 - t) > toleranceSec) return false;

  // 2. Recompute and compare in constant time
  const expected = crypto.createHmac('sha256', secret)
    .update(`${t}.${rawBody}`)
    .digest('hex');
  const a = Buffer.from(expected), b = Buffer.from(parts.v1);
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}
Use the raw body. Frameworks that auto-parse JSON re-serialise it differently, which breaks the HMAC. Capture the raw body for the signed route (for example, express.raw()), verify, then parse.

Responding

Return any 2xx quickly to acknowledge receipt, then process asynchronously. Non-2xx responses are treated as failures and retried (see below). Attachments are referenced in the envelope with short-lived presigned url / expiresAt pairs — download the bytes you need promptly, as the URLs expire about an hour after delivery.

Retries

If a delivery fails, TraceItX retries on an increasing backoff — roughly: immediately, then after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours, 12 hours, 24 hours, 36 hours, and 72 hours. After 10 failed attempts (about four days) the delivery is marked dead-letter.

How responses are classified

  • 2xx → success.
  • 408, 429, 5xx, timeouts, network errors → retried until the schedule is exhausted.
  • Other 4xx (bad config) → dead-lettered immediately, no retries.
  • Blocked destination (SSRF protection) → dead-lettered immediately.

Circuit breaker

If a subscriber accumulates several consecutive dead-letters, it is automatically disabled with a clear reason so a broken endpoint can’t silently swallow your reports. Re-enable it from the admin console once you’ve fixed the receiver; the delivery log shows the response code and attempt history for every delivery.

Every attempt carries a fresh X-TraceItX-Delivery-Id and an X-TraceItX-Attempt counter, so your receiver can reconcile retries and stay idempotent on its own side.