Webhooks
Get a message when something happens to a candidate.
LessRounds can send a message to a web address of yours when a candidate finishes, is scored, has a final result or changes status. Here’s how to set it up and what each message contains.
Updated October 3, 2026
A webhook is a web address that receives a message from LessRounds when something happens to a candidate: they finish the interview, the AI scores it, their result is final, or their status changes. Tools like Make and n8n give you such an address, and so can your own server.
Add a webhook
- In LessRounds, go to Settings → API & webhooks and click Add webhook. Only admins can.
- Paste the address. It must start with
https://. - Choose which events to send, and for which interviews: all of them (including ones you create later), or only some.
- Click Send a sample, choose an interview, and send. The sample is a made-up candidate with that interview’s real questions, marked
"test": true, so your tool can learn every field.
A company can have up to 10 webhooks. Each one has a delivery log showing the last 30 days of messages and what your address answered.
The four events
| Event | In LessRounds | When it’s sent |
|---|---|---|
| candidate.submitted | Candidate finished | Sent once per attempt, when the candidate has finished every step. |
| candidate.scored | AI score ready | Sent once per attempt, when the AI score is saved (never before candidate.submitted). Not sent when the interview couldn’t be scored. |
| candidate.ready | Result is final | Sent once per attempt, when the result is final: complete (score and recording), incomplete (result.incomplete_reason says why) or not_applicable (no AI step). Nothing more arrives for this attempt except status changes. |
| candidate.status_changed | Status changed | Sent on every status change after submission, by the AI or your team. change says from what, to what, and who made it. |
Which should I choose? For one message per candidate with everything in it, choose only Result is final (candidate.ready). Add Status changed if you also want your team’s decisions, like shortlisting or rejecting.
Nothing is sent before a candidate finishes, and nothing is sent for previews your team takes.
What a message looks like
Each message is a POST request with a JSON body. Here’s a status change, with the candidate shortened:
{
"id": "Hk3d9xQ2mZpL0aB7cD1eF",
"type": "candidate.status_changed",
"created_at": "2026-10-03T10:41:00Z",
"api_version": "v1",
"test": false,
"change": {
"from": "passed",
"to": "shortlisted",
"by": { "type": "team", "name": "Priya Nair" }
},
"candidate": {
"id": "V1StGXR8_Z5jdHi6Bmy2k",
"attempt": 1,
"name": "Riya Sharma",
"email": "riya@example.com",
"interview": { "id": "k9TzQ1bL7xR3mN5pW8vY2", "title": "Customer Support Associate" },
"status": "shortlisted",
"status_label": "Shortlisted",
"result": { "state": "complete", "score": 82, "recommendation": "recommended" },
"answers": {
"q-x7k2m9a1b3": { "question": "What is your notice period?", "answer": "30 days" }
},
"dashboard_url": "https://lessrounds.ai/dash/pipeline/V1StGXR8_Z5jdHi6Bmy2k"
}
}
candidate is the same object the API returns. Every field is described in the API reference. The transcript isn’t included. Get it from the API when you need it.
Headers
| Header | What it is |
|---|---|
| LessRounds-Event-Id | The event id (the same as the body’s id). Stays the same on every retry: use it to ignore a message you already handled. |
| LessRounds-Event-Type | The event type, the same as the body’s type. |
| LessRounds-Delivery-Attempt | 1 for the first try, 2 for the first retry, and so on (at most 9). |
| LessRounds-Signature | t=<unix seconds>,v1=<hex HMAC-SHA256 of "<t>.<raw body>" with your webhook’s signing secret>. For 24 hours after you replace the secret there are two v1 values, one per secret. Checking it is optional. |
Fields
| Field | Type | What it is |
|---|---|---|
| id | string | Stays the same if you receive the event again — use it to ignore repeats. |
| type | string | One of: candidate.submitted, candidate.scored, candidate.ready, candidate.status_changed |
| created_at | time | When it happened. |
| api_version | string | Always "v1" |
| test | boolean | true for samples sent from LessRounds; real events are false. |
| change | StatusChange or null | Only on candidate.status_changed. |
| candidate | Candidate | The candidate's latest data — not frozen at the time of the event. |
Repeats, order and the latest data
- The same message can arrive twice, for example when your address answered too slowly the first time. Its
idstays the same, so you can ignore one you’ve already handled. candidateis always the latest data, not a copy from the moment of the event. A late or repeated message never brings back older data. What changed at the time is kept inchange.- Messages about one candidate arrive in the order they happened. A message waiting to be retried holds back that candidate’s later messages to the same address. Other candidates aren’t held up.
When your address doesn’t answer
Your address must answer with any 2xx status within 10 seconds. Redirects aren’t followed and count as a failure. If it fails, we try again after 1, 5 and 30 minutes, then after 2, 6, 12, 24 and 24 hours: 9 tries over about 3 days.
- After a day of failures, your admins get an email.
- After 3 days with no success, the webhook is turned off, messages still waiting are marked failed, and your admins get a second email.
- When it works again, turn the webhook back on (it gets new events from then on) and resend what you missed from its delivery log. Or catch up with
GET /events, which keeps 30 days. - If you change the webhook’s address, its waiting messages are sent to the new address straight away.
Check that a message comes from LessRounds (optional)
Every message is signed with your webhook’s signing secret. Find it in the webhook’s menu, under Signing secret. Checking the signature is optional: the addresses Make and n8n create are long and random, so most people skip it. Your own server should check it.
The LessRounds-Signature header looks like t=1791019260,v1=5f2c…. To check it, compute an HMAC-SHA256 of the timestamp, a dot and the raw body, using the whole secret (including whsec_) as the key. Compare it with v1, and reject the message if t is more than 5 minutes away from now.
Node.js
import crypto from 'node:crypto';
// body: the raw request body as a string, before JSON.parse. header: LessRounds-Signature.
export function isFromLessRounds(body, header, secret) {
const parts = header.split(',').map((p) => p.trim().split('='));
const t = parts.find(([k]) => k === 't')?.[1];
const signatures = parts.filter(([k]) => k === 'v1').map(([, v]) => v);
if (!t || Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
const expected = Buffer.from(
crypto.createHmac('sha256', secret).update(`${t}.${body}`).digest('hex')
);
return signatures.some(
(s) => s.length === expected.length && crypto.timingSafeEqual(Buffer.from(s), expected)
);
}
Python
import hashlib, hmac, time
# body: the raw request body as bytes, before parsing. header: LessRounds-Signature.
def is_from_lessrounds(body: bytes, header: str, secret: str) -> bool:
parts = [p.strip().split("=", 1) for p in header.split(",") if "=" in p]
t = next((v for k, v in parts if k == "t"), None)
signatures = [v for k, v in parts if k == "v1"]
if t is None or abs(time.time() - int(t)) > 300:
return False
expected = hmac.new(secret.encode(), t.encode() + b"." + body, hashlib.sha256).hexdigest()
return any(hmac.compare_digest(s, expected) for s in signatures)
Use the body exactly as it arrived. Parsing and re-encoding the JSON changes it, and the check fails.
Replacing the secret: in the webhook’s menu, under Signing secret, click Replace secret. For the next 24 hours, messages carry two v1 values, one for each secret, so you can switch without missing any.
CVs, recordings and transcripts
Links in a message (documents[].url, ai_interview.recording.url, ai_interview.transcript_url) point to the API, so they need your API key. A CV or recording never sits at a public address. Calling a file or recording link with your key gives a download_url that works for 15 minutes.
To let people open a candidate from your sheet or tracking system, use dashboard_url. It opens the candidate’s page in LessRounds for anyone on your team who is signed in.
Which addresses are allowed
Addresses must use https:// and a public host name. For safety, we never send messages to private, local or internal network addresses, and we check this again each time we connect.