Alfa API
Webhooks
Alfa sends an event to your HTTPS endpoint whenever a candidate on a linked job gets a status, and whenever that status changes. You do not need to poll.
Events
- candidate.screened: The first time a candidate on a linked job has a status: an applicant once screening finishes, or a sourced candidate who fits the job.
- candidate.status_changed: Every later change to the candidate's status, for example when a recruiter shortlists them.
- webhook.test: When you ask for a test delivery. It carries a sample candidate.
Only Alfa jobs linked to an ATS job produce events. New event types may be added; ignore types you do not handle, and acknowledge them with a 2xx status.
Setting up an endpoint
Set the URL with PUT /v1/integration/webhook-endpoint and a body of { "url": "https://…" }, using a key with the webhooks:manage scope. A company key has one endpoint. A platform key has one for each connected company: send that company's Alfa-Company-Id.
# Platform keys only: add -H "Alfa-Company-Id: org_2abcDEF123"
curl -X PUT "https://api.welovealfa.com/v1/integration/webhook-endpoint" \
-H "Authorization: Bearer $ALFA_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url":"https://ats.example.com/alfa/webhooks"}'- The URL must be HTTPS. A URL that resolves to a private, loopback or link-local address is refused with
422invalid_webhook_url. - The response includes the signing secret,
whsec_…, only when the endpoint is first created. Store it as a secret. POST /v1/integration/webhook-endpoint/rotate-secretissues a new secret, returned once. Events are signed with the new secret from then on.POST /v1/integration/webhook-endpoint/testsends a signedwebhook.testevent at once and answers witheventId,delivered,statusCode,durationMsanderror.
Payload
Each event is a JSON POST. candidate has the same fields as a candidate from the API, without jobId and externalJobId, which are on the event itself. For applicants sent through applicant intake, it carries your externalCandidateId and externalApplicationId.
{
"id": "0b7c3c58-3c9a-4a8e-9a2e-6f1f8a0c9d11",
"type": "candidate.screened",
"apiVersion": "v1",
"occurredAt": "2026-10-07T09:30:00.000Z",
"organizationId": "org_2abcDEF123",
"jobId": 9301,
"externalJobId": "job_8841",
"candidate": {
"candidateId": "app_254928",
"externalCandidateId": "cand_123",
"externalApplicationId": "appl_456",
"firstName": "Ada",
"lastName": "Lovelace",
"email": "ada@example.com",
"phone": "+44 20 7946 0000",
"linkedinUrl": "https://www.linkedin.com/in/example",
"source": "application",
"status": "suitable",
"statusUpdatedAt": "2026-10-07T09:30:00.000Z",
"score": 82,
"summary": "Eight years of backend engineering, most recently leading a payments team.",
"resumeUrl": "https://files.welovealfa.com/resume.pdf?Signature=…",
"profileUrl": "https://welovealfa.com/jobs/9301/254928"
}
}Verifying signatures
Every delivery carries an Alfa-Signature header:
Alfa-Signature: t=1791365400,v1=5f2b9c…
t is the time Alfa signed the event, in Unix seconds. v1 is the hex HMAC-SHA256 of <t>.<raw body>, keyed with your signing secret. Before you trust an event:
- Use the raw request body, exactly as received. Parsing and re-serializing the JSON changes the bytes and breaks the signature.
- Compute the HMAC and compare it with
v1in constant time. - Reject the event if
tis more than 5 minutes from your clock.
const crypto = require('node:crypto');
function verifyAlfaSignature(secret, rawBody, signatureHeader, toleranceSeconds = 300) {
if (typeof signatureHeader !== 'string') {
return false;
}
const parts = {};
for (const part of signatureHeader.split(',')) {
const separator = part.indexOf('=');
if (separator > 0) {
parts[part.slice(0, separator).trim()] = part.slice(separator + 1).trim();
}
}
if (!/^\d+$/.test(parts.t || '') || !parts.v1) {
return false;
}
const nowSeconds = Math.floor(Date.now() / 1000);
if (Math.abs(nowSeconds - Number(parts.t)) > toleranceSeconds) {
return false;
}
const expected = crypto
.createHmac('sha256', secret)
.update(`${parts.t}.${rawBody}`)
.digest('hex');
const expectedBytes = Buffer.from(expected, 'utf8');
const receivedBytes = Buffer.from(parts.v1, 'utf8');
if (expectedBytes.length !== receivedBytes.length) {
return false;
}
return crypto.timingSafeEqual(expectedBytes, receivedBytes);
}
Both functions return true only for a valid, recent signature. Answer a request that fails with a 4xx status and ignore it.
Responding and retries
- Answer with any 2xx status within 10 seconds. Do slow work after you answer, for example from a queue.
- Redirects are not followed. Any other status, a redirect or a timeout counts as a failure.
- A failed delivery is retried with exponential backoff, starting at 1 minute and doubling up to 4 hours between attempts, for 24 hours.
Ordering and duplicates
Delivery is at least once, and events can arrive out of order. De-duplicate by the event id, and when you store a candidate's status, keep the one with the newest statusUpdatedAt. An older event arriving late must not overwrite a newer status.
Delivery log and redelivery
GET /v1/integration/webhook-deliveries/page lists recent delivery attempts with their status code, duration and error. It does not store payloads. To send an event again, for example after fixing your endpoint, call POST /v1/integration/webhook-deliveries/{deliveryId}/redeliver. It answers 202 and delivers the event, with the same event id, within a minute.
# Platform keys only: add -H "Alfa-Company-Id: org_2abcDEF123"
curl -X POST "https://api.welovealfa.com/v1/integration/webhook-deliveries/7715/redeliver" \
-H "Authorization: Bearer $ALFA_API_KEY"When deliveries stop
- After 3 days of failed deliveries, Alfa turns the endpoint off and emails the company's admin. Set the URL again to turn it back on.
- Revoking a key, or a platform's authorization, stops its deliveries.
- Rotating a key moves its webhook endpoints to the new key, so deliveries continue.
Reference
See the webhook operations and event payloads in the API reference, and the changelog.
