Documentation / Guides
Webhooks
Subscribe to application.created, application.status_changed, or job.status_changed
through the /v1/webhooks endpoints. Each delivery is POSTed to your target_url as JSON:
{
"id": "6f1c0a52-3d8e-4b7a-9c41-2e5d8a7b0f13",
"event": "application.created",
"data": { "application_id": "...", "job_id": "...", "external_job_id": "..." },
"created_at": "2026-03-14T09:30:00.000Z"
}
The application.created event is deliberately small. It tells you an application exists;
fetch the full application, including the candidate's details, resume link and answers, with
GET /v1/applications/{id}. Personal details are therefore only sent when you ask for them,
over an authenticated call.
idis unique to the event and identical on every retry. Store it and ignore a delivery whoseidyou have already processed.- Events are sent within about a minute. A delivery that doesn't get a
2xxanswer within 10 seconds is retried after 1 minute, 5 minutes, 30 minutes, 2 hours and 12 hours. After the sixth failed attempt it is marked failed. Redirects are not followed. The body is byte-for-byte identical on every retry. The attempt number is sent in theX-Rolefy-Attemptheader instead (1for the first try,2for the first retry, ...).GET /v1/webhooks/{id}/deliveriesshows every delivery and why it failed. - Answer with any
2xxstatus as soon as you have stored the event. Do the real work after.
Verifying the signature
Every delivery carries X-Rolefy-Signature: t=<unix ms>,v1=<hex hmac>. v1 is the
HMAC-SHA256, keyed with the secret you were given when the subscription was created
(shown once, like your client secret), of the string <t>.<raw request body>. t is when
that attempt was signed. A retry gets a fresh one, so reject any delivery whose t is
more than a few minutes from your clock to defeat replays.
const [t, v1] = header.split(",").map((part) => part.split("=")[1]);
const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
const valid =
v1.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(v1), Buffer.from(expected)) &&
Math.abs(Date.now() - Number(t)) < 5 * 60 * 1000;
Compute the HMAC over the raw bytes you received, not a re-serialized copy of the parsed JSON.
