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.

Set the webhook endpoint
# 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 422 invalid_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-secret issues a new secret, returned once. Events are signed with the new secret from then on.
  • POST /v1/integration/webhook-endpoint/test sends a signed webhook.test event at once and answers with eventId, delivered, statusCode, durationMs and error.

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.

candidate.screened
{
  "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:

  1. Use the raw request body, exactly as received. Parsing and re-serializing the JSON changes the bytes and breaks the signature.
  2. Compute the HMAC and compare it with v1 in constant time.
  3. Reject the event if t is more than 5 minutes from your clock.
Verify a signature
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.

Redeliver an event
# 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.