Rolefy

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.

  • id is unique to the event and identical on every retry. Store it and ignore a delivery whose id you have already processed.
  • Events are sent within about a minute. A delivery that doesn't get a 2xx answer 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 the X-Rolefy-Attempt header instead (1 for the first try, 2 for the first retry, ...). GET /v1/webhooks/{id}/deliveries shows every delivery and why it failed.
  • Answer with any 2xx status 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.

Webhook delivery flow