Webhooks
Trace pushes rather than expecting you to poll. Register an endpoint under
Integrations, choose the events you care about, and every matching change arrives as a
signed POST.
Events
| Event | Fires when |
|---|---|
photo.uploaded |
A photo finished uploading and was filed to a project. |
photo.updated |
A photo was retagged, described or moved to another project. |
project.created |
A project was created, including from the mobile app. |
report.generated |
A PDF report finished rendering. |
report.shared |
A public share link was enabled for a report. |
The envelope
Every delivery has the same outer shape. data is the resource, in exactly
the form the REST API returns it — so an integrator writes one parser, not two.
{
"event": "photo.uploaded",
"created_at": "2026-08-19T18:40:02+00:00",
"team": { "id": 3, "name": "Northpoint Exteriors" },
"data": { … the resource … }
}
Headers
X-Trace-Event |
The event name, so you can route without parsing the body. |
X-Trace-Delivery |
A unique id for this attempt chain. Use it to deduplicate — a retry reuses it. |
X-Trace-Timestamp |
Unix seconds. Part of the signed payload. |
X-Trace-Signature |
sha256=… — the HMAC described below. |
Verifying a delivery
The signature is an HMAC-SHA256 over timestamp + "." + raw body, keyed with
the endpoint secret. Verify against the raw body — re-encoding the JSON first
will change the bytes and the check will fail.
$timestamp = $request->header('X-Trace-Timestamp');
$signature = $request->header('X-Trace-Signature');
$expected = 'sha256=' . hash_hmac(
'sha256',
$timestamp . '.' . $request->getContent(),
$endpointSecret
);
// Constant time. A plain === leaks the secret one byte at a time.
if (! hash_equals($expected, $signature)) {
abort(401);
}
// Reject anything older than five minutes, or a captured delivery can be
// replayed at leisure.
if (abs(time() - (int) $timestamp) > 300) {
abort(401);
}
Retries
Any 2xx is success. Anything else is retried with exponential backoff up to
5 attempts. An endpoint that fails
25 times in a row is muted
and flagged in the office — a dead URL should stop generating noise, and somebody should
be told rather than left to notice.
Delivery history is kept for 30
days, with the request and response body of each attempt, and can be replayed from the
Integrations screen.
Answer fast, work later.
Trace waits 10 seconds. Acknowledge
with a 200 and queue your own processing; doing the work inline is how a slow
downstream system turns into a muted endpoint.