API reference · v1

TalentOS API

89 HTTP endpoints across 18 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. They are not equivalent beyond that: `resolveCaller()` returns a Bearer caller before the tenant IP allowlist is applied, and the MFA gate exempts API-key callers, so a key is not subject to either control. Keys also carry no expiry — any admin or CHRO can mint an admin-role key that stays valid until someone revokes it by hand.

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 per 60s window per email — the counter is an in-process Map, so on serverless the cap applies per function instance and resets on cold start; the fleet-wide ceiling is that figure multiplied by the number of live instances. 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.). Tenant-scoped on read and write. Not append-only: the DELETE below hard-deletes the row, there is no soft delete, no version history and no backup to restore from — only the audit event that the deletion happened survives.

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_managerrecruiter

Uploads

File storage. Bytes go to Vercel Blob in production or local FS in dev; metadata in KV. Only the public careers-site purposes are served from the Blob CDN — everything else streams through the authenticated route below. Nothing here is backed up: there is no snapshot, no restore path and no soft delete, so a DELETE is permanent.

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. Re-checks session, MFA, tenant and agency scope on every request, then streams the bytes: CVs, assessments, portfolios, offers and attachments are never redirected to their blob URL. Only public careers-site purposes (logo, careers-logo, careers-image, careers, jd) redirect to the Blob CDN URL. Caveat: the Blob store is provisioned public-only, so private bytes are still written with public access and `blobAccess` on the row reads `public` even for a CV — the redirect is blocked by purpose, not by the store. This route is therefore the only access control; the byte URL itself is unauthenticated, and anyone holding it can fetch the file with no session. That URL is not linked from the app or the careers site, but it is returned verbatim in the `POST /api/upload` response and in the tenant JSON export, so treat it as a secret rather than as protected storage. A private Blob store is not provisioned.
DELETE/api/upload/[id]session
Delete bytes + metadata. Uploader or admin only.

Audit + Analytics

Audit trail + executive analytics. Storage is ONE platform-wide Redis list (`talentos:audit:list`): LPUSH on write, then LTRIM to the newest 2,000 events — the cap is shared across all tenants, so another tenant's volume evicts your events and there is no fixed retention period. Rows are not hash-chained, signed or sequence-numbered, so the trail is NOT tamper-evident; GDPR erasure (`redactCandidate`) rewrites rows in place, so it is not immutable either. The list is global, not tenant-scoped — tenant scoping is applied on read: `list()`/`stats()` filter on `tenantId` and return nothing when no scope is passed (deny-by-default), and cross-tenant reads need the platform-admin role plus an explicit `allTenants=1`. For retention beyond the rolling window, export the CSV or stream events to your own SIEM (`/api/tenants/me/siem`).

GET/api/auditsession
List audit events (filters: type, since, q, limit).
POST/api/auditsession
Record an audit event from the client. Requires a session and stamps the caller's tenant, and `type` must be one of view/create/update/delete/approve/reject/export/permission_change/config_change. `actor` and `actorEmail` are taken from the body when supplied, so any authenticated user of the tenant can write a row attributed to a colleague — another reason the trail is not evidence-grade.
GET/api/audit/exportsession + role
Download tenant audit trail as CSV (21 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`, `candidate.rejected`, `candidate.merged`, `vacancy.published`, `vacancy.updated`, `vacancy.approval_requested`, `vacancy.approved`, `offer.approval_requested`, `offer.approved`, `offer.rejected`, `offer.sent`, `offer.viewed`, `offer.signed`, `offer.accepted`, `offer.declined`, `offer.expired`, `interview.scheduled`, `interview.rescheduled`, `interview.cancelled`, `interview.completed`, `interview.feedback_submitted`, `approval.requested`, `approval.decided` (the live list is always in the `availableEvents` field of GET /api/tenants/me/webhooks). 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 }

Integrations

The tenant's connected tools — Slack / Teams channels, calendar accounts, job-board feeds, HRIS export. The catalogue is honest: a provider only ever shows as connected when a real, working row exists for your workspace; env-gated providers (calendar OAuth) report `needs_env` until the deployment operator adds the client credentials.

GET/api/integrations/statussession
Full integration posture for your workspace: every provider with availability (live / needs_env / coming_soon), connection state, connected rows and calendar accounts. Secrets are never included.
// Response
{ "ok": true, "providers": [ { "id": "slack", "name": "Slack", "category": "communication", "availability": "live", "connected": true, "rows": [...] } ], "email": { "active": false, "note": "dry-run" } }
GET/api/integrationssession
List this workspace's integration rows (connected channels, feeds marked as registered).
POST/api/integrationssession + role
Connect a provider. For Slack / Teams pass the incoming-webhook URL — it is SSRF-checked, then stored encrypted; pick which events post to the channel.
Roles:adminchro
// Request body
{ "provider": "slack", "displayName": "#hiring", "config": { "webhookUrl": "https://hooks.slack.com/services/…", "events": ["candidate.applied", "offer.accepted"] } }
POST/api/integrations/[id]/testsession + role
Send a test message through a chat integration. Updates the row's status/lastError with the real outcome.
Roles:adminchro
// Response
{ "ok": true, "delivered": true, "status": 200 }
DELETE/api/integrations/[id]session + role
Disconnect an integration. The stored (encrypted) webhook URL / config is deleted with the row.
Roles:adminchro
POST/api/integrations/promotesession + role
Record that a vacancy was posted to a channel (feeds, LinkedIn/X/WhatsApp shares). Appends to vacancy.postedTo so the whole team sees distribution state.
Roles:adminchrorecruiterhiring_manager
// Request body
{ "vacancyId": "vac_…", "channel": "linkedin-feed", "url": "https://…", "action": "mark" }
GET/api/integrations/hris/export?format=csv|json&since=YYYY-MM-DDsession + role
Download hires with accepted offers as CSV/JSON in an HRIS-import layout (BambooHR, Workday, HiBob, SAP — mapping in docs/INTEGRATIONS/HRIS.md). Audited; X-TalentOS-Export-Count header carries the row count.
Roles:adminchrorecruiter

Calendar sync

Google Calendar / Microsoft 365 sync for interviews, plus a standards-based .ics fallback that always works. OAuth connect is env-gated — when the deployment has no OAuth client configured the UI says so truthfully and interviews still carry .ics invites by email.

GET/api/integrations/calendar/[provider]/connect?returnTo=/integrationssession + role
Start the OAuth flow for `google` or `microsoft` (browser navigation — redirects to the provider). Signed HMAC state + nonce cookie bind the flow to this browser and tenant.
Roles:adminchrorecruiterhiring_managerinterviewer
GET/api/integrations/calendar/[provider]/callbackpublic
OAuth redirect target (public — trust comes from the signed state, not a session). Stores tokens encrypted per tenant and redirects back into the app.
GET/api/calendar/connectionssession
List connected calendar accounts in this workspace (no tokens — public projection only).
DELETE/api/calendar/connections/[id]session
Disconnect a calendar. Owners can remove their own connection; admins/CHROs can remove anyone's.
POST/api/calendar/freebusysession + role
Free/busy lookup through a connected calendar (409 with `noConnection: true` when none is connected). Addresses the provider can't resolve come back under `errors` — treat them as unknown, not free.
Roles:adminchrorecruiterhiring_managerinterviewer
// Request body
{ "emails": ["jane@acme.com"], "from": "2026-09-01T08:00:00Z", "to": "2026-09-01T18:00:00Z" }
// Response
{ "ok": true, "provider": "google", "busy": { "jane@acme.com": [{ "start": "…", "end": "…" }] }, "errors": {} }
POST/api/calendar/pushsession + role
Push an interview to the tenant's connected calendar (runs as a background job; writes back externalEventId + meeting link). 409 when no calendar is connected.
Roles:adminchrorecruiterhiring_manager
// Request body
{ "interviewId": "iv_…" }
GET/api/interviews/[id]/icssession
Download the interview as an RFC 5545 .ics file (text/calendar attachment).
POST/api/interviews/[id]/notifysession + role
Re-send the interview invite email (with .ics attachment) to the candidate and interviewers. Reports dry-run truthfully when the deployment has no email provider configured.
Roles:adminchrorecruiterhiring_manager

Job feeds + distribution

Public per-tenant feeds job boards crawl, plus schema.org JobPosting JSON-LD embedded on every public vacancy page for Google for Jobs. Feeds list only open, non-internal vacancies, refresh hourly, and never include recruiter emails. Tenants opt out with feature flags (indeedFeed / linkedinFeed).

GET/api/careers/[tenantId]/feed.xmlpublic
Indeed-format XML feed of the tenant's open vacancies (also accepted by Glassdoor and other boards ingesting the Indeed schema). Runbook: docs/INTEGRATIONS/INDEED-XML-FEED.md.
GET/api/careers/[tenantId]/linkedin.xmlpublic
LinkedIn Limited Listings XML feed. Register once in LinkedIn's job-source onboarding; listings stay in sync via partnerJobId. Runbook: docs/INTEGRATIONS/LINKEDIN-XML-FEED.md.

Offer e-sign (candidate)

The candidate side of native offer e-signature. The 256-bit token in the signing link is the credential (same posture as a password-reset link) — no account needed. Requests are rate-limited per IP — 30/min on the GET, 10/min on the POST — by the same in-process counter used elsewhere, so the cap is per function instance rather than fleet-wide; accepting stores a full evidence record (typed name, IP, user agent, SHA-256 of the letter body) and fires offer.viewed / offer.signed / offer.accepted webhooks.

GET/api/offer-sign/[token]public
Candidate-safe offer projection (title, compensation, letter body, company branding). First view stamps viewedAt and fires offer.viewed.
POST/api/offer-sign/[token]public
Accept (typed-name click-to-sign) or decline the offer. Accept requires the typed name and explicit agreement.
// Request body
{ "action": "accept", "signedName": "Jane Doe", "agree": true }  |  { "action": "decline", "reason": "…" }
POST/api/offers/[id]/sendsession + role
Staff side: send the offer for signature. Mints the signing token, emails the candidate, and returns the signing URL — with `dryRun: true` when the deployment has no email provider, so you can copy the link and share it yourself.
Roles:adminchrorecruiterhiring_manager
// Response
{ "ok": true, "url": "https://…/offer/<token>", "dryRun": false }

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.