Webhooks
BetaReceive a signed callback when a risk job reaches a terminal state, instead of polling.
Beta
Overview
Register an HTTPS endpoint and we'll POST a signed event to it whenever one of your jobs reaches a terminal state. Two events are delivered:
| Event | Sent when |
|---|---|
job.completed | A job finished successfully. |
job.failed | A job stopped with an error. |
Webhooks are scoped to your organization — you only receive events for jobs you submitted. They complement, not replace, polling: you can still call GET /v2/risk/{job_id} at any time.
Register a webhook
Create a subscription with the HTTPS URL to call. Optionally restrict it to specific events:
POST /v2/webhooks
The response includes a signing secret. It is shown only once — store it securely; you'll use it to verify every delivery.
Created
Delivery payload
Each delivery is a POST with a JSON body and these headers:
| Header | Description |
|---|---|
X-R6-Event | The event type (job.completed or job.failed). |
X-R6-Webhook-Id | Stable delivery id — use it to deduplicate retries. |
X-R6-Timestamp | Unix-seconds timestamp the signature was computed at. |
X-R6-Signature | Hex HMAC-SHA256 signature (see below). |
POST to your endpoint
Webhook delivery bodies use camelCase field names (jobId, pollUrl, and so on). Use pollUrl to fetch the full job result via the poll endpoint; the completed response object inside that poll uses snake_case wire fields (job_id, metadata, categories, source_ids, and so on).
Verify signatures
Every request is signed so you can confirm it came from Relativity6. The signature is HMAC-SHA256 over the string `${timestamp}.${rawBody}` using your subscription secret, hex-encoded.
To verify: read X-R6-Timestamp and X-R6-Signature, recompute the HMAC over the raw request body, and compare in constant time.
import { createHmac, timingSafeEqual } from "node:crypto";
function verifyR6Webhook(rawBody, headers, secret) {
const timestamp = headers["x-r6-timestamp"];
const signature = headers["x-r6-signature"];
// Reject stale deliveries (5-minute replay window).
const ageSeconds = Math.abs(Date.now() / 1000 - Number(timestamp));
if (!timestamp || ageSeconds > 300) return false;
const expected = createHmac("sha256", secret)
.update(`${timestamp}.${rawBody}`)
.digest("hex");
const a = Buffer.from(expected);
const b = Buffer.from(signature ?? "");
return a.length === b.length && timingSafeEqual(a, b);
}
Use the raw body
Compute the HMAC over the exact bytes you received, before any JSON parsing or re-serialization. Re-stringifying the body can change whitespace or key order and break verification.
Reliability
- Respond fast with a 2xx. Acknowledge receipt, then do your processing asynchronously. Non-2xx responses are retried.
- Expect retries and duplicates. Deliveries are retried on failure, so the same event may arrive more than once. Deduplicate on
X-R6-Webhook-Id. - Treat the webhook as a trigger. The authoritative result always lives at
pollUrl; fetch it if you need the full payload.
Manage subscriptions
- List your subscriptions:
GET /v2/webhooks - Delete a subscription:
DELETE /v2/webhooks/{id}
The list view never returns the signing secret. If you lose a secret, delete the subscription and create a new one.