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 } }. Uselimit(1–100, default 50) andoffset. - 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-Afterseconds.
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.created | New application |
| application.stage_changed | Candidate moved to another stage |
| candidate.hired | Candidate hired |
| offer.sent | Offer sent |
| offer.accepted | Offer accepted and signed |
| offer.declined | Offer declined |
| offer.withdrawn | Offer withdrawn |
| job.published | Job published |
| job.closed | Job 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);
}