Integration API
RecruitScreen pushes screening results into your HR or ATS system — the moment a candidate finishes their video screen, and the moment a recruiter makes a decision. Campaigns and screening stay in RecruitScreen; your system of record stays up to date automatically.
Get started — 15 minutes Full API reference →
Overview
There are two ways to receive data. Use whichever suits your stack — they carry identical payloads.
| Channel | How it works | Best for |
|---|---|---|
| Webhooks (push) | We POST signed JSON to your HTTPS endpoint in real time, with automatic retries. | Modern systems, middleware (Power Automate, Zapier, Workato), custom receivers. |
| Events API (pull) | You poll GET /v1/events on a schedule with a cursor. Nothing is ever missed or duplicated. | Systems that can't receive inbound HTTP; scheduled batch jobs; older middleware. |
Base URL: https://api.recruitscreen.app/v1
Quickstart
- Get an API key. A RecruitScreen workspace admin creates one under Settings → API. Keys look like
rs_live_…and are shown once. - Check it works:
curl https://api.recruitscreen.app/v1/ping \ -H "Authorization: Bearer rs_live_YOUR_KEY" # → {"ok":true,"organization":{"id":"…","name":"Your Agency"}} - Add a webhook endpoint (in Settings → API, or via the API). Store the returned
whsec_…signing secret.curl -X POST https://api.recruitscreen.app/v1/webhook-endpoints \ -H "Authorization: Bearer rs_live_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{"url":"https://your-system.example.com/webhooks/recruitscreen"}' - Send a test event — click Send test in the portal or:
curl -X POST https://api.recruitscreen.app/v1/webhook-endpoints/{id}/test \ -H "Authorization: Bearer rs_live_YOUR_KEY" - Verify the signature (snippets below) and you're live.
Authentication
Every request needs a per-organization API key in the Authorization header:
Authorization: Bearer rs_live_35b0…
Keys are created and revoked by workspace admins in Settings → API (up to 10 active keys). They're stored hashed on our side and shown only once at creation. Rotate by creating a new key, switching your integration over, then revoking the old one — both work during the overlap.
Webhooks
| Event | Fires when |
|---|---|
candidate.screen_completed | A candidate finishes recording all their answers. |
candidate.decision_made | A recruiter records a decision — shortlist, maybe, or reject (with rating and notes). |
test.ping | You send a test from the portal or API. |
Example payload — candidate.decision_made
{
"id": "evt_9f2c1d8a44b0e6a2c3d1",
"type": "candidate.decision_made",
"created_at": "2026-07-03T04:12:09Z",
"data": {
"candidate": {
"id": "c3d4e5f6-…",
"campaign_id": "a1b2c3d4-…",
"full_name": "Jane Doe",
"email": "jane@example.com",
"external_ref": "DAYFORCE-12345",
"status": "completed",
"stage": "reviewed",
"rejected_at": null,
"completed_at": "2026-07-02T23:41:00Z",
"created_at": "2026-07-02T23:20:00Z"
},
"campaign": { "id": "a1b2c3d4-…", "name": "Night Shift Supervisor — Bairnsdale" },
"evaluation": {
"rating": 4,
"decision": "shortlist",
"notes": "Strong comms, available immediately",
"decided_at": "2026-07-03T04:12:01Z"
},
"review_url": "https://recruitscreen.indxtree.com/portal/candidates/c3d4e5f6-…"
}
}
review_url links to the candidate's screen in the RecruitScreen portal (requires a RecruitScreen login). Videos themselves never leave RecruitScreen.
Delivery & retries
- Respond with any 2xx within 10 seconds. Anything else is retried: 5s → 5m → 30m → 2h → 6h → 24h (7 attempts over ~32 hours).
webhook-idis identical across retries — dedupe on it if your handler isn't idempotent.- Every attempt is visible in Settings → API → Recent deliveries.
- An endpoint failing continuously for 3 days is disabled automatically (re-enable it in the portal).
- Endpoints must be HTTPS. Redirects are not followed.
Verifying signatures
Webhooks are signed per the Standard Webhooks specification. Each request carries:
webhook-id: evt_9f2c1d8a44b0e6a2c3d1
webhook-timestamp: 1783058400
webhook-signature: v1,K5oZfzN95Z9UVu1EsfQmfVNQhnkZ2pCPFm3sTBP8Rb0=
Recompute HMAC-SHA256 over {id}.{timestamp}.{raw body} using the base64-decoded part of your whsec_ secret, and compare. Reject timestamps older than 5 minutes.
Node.js
import { createHmac, timingSafeEqual } from "node:crypto";
function verify(secret, headers, rawBody) {
const id = headers["webhook-id"], ts = headers["webhook-timestamp"];
if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false; // stale
const key = Buffer.from(secret.replace("whsec_", ""), "base64");
const expected = "v1," + createHmac("sha256", key)
.update(`${id}.${ts}.${rawBody}`).digest("base64");
return headers["webhook-signature"].split(" ").some((sig) =>
sig.length === expected.length &&
timingSafeEqual(Buffer.from(sig), Buffer.from(expected)));
}
Python
import base64, hashlib, hmac, time
def verify(secret: str, headers: dict, raw_body: bytes) -> bool:
msg_id, ts = headers["webhook-id"], headers["webhook-timestamp"]
if abs(time.time() - int(ts)) > 300:
return False
key = base64.b64decode(secret.removeprefix("whsec_"))
digest = hmac.new(key, f"{msg_id}.{ts}.".encode() + raw_body, hashlib.sha256).digest()
expected = "v1," + base64.b64encode(digest).decode()
return any(hmac.compare_digest(s, expected)
for s in headers["webhook-signature"].split(" "))
C# / .NET
using System.Security.Cryptography;
using System.Text;
static bool Verify(string secret, string id, string ts, string sigHeader, string rawBody)
{
if (Math.Abs(DateTimeOffset.UtcNow.ToUnixTimeSeconds() - long.Parse(ts)) > 300) return false;
var key = Convert.FromBase64String(secret.Replace("whsec_", ""));
using var h = new HMACSHA256(key);
var expected = "v1," + Convert.ToBase64String(
h.ComputeHash(Encoding.UTF8.GetBytes($"{id}.{ts}.{rawBody}")));
return sigHeader.Split(' ').Any(s =>
CryptographicOperations.FixedTimeEquals(
Encoding.UTF8.GetBytes(s), Encoding.UTF8.GetBytes(expected)));
}
Polling — the no-webhook fallback
If your system can't receive inbound HTTP, poll the same event stream on any schedule:
# First call — remember the id of the last event you process
GET /v1/events?limit=100
# Every call after — pick up exactly where you left off
GET /v1/events?limit=100&after=evt_9f2c1d8a44b0e6a2c3d1
Events are strictly ordered and immutable. Persist your cursor (the last processed event id) and you can't miss or double-process anything — even across restarts. A 5-minute schedule from Power Automate, cron, or your integration platform is typical.
Matching candidates to your records — external_ref
The one thing your side controls: append ?ref= with your identifier when you share a RecruitScreen link with a candidate.
https://recruitscreen.indxtree.com/c/night-shift-supervisor?ref=DAYFORCE-12345
Whatever you put in ref (up to 200 characters — a candidate ID, requisition + candidate composite, GUID, anything) comes back as external_ref in every webhook payload and API response for that candidate. Your receiver matches on it and updates the right record — no lookup tables, no name/email fuzzy matching.
Candidates who applied without a ref have external_ref: null; fall back to matching on email.
Recipes
Dayforce
Dayforce Recruiting supports third-party screening results via its Open API pathway and Integration Studio. The typical shape: a small middleware receiver (or Integration Studio flow) accepts our candidate.decision_made webhook, matches on external_ref (set it to the Dayforce candidate ID when sending the RecruitScreen link), and PATCHes the screening status onto the Dayforce candidate record. Your Dayforce administrator or implementation partner configures the Dayforce side; everything they need from us is on this page.
Power Automate (no code)
- Create a flow with the “When an HTTP request is received” trigger; copy its URL into Settings → API → Webhook endpoints.
- Add a Parse JSON step (paste the example payload above as the sample).
- Act on it: update a Dataverse/SharePoint/Excel record, post to Teams, or call your HR system's connector. Filter on
typeto handle completions and decisions differently.
Prefer pull? Use a Recurrence trigger + HTTP GET to /v1/events with the cursor stored in Dataverse.
Anything else
If your ATS/HRIS has an inbound API (Workday, SuccessFactors, Employment Hero, JobAdder, Bullhorn, PageUp and most others do), any middleware that can receive a webhook or poll an endpoint can bridge us into it. Send this page to your integration partner — the receiver is typically under an hour of work.
Errors, pagination & limits
- Errors are RFC 9457
application/problem+jsonwith accurate HTTP status codes. - List endpoints use cursor pagination: pass
?after=<next_cursor>from the previous response;has_moretells you when to stop. Maxlimitis 100. - Rate limit: 120 requests/minute per key — you'll get
429withRetry-After. - Timestamps are RFC 3339 UTC. IDs are UUIDs (events:
evt_strings). - Within v1, changes are additive only — new fields and event types may appear; nothing is removed or renamed.
Changelog
| Date | Change |
|---|---|
| 3 Jul 2026 | v1 launched: candidate.screen_completed and candidate.decision_made webhooks, read API, events polling, external_ref passthrough. |