API reference · v1

TalentOS API

69 HTTP endpoints across 14 domains. Cookie sessions or Bearer-token auth. JSON request/response. Tenant-scoped on every read + write. Press ⌘F to search.

# Cookie session — browser apps
curl https://talentosglobal.co/api/vacancies \
  -H 'Cookie: talentos_sid=…'

# Bearer token — server-to-server (mint at /account)
curl https://talentosglobal.co/api/vacancies \
  -H 'Authorization: Bearer tk_…'

# Webhook payload arriving at YOUR server
POST /your-talentos-hook
X-TalentOS-Signature: t=1758000000,v1=<hmac-sha256-hex>
Content-Type: application/json

{"id":"evt_…","type":"candidate.applied","ts":"…","tenantId":"…","data":{}}
publicsessionsession + role

Authentication

Two methods. Browser apps use cookie sessions (login sets a 14-day HttpOnly cookie). Programmatic clients use Bearer tokens — generate a key under /account → API keys, then send `Authorization: Bearer tk_…` with every request. Both methods resolve to the same tenant scoping + RBAC; any endpoint marked `session` or `session + role` also accepts a Bearer token of the appropriate role.

POST/api/auth/signuppublic
Create a tenant + admin user, sign in. Also fires a verification email — the new admin lands signed-in but `emailVerified: false` until they click the link.
// Request body
{ "companyName": "Acme Bank", "adminName": "Jane Doe", "adminEmail": "jane@acmebank.com", "password": "••••••••" }
// Response
{ "ok": true, "tenant": { "id": "acme-bank", "name": "Acme Bank" }, "user": { "email": "…", "emailVerified": false, … } }
POST/api/auth/verify-emailpublic
Consume a verification token. Idempotent: a second click returns `alreadyVerified: true` rather than 404. POST (not GET) so mail-client prefetchers can't accidentally consume the token.
// Request body
{ "token": "<64-hex>" }
// Response
{ "ok": true, "user": { "emailVerified": true, … } }
POST/api/auth/resend-verificationsession
Mint a fresh verification token + email it. Rate-limited to 3/min per email. Already-verified users get `alreadyVerified: true` instead of a new mail.
// Response
{ "ok": true, "sent": true }
POST/api/auth/loginpublic
Sign in. Sets session cookie.
// Request body
{ "email": "jane@acmebank.com", "password": "••••••••" }
// Response
{ "ok": true, "user": { ... } }
POST/api/auth/logoutsession
Destroy current session.
// Response
{ "ok": true }
GET/api/auth/mesession
Resolve the current session to a user.
// Response
{ "ok": true, "user": { "email": "...", "name": "...", "role": "...", "tenantId": "..." } }
POST/api/auth/forgot-passwordpublic
Request a password-reset email.
// Request body
{ "email": "jane@acmebank.com" }
// Response
{ "ok": true, "dryRun": false }
POST/api/auth/reset-passwordpublic
Complete password reset using a one-time token.
// Request body
{ "token": "<32-byte-hex>", "password": "••••••••" }
// Response
{ "ok": true, "user": { ... } }
POST/api/auth/invitesession + role
Invite a teammate (admin/CHRO only).
Roles:adminchro
// Request body
{ "email": "amara@acmebank.com", "name": "Amara D.", "role": "recruiter" }
// Response
{ "ok": true, "invite": { "inviteUrl": "..." } }
POST/api/auth/accept-invitepublic
Set password on an invited account; signs the user in.
// Request body
{ "token": "<invite-token>", "password": "••••••••" }
// Response
{ "ok": true, "user": { ... } }

Tenants + Team

Self-tenant operations + team management. Every tenant is isolated; the API never returns cross-tenant data.

GET/api/tenants/mesession + role
Get the caller's tenant record.
Roles:adminchrorecruiterhiring_manager
// Response
{ "ok": true, "tenant": { "id": "...", "name": "...", "plan": "pilot", "logoUrl": "...", "brandColor": "#7c3aed", "tagline": "..." } }
PATCH/api/tenants/mesession + role
Update branding (name, tagline, brandColor, logoUploadId, logoUrl) and monthly AI budget.
Roles:adminchro
// Request body
{ "name": "Acme Bank", "tagline": "...", "brandColor": "#7c3aed", "monthlyBudgetUsd": 50 }
GET/api/teamsession + role
List users in the caller's tenant (hashed passwords scrubbed).
Roles:adminchro
GET/api/tenants/me/api-keyssession + role
List Bearer tokens issued for this tenant. Returns sanitised projections (prefix only — full token is only visible once at create time).
Roles:adminchro
// Response
{ "ok": true, "keys": [ { "id": "key_…", "name": "Production sync", "tokenPrefix": "tk_a1b2c3d4", "role": "admin", "createdAt": "…", "createdBy": "jane@acme.com", "lastUsedAt": "…", "revoked": false } ] }
POST/api/tenants/me/api-keyssession + role
Mint a new Bearer token. The raw token is returned ONCE — store it immediately; we keep only a scrypt hash. Role defaults to admin.
Roles:adminchro
// Request body
{ "name": "Workday integration", "role": "recruiter" }
// Response
{ "ok": true, "key": { "id": "key_…", "tokenPrefix": "tk_a1b2c3d4", … }, "rawToken": "tk_<64-hex>" }
DELETE/api/tenants/me/api-keys/[id]session + role
Revoke a key. Soft-deletes (sets revoked=true) — the row is retained for the audit trail. Subsequent requests using that token return 401.
Roles:adminchro
// Response
{ "ok": true }

Vacancies

Job postings. Public read for open status only — used by the careers portal. Admin/recruiter writes only.

GET/api/vacanciessession
List the caller's tenant vacancies (filters: q, status, subsidiaryId).
POST/api/vacanciessession + role
Create a new vacancy.
Roles:adminchrorecruiterhiring_manager
// Request body
{ "title": "Senior Backend Engineer", "description": "...", "location": "Lagos", "level": "senior", "employmentType": "full_time", "skills": ["Go", "Postgres"] }
GET/api/vacancies/[id]session
Get a vacancy (cross-tenant id returns 404).
PATCH/api/vacancies/[id]session + role
Update mutable fields.
Roles:adminchrorecruiterhiring_manager
DELETE/api/vacancies/[id]session + role
Delete a vacancy.
Roles:adminchrorecruiterhiring_manager
GET/api/careers/vacanciespublic
PUBLIC list of all open vacancies across all tenants. Sanitized projection. Pagination via limit + offset.
GET/api/careers/vacancies/[id]public
PUBLIC vacancy read. Open-status only. Stripped of internal fields.

Candidates

Applicants linked to vacancies. Public apply endpoint for the careers portal; tenant-scoped CRUD for recruiters.

POST/api/careers/applypublic
PUBLIC application submit. Dedupes by (email, vacancyId). Increments vacancy.applicants.
// Request body
{ "vacancyId": "vac_...", "name": "Kwame M.", "email": "kwame@...", "experience": 7, "skills": ["Go"] }
// Response
{ "ok": true, "candidate": { "id": "cand_...", "dedup": false } }
GET/api/candidatessession + role
List tenant candidates (filters: jobId, stage, q).
Roles:adminchrorecruiterhiring_managerinterviewer
GET/api/candidates/[id]session + role
Get a candidate.
Roles:adminchrorecruiterhiring_managerinterviewer
PATCH/api/candidates/[id]session + role
Update candidate; common use is stage transitions. Hiring transitions auto-bump vacancy.hired.
Roles:adminchrorecruiterhiring_managerinterviewer
// Request body
{ "stage": "interview" }
DELETE/api/candidates/[id]session + role
Delete a candidate.
Roles:adminchrorecruiter
POST/api/candidates/importsession + role
Bulk import up to 500 rows. Per-row error reporting. Dedup by (email, vacancyId).
Roles:adminchrorecruiterhiring_manager
// Request body
{ "rows": [ { "name": "...", "email": "...", "vacancyId": "..." } ] }
// Response
{ "ok": true, "total": 100, "created": 87, "updated": 8, "skipped": 5, "errors": [ ... ] }

Offers

Formal offer letters extended to candidates at the offer pipeline stage. Accepting an offer auto-bumps the candidate to `hired` and increments the vacancy's hired counter. Status transitions fire `offer.sent`, `offer.accepted`, `offer.declined` webhooks.

GET/api/offerssession + role
List offers in your tenant. Filters: candidateId, vacancyId, status.
Roles:adminchrorecruiterhiring_managerinterviewer
POST/api/offerssession + role
Create an offer. Candidate + vacancy must belong to your tenant. Setting status="sent" stamps `sentAt` and fires `offer.sent`.
Roles:adminchrorecruiterhiring_manager
// Request body
{ "candidateId": "cand_...", "vacancyId": "vac_...", "baseSalary": 80000, "currency": "USD", "bonus": 10000, "signOnBonus": 5000, "startDate": "2026-06-01", "status": "draft" }
// Response
{ "ok": true, "offer": { "id": "ofr_...", "status": "draft", ... } }
GET/api/offers/[id]session + role
Single-offer detail. Cross-tenant id → 404.
Roles:adminchrorecruiterhiring_managerinterviewer
PATCH/api/offers/[id]session + role
Update mutable fields + transition status. `accepted` triggers candidate stage = hired + vacancy.hired++.
Roles:adminchrorecruiterhiring_manager
// Request body
{ "status": "accepted", "decision": "Candidate accepted via Slack" }
DELETE/api/offers/[id]session + role
Delete an offer.
Roles:adminchrorecruiter

Interviews

Scheduled interviews tied to a candidate + vacancy + panel. Scorecards are recorded inline on the interview record. `interview.scheduled` and `interview.completed` webhooks fire on the corresponding transitions.

GET/api/interviewssession + role
List interviews. Filters: candidateId, vacancyId, status, from, to (ISO datetimes).
Roles:adminchrorecruiterhiring_managerinterviewer
POST/api/interviewssession + role
Schedule an interview. Kind: phone | video | onsite | technical | panel. Fires `interview.scheduled`.
Roles:adminchrorecruiterhiring_manager
// Request body
{ "candidateId": "cand_...", "vacancyId": "vac_...", "scheduledAt": "2026-06-01T14:00:00Z", "durationMin": 45, "kind": "video", "locationOrUrl": "https://meet.example.com/x", "interviewers": ["Jane Doe", "John Smith"] }
// Response
{ "ok": true, "interview": { "id": "iv_...", "status": "scheduled", ... } }
PATCH/api/interviews/[id]session + role
Update fields, transition status, or upsert a scorecard. Pass `addScorecard: { interviewer, score, recommendation, notes? }` to record post-interview feedback. `status=completed` fires `interview.completed` webhook.
Roles:adminchrorecruiterhiring_managerinterviewer
// Request body
{ "status": "completed", "addScorecard": { "interviewer": "Jane Doe", "score": 87, "recommendation": "strong_hire", "notes": "Excellent system design." } }
DELETE/api/interviews/[id]session + role
Delete an interview.
Roles:adminchrorecruiter

Subsidiaries / Agencies / Integrations

Organisational structure + external partners + connected systems. All three share the same shape — tenant-scoped list/create + per-id get/patch/delete.

GET/api/subsidiariessession + role
List + POST create. Detail at /api/subsidiaries/[id] (GET/PATCH/DELETE).
Roles:adminchro
GET/api/agenciessession + role
List + POST create. Detail at /api/agencies/[id] (GET/PATCH/DELETE).
Roles:adminchrorecruiter
GET/api/integrationssession + role
List + POST create. Detail at /api/integrations/[id] (GET/PATCH/DELETE). Provider slug must be lowercase alphanumeric + hyphens.
Roles:adminchro

AI tools

Claude-powered features. Each call tracks tokens + cost per tenant and shows up on /spend. Pass entity ids OR inline payloads — server fetches authoritative data when ids are supplied.

POST/api/ai/chatsession
Conversational AI Recruiter (streaming).
// Request body
{ "messages": [...], "stream": true, "language": "en" }
POST/api/ai/screensession
Score a candidate against a job.
// Request body
{ "candidateId": "cand_...", "vacancyId": "vac_..." }   // OR inline { "candidate": {...}, "job": {...} }
// Response
{ "ok": true, "result": { "matchScore": 87, "verdict": "strong_match", "strengths": [...], "concerns": [...], "recommendation": "..." } }
POST/api/ai/outreachsession
Draft personalised candidate outreach.
// Request body
{ "candidateId": "cand_...", "vacancyId": "vac_...", "tone": "warm" }
// Response
{ "ok": true, "result": { "subject": "...", "body": "...", "follow_up_body": "..." } }
POST/api/ai/draft-offersession
Draft an offer letter.
// Request body
{ "candidateId": "cand_...", "vacancyId": "vac_...", "baseSalary": 12000, "currency": "USD" }
POST/api/ai/interview-questionssession
Generate technical / behavioral / scenario interview questions for a role.
// Request body
{ "vacancyId": "vac_..." }   // OR { "role": "Backend Engineer", "level": "senior", "skills": [...] }
// Response
{ "ok": true, "result": { "technical": [...], "behavioral": [...], "scenario": [...], "red_flag_signals": [...] } }
POST/api/ai/source-candidatessession
Find best matches from the tenant talent pool given a brief.
// Request body
{ "brief": "Looking for a senior fintech eng with...", "count": 5 }
POST/api/ai/generate-jdsession
Generate a job description from a brief.
POST/api/ai/salary-benchmarksession
Benchmark compensation for a role + location.
POST/api/ai/transcript-summarysession
Analyse an interview transcript.
POST/api/ai/attrition-risksession
Score attrition risk for an employee.

Library

Saved artifacts from AI calls (JDs, screens, offers, etc.). Append-only with tenant scoping.

GET/api/librarysession
List items (filters: kind, q).
POST/api/librarysession
Save an item.
// Request body
{ "kind": "jd", "title": "Senior Backend JD", "payload": {...}, "source": "claude" }
GET/api/library/[id]session
Get an item.
DELETE/api/library/[id]session + role
Delete an item.
Roles:adminchrohiring_managerrecruiteragency_admin

Uploads

File storage. Bytes go to Vercel Blob in production (durable, CDN-fronted) or local FS in dev. Metadata in KV.

POST/api/uploadsession
Multipart upload. Accepts PDF/DOC/DOCX/TXT/RTF/ODT/JPG/PNG/WebP. 8 MB max.
GET/api/upload/[id]session
Read bytes. Redirects to Blob CDN URL when active.
DELETE/api/upload/[id]session
Delete bytes + metadata. Uploader or admin only.

Audit + Analytics

Tamper-evident audit trail + executive analytics. Append-only LIST (LPUSH on KV). Tenant-scoped.

GET/api/auditsession
List audit events (filters: type, since, q, limit).
POST/api/auditsession
Record an arbitrary audit event from the client.
GET/api/audit/exportsession + role
Download tenant audit trail as CSV (17 columns).
Roles:adminchro
GET/api/audit/spendsession + role
AI spend aggregation (window: 24h, 7d, 30d, all).
Roles:adminchrorecruiterhiring_manager
GET/api/insightssession + role
Executive dashboard data. Vacancies + candidates + audit + AI cost aggregated for /reports.
Roles:adminchrorecruiterhiring_managerinterviewer

Account / NDPR + GDPR

Data-subject rights endpoints. Admin self-serves export + tenant delete; no support ticket required.

GET/api/account/exportsession + role
Download tenant-scoped JSON: users + library + uploads + vacancies + candidates + audit. Hashed passwords + tokens scrubbed.
Roles:adminchro
POST/api/account/deletesession + role
Cascade-delete tenant + all owned data. Requires typed confirmation matching tenant id. Clears session cookie on success.
Roles:admin
// Request body
{ "confirm": "<tenant-id>" }

Webhooks

Outbound HTTP push. Subscribe an HTTPS endpoint to one or more events; we POST a signed JSON envelope when they happen. Pairs with API keys (pull) to give you a complete integration surface. Every delivery includes an `X-TalentOS-Signature: t=<unix>,v1=<hex>` header where `hex = HMAC-SHA256(t + "." + raw_body, secret)`. Verify the signature on every request — and reject any payload more than 5 minutes old to defeat replay. Available events: `candidate.applied`, `candidate.stage_changed`, `vacancy.published`, `offer.sent`, `offer.accepted`, `offer.declined`, `interview.scheduled`, `interview.completed`. Delivery is durable: failed deliveries enter a retry queue with exponential backoff (30s → 2m → 10m → 1h → 6h) and the subscription auto-disables after 20 consecutive failures.

GET/api/tenants/me/webhookssession + role
List webhook subscriptions for this tenant. Secrets are NOT returned — they're shown ONCE at create time.
Roles:adminchro
// Response
{ "ok": true, "webhooks": [ { "id": "wh_…", "url": "https://…", "events": ["candidate.applied"], "enabled": true, "createdAt": "…", "lastDeliveryAt": "…", "lastStatus": "200" } ], "availableEvents": ["candidate.applied", "candidate.stage_changed", "vacancy.published"] }
POST/api/tenants/me/webhookssession + role
Register a new subscription. URL must be HTTPS. Response returns the full record INCLUDING `secret` — store it on your end; we don't show it again.
Roles:adminchro
// Request body
{ "url": "https://your-system.example.com/talentos-hook", "events": ["candidate.applied", "vacancy.published"] }
// Response
{ "ok": true, "webhook": { "id": "wh_…", "url": "…", "events": [...], "secret": "whsec_<64-hex>", "enabled": true, … } }
DELETE/api/tenants/me/webhooks/[id]session + role
Delete a subscription. Hard-delete — secret is unrecoverable, so retention has no value.
Roles:adminchro
// Response
{ "ok": true }

Health + Status

Operational endpoints. Public; safe to expose to monitors + uptime tools.

GET/api/healthpublic
Platform health. Storage backend + probe, AI live state, integration posture (Blob, Resend, Sentry), audit stats. Polled by /status page.