Trace Developers

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.