Skip to content
Menu
On this page
  1. Events
  2. Verifying signatures
  3. Retries and replay
  4. Test events

Recruit100x API v1

Webhooks

Every event Recruit100x can send, its schema and sample, how deliveries are signed and retried, and how to test them.

Subscribe with POST /api/v1/hooks (scope manage:hooks) or in Settings › Integrations › Webhooks. Choose the payload version per endpoint with api_version:

  • 2026-09-01 — the original payloads, frozen: fields are only ever added.
  • 2026-11-01 — adds your own ids (external_ref), the invite, band counts, livemode, and the role.* events.

Events

EventWhenScopeSince
invite.sentAn interview invitation was sent (or, with return_link, created for you to deliver).read:roles2026-09-01
invite.openedThe candidate opened their invitation link for the first time.read:roles2026-09-01
screen.completedThe candidate finished the interview; the report is being written.read:reports2026-09-01
report.readyThe report is ready — the same payload as GET /api/v1/reports/{reportId}.read:reports2026-09-01
report.failedThe report is delayed; we keep trying and send report.ready when it is done.read:reports2026-09-01
decision.recordedA person on your team recorded a decision (advance, reject or hold).read:reports2026-09-01
client_feedback.receivedYour client left feedback on a shared shortlist.read:reports2026-09-01
candidate.sms_opted_outThe candidate opted out of text messages.read:roles2026-09-01
shortlist.createdA shortlist link was created for your client.read:reports2026-09-01
candidate.handoffAn advanced candidate was sent on to your ATS.read:reports2026-09-01
role.readyThe role's interview guide was approved: you can invite candidates.read:roles2026-11-01
role.closedThe role was closed.read:roles2026-11-01
role.guide_failedThe role's interview guide could not be drafted.read:roles2026-11-01

Each event's JSON Schema and a sample are published: webhooks.2026-11-01.schema.json, webhooks.2026-09-01.schema.json, and /developers/samples/{event}.json. There is no per-candidate candidate-care event; use the monthly stats (GET /api/v1/stats/candidate-care).

Verifying signatures

Each delivery is signed per Standard Webhooks with your endpoint's secret: headers webhook-id, webhook-timestamp and webhook-signature, plus x-recruit100x-event. Verify the raw body before you trust it:

Node

const crypto = require("node:crypto");

// secret: the whsec_… value shown once when you created the endpoint.
// headers: the request headers (lower-case names). rawBody: the body exactly as received.
function verifyRecruit100xWebhook(secret, headers, rawBody) {
  const id = headers["webhook-id"];
  const timestamp = headers["webhook-timestamp"];
  const signatures = headers["webhook-signature"] || "";
  if (!id || !timestamp) return false;
  // Refuse messages older or newer than 5 minutes (replay protection).
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
  const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64");
  const expected = "v1," + crypto
    .createHmac("sha256", key)
    .update(id + "." + timestamp + "." + rawBody)
    .digest("base64");
  // During a secret rotation the header carries two values; either may match.
  return signatures.split(" ").some((candidate) =>
    candidate.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(candidate), Buffer.from(expected)));
}

module.exports = { verifyRecruit100xWebhook };

Python

import base64, hashlib, hmac, time

# secret: the whsec_... value shown once when you created the endpoint.
# headers: a dict of request headers (lower-case names). raw_body: the body bytes as received.
def verify_recruit100x_webhook(secret, headers, raw_body):
    msg_id = headers.get("webhook-id")
    timestamp = headers.get("webhook-timestamp")
    signatures = headers.get("webhook-signature", "")
    if not msg_id or not timestamp:
        return False
    # Refuse messages older or newer than 5 minutes (replay protection).
    if abs(time.time() - int(timestamp)) > 300:
        return False
    key = base64.b64decode(secret.removeprefix("whsec_"))
    signed = f"{msg_id}.{timestamp}.".encode() + raw_body
    expected = "v1," + base64.b64encode(hmac.new(key, signed, hashlib.sha256).digest()).decode()
    # During a secret rotation the header carries two values; either may match.
    return any(hmac.compare_digest(candidate, expected) for candidate in signatures.split(" "))

PHP

<?php
// $secret: the whsec_… value shown once when you created the endpoint.
// $headers: request headers with lower-case names. $rawBody: the body exactly as received.
function verifyRecruit100xWebhook(string $secret, array $headers, string $rawBody): bool
{
    $id = $headers['webhook-id'] ?? '';
    $timestamp = $headers['webhook-timestamp'] ?? '';
    $signatures = $headers['webhook-signature'] ?? '';
    if ($id === '' || $timestamp === '') {
        return false;
    }
    // Refuse messages older or newer than 5 minutes (replay protection).
    if (abs(time() - (int) $timestamp) > 300) {
        return false;
    }
    $key = base64_decode(preg_replace('/^whsec_/', '', $secret));
    $expected = 'v1,' . base64_encode(hash_hmac('sha256', $id . '.' . $timestamp . '.' . $rawBody, $key, true));
    // During a secret rotation the header carries two values; either may match.
    foreach (explode(' ', $signatures) as $candidate) {
        if (hash_equals($expected, $candidate)) {
            return true;
        }
    }
    return false;
}

Retries and replay

A delivery that doesn't get a 2xx answer is retried after 5 seconds, 5 minutes, 30 minutes, 2 hours, 5 hours, 10 hours and 10 hours. Use webhook-id to ignore a delivery you already handled. Failed deliveries can be replayed from Settings › Integrations.

Test events

POST /api/v1/hooks/{hookId}/test sends a signed test.ping to one endpoint now. In test mode the test page sends the interview events to your test endpoints.

View as Markdown · OpenAPI · llms.txt