The delivery request
TraceItX sends a POST to your subscriber URL with the
envelope as the JSON body and these headers:
| Header | Value |
|---|---|
X-TraceItX-Signature | t=<unix-seconds>,v1=<hmac-hex> |
X-TraceItX-Event | report.received |
X-TraceItX-Delivery-Id | Unique per attempt (fresh on retry) |
X-TraceItX-Attempt | 1-indexed attempt number |
X-TraceItX-Subscriber-Id | Your subscriber’s id |
Content-Type | application/json |
User-Agent | TraceItX-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:
| Field | Value |
|---|---|
data.reporter.tier | verified, self_declared, or anonymous |
data.reporter.identity | { id, label, verified }, or null when the tier is anonymous |
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);
} 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.
X-TraceItX-Delivery-Id and an
X-TraceItX-Attempt counter, so your receiver can reconcile retries
and stay idempotent on its own side.