API & Webhooks

Connect Lyvable Talent to your HRIS, payroll, BI or automation tools (Zapier, Make). The REST API reads jobs and candidates, adds applicants and moves them through your pipeline; webhooks push events to you the moment they happen. Available on plans with the ATS.

Authentication

The account owner or an admin creates keys in Settings → API. A key is shown once — we store only a fingerprint. Send it on every request:

curl https://www.lyvabletalent.com/api/v1/jobs \
  -H "Authorization: Bearer lyv_live_…"

Keys belong to one company and see only its data. Keep them on your server — never in a browser or mobile app. Revoking a key takes effect immediately.

Conventions

  • JSON in and out. Times are ISO 8601 in UTC.
  • Lists return { data: [...], pagination: { limit, offset, has_more } }. Use limit (1–100, default 50) and offset.
  • Errors return { error: { code, message } } with 400, 401, 403 (plan), 404, 409 (duplicate), 429 or 500.
  • Rate limit: 120 requests per minute per key. On 429, wait for Retry-After seconds.

Jobs

GET/api/v1/jobs

Your jobs, newest first. Filter with status=open|draft|closed.

GET/api/v1/jobs/{id}

One job, including description, requirements and benefits (HTML).

Applications

GET/api/v1/applications

Filters: job_id, stage, updated_since (ISO date — ideal for incremental syncs).

GET/api/v1/applications/{id}

Full record: candidate contact (email, phone, LinkedIn), cover letter, screening answers and a resume download link valid for one hour.

POST/api/v1/applications

Add a candidate from another source (career fair, referral tool, other ATS).

curl -X POST https://www.lyvabletalent.com/api/v1/applications \
  -H "Authorization: Bearer lyv_live_…" \
  -H "Content-Type: application/json" \
  -d '{
    "job_id": "…",
    "name": "Jordan Smith",
    "email": "jordan@example.com",
    "phone": "(555) 555-0100",
    "source": "Career fair",
    "notes": "Met at the NYC ABA expo",
    "stage": "applied"
  }'

Returns 201. Returns 409 if that email already applied to the job.

PATCH/api/v1/applications/{id}

Move a candidate or update rating/tags. Send stage (one of the six base steps) or stage_id (one of your custom stages), plus optional rating (1–5) and tags. Stage automations and emails run just like a move in the pipeline.

{ "stage": "interview", "tags": ["bilingual"] }

Stages

GET/api/v1/stages

The six base steps (applied, in_review, interview, offer, hired, rejected) and your custom stages with their ids.

Webhooks

Add an HTTPS endpoint in Settings → API and pick events. We send a POST with a JSON body; answer with any 2xx within 10 seconds. Failed deliveries are retried after 1 min, 5 min, 30 min, 2 h and 6 h. An endpoint that fails 20 deliveries in a row is turned off (turn it back on in Settings).

application.createdNew application
application.stage_changedCandidate moved to another stage
candidate.hiredCandidate hired
offer.sentOffer sent
offer.acceptedOffer accepted and signed
offer.declinedOffer declined
offer.withdrawnOffer withdrawn
job.publishedJob published
job.closedJob closed or unpublished
POST /your/endpoint
X-Lyvable-Event: application.stage_changed
X-Lyvable-Delivery: 5f0c…
X-Lyvable-Timestamp: 1790409889
X-Lyvable-Signature: t=1790409889,v1=fd90…

{
  "id": "5f0c…",
  "event": "application.stage_changed",
  "created_at": "2026-09-27T14:03:11Z",
  "data": {
    "application_id": "…", "job_id": "…", "job_title": "RBT",
    "candidate_id": "…", "candidate_name": "Jordan Smith",
    "stage": "interview", "stage_name": "Phone screen",
    "previous_stage": "in_review", "source": "Indeed",
    "applied_at": "…"
  }
}

Use X-Lyvable-Delivery to ignore repeats — a retry carries the same id. Webhook bodies don't include contact details; fetch them with GET /api/v1/applications/{id}.

Verifying signatures

Compute HMAC-SHA256 of <timestamp>.<raw body> with your endpoint's signing secret and compare it to v1. Reject messages older than 5 minutes.

// Node.js
import crypto from "node:crypto";

function isFromLyvable(rawBody, header, secret) {
  const { t, v1 } = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
  const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
  const a = Buffer.from(expected);
  const b = Buffer.from(v1 || "");
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}