Open appvitae.ai

Outbound Webhooks

Webhooks push recruiting events to your endpoint as they happen, so you don’t have to poll. When a subscribed event fires, Vitae.ai POSTs a signed JSON payload to every active subscription in the organization that listens for that event type.

Events fire for every surface that performs the write: the dashboard, career page applications, bulk imports, and API calls all emit the same events. An integration no longer sees only API-originated changes.

Delivery is durable: each event is persisted before it is sent, retried with exponential backoff on failure, and moved to a dead-letter state you can inspect if it never succeeds.

Subscribing

Webhook subscriptions live at /v1/webhook-subscriptions, reachable with a signed-in session or with an API key that is unscoped or holds the webhooks_read / webhooks_write scopes. Reads are open to any org member; creating, updating, rotating, or deactivating a subscription requires an org admin (for a key, the key’s owner must be an org admin).

POST    /v1/webhook-subscriptions                 Create a subscription (secret returned once)
GET     /v1/webhook-subscriptions                 List subscriptions
GET     /v1/webhook-subscriptions/:id             Fetch one subscription
PATCH   /v1/webhook-subscriptions/:id             Update url / events / active
POST    /v1/webhook-subscriptions/:id/rotate-secret  Rotate signing secret (returned once)
GET     /v1/webhook-subscriptions/:id/deliveries  Recent delivery attempts
DELETE  /v1/webhook-subscriptions/:id             Deactivate (soft; preserves history)

A subscription has an HTTPS url, a set of eventTypes to deliver, an optional description, and an isActive flag. The destination URL is validated for SSRF safety before it is saved — it must be a public host, not a private, loopback, or link-local address.

curl -X POST https://api.vitae.ai/v1/webhook-subscriptions \
  -H "Authorization: Bearer <sessionToken>" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/webhooks/vitae",
    "eventTypes": ["candidate_created", "application_updated"]
  }'

The response includes a signing secret (whsec_...) exactly once. Store it securely: it is never retrievable again, only rotatable.

Automation platforms such as Zapier and Make manage the same lifecycle over the same endpoints, authenticated by an API key instead of a session.

Event types

Subscribe to any subset of:

Event typeFires when
candidate_createdA candidate is created
job_createdA job is created
application_updatedAn application’s pipeline stage changes
application_stage_changedAn application changes stage; includes previous/current stage and transition time
linkedin_connection_acceptedA LinkedIn connection is accepted for a connected account
message_receivedAn inbound LinkedIn, email, or messaging-channel message is persisted

A subscription with no eventTypes receives nothing.

Payload

Each delivery is a POST with a JSON envelope. The data field is a curated public projection of the affected resource, never raw database rows, so no internal scoring or un-curated PII is leaked. The envelope format is unchanged from the previous developer API.

{
  "id": "f1d2c3b4-...",
  "type": "candidate_created",
  "createdAt": "2026-06-06T12:00:00.000Z",
  "apiVersion": "v1",
  "data": {
    "id": "cand_123",
    "fullName": "Ada Lovelace",
    "email": "ada@example.com",
    "status": "active"
  }
}
FieldMeaning
idUnique id for this event occurrence — safe to dedupe on
typeThe event type (matches the subscribed values)
createdAtISO-8601 instant the event was emitted
apiVersionPayload contract version (v1); pin your parser to it
dataCurated public attributes of the affected resource

Headers

Every delivery carries:

HeaderValue
x-vitae-signatureTimestamped HMAC, e.g. t=1700000000,v1=<hex>
x-vitae-eventEvent type, e.g. candidate_created
x-vitae-event-idThe envelope id (dedupe key)
x-vitae-delivery-idId of this specific delivery attempt
content-typeapplication/json
user-agentVitae-Webhooks/1.0

Verifying signatures

The x-vitae-signature header is t=<unix-seconds>,v1=<hex>, where the hex is an HMAC-SHA256 of "<t>.<rawBody>" keyed by your signing secret — the same recipe Stripe uses. Always verify against the raw request body, before any JSON parsing or re-serialization.

import { createHmac, timingSafeEqual } from 'node:crypto';
 
function isValidVitaeSignature(rawBody: string, header: string, secret: string): boolean {
  const parts = Object.fromEntries(header.split(',').map((kv) => kv.split('=')));
  const timestamp = parts.t;
  const signature = parts.v1;
  if (!timestamp || !signature) return false;
 
  const expected = createHmac('sha256', secret)
    .update(`${timestamp}.${rawBody}`)
    .digest('hex');
 
  const a = Buffer.from(signature);
  const b = Buffer.from(expected);
  if (a.length !== b.length) return false;
  return timingSafeEqual(a, b);
}

Reject the request if the signature does not match. Because the timestamp is signed into the payload, you can also reject deliveries whose t is outside a freshness window (e.g. older than five minutes) to defend against replay.

Responding

Return any 2xx status to acknowledge receipt. Any non-2xx response, a connection error, or a timeout is treated as a failed attempt and retried.

  • Respond quickly (under ten seconds) — slow handlers are aborted and retried.
  • Acknowledge first, then process asynchronously, so a slow downstream never causes duplicate deliveries.
  • Be idempotent: dedupe on x-vitae-event-id, since at-least-once delivery means an event can arrive more than once.
  • Redirects are not followed; point the subscription at the final URL.

Retries and dead-letters

A delivery is attempted up to 5 times with exponential backoff (base 10s: ~10s, 20s, 40s, 80s). The endpoint URL is re-validated for SSRF safety on every attempt, not just at subscribe time, to defend against DNS rebinding.

If all attempts fail, the delivery is dead-lettered — it stops retrying and is recorded with its last response status, body (truncated), and error. Inspect recent attempts, including dead-letters, via:

GET /v1/webhook-subscriptions/:id/deliveries

Each delivery row exposes status (pending, delivering, succeeded, failed, dead_lettered), attempts, responseStatus, error, and timing fields, so you can diagnose a misbehaving endpoint and, once fixed, rotate the secret or re-create the subscription.

Security

  • Always verify the x-vitae-signature before trusting a payload.
  • Serve your endpoint over HTTPS on a public host.
  • Treat the signing secret like a password; rotate it if it may be exposed.
  • Enforce a timestamp freshness window to reject replays.