Skip to Content
ReferenceOutbound Webhooks

Outbound Webhooks

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

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 are managed from the dashboard, or by API-key-authenticated automation apps such as Zapier and Make.

Dashboard management endpoints are authenticated by a logged-in organization user (a session bearer token), not by an API key:

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 can manage the same lifecycle with a scoped API key:

POST /v1/public-api/webhook-subscriptions webhooks_write GET /v1/public-api/webhook-subscriptions webhooks_read GET /v1/public-api/webhook-subscriptions/:id webhooks_read PATCH /v1/public-api/webhook-subscriptions/:id webhooks_write DELETE /v1/public-api/webhook-subscriptions/:id webhooks_write GET /v1/public-api/webhook-subscriptions/:id/deliveries webhooks_read

Event types

Subscribe to any subset of:

Event typeFires when
candidate_createdA candidate is created
candidate_updatedA candidate is updated
job_createdA job is created
job_updatedA job is updated
application_createdAn application is created
application_updatedAn application’s stage or notes change
placement_createdA placement is created
placement_updatedA placement is updated

A subscription with no eventTypes receives nothing.

Payload

Each delivery is a POST with a JSON envelope. The data field is the same curated public projection returned by the REST API — never raw database rows — so no internal scoring or un-curated PII is leaked.

{ "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.
Last updated on