API reference

API changelog

Every change to the V1 API, newest first. Current version: 7.2.0. This page and the OpenAPI spec are composed from the same source, so they can never disagree.

  1. 7.2.0

    2026-08-16

    booking an internal interview (POST /interviews with flavour internal, the default) now moves a `yes` application to `screen` - the screen stage holds phone screens and internal interviews, and `client_interview` is only ever client-flavour interviews. Triage applications (`funnel` / `maybe` / `no`) are never moved by a booking, and an application already at `screen` or with the client stays put. The move fires the usual application.stage_changed webhook. Client-interview booking is unchanged: `submitted` moves to `client_interview`.

  2. 7.1.0

    2026-08-14

    actions now walk candidates through the `screen` and `references` stages automatically. Sending an AI voice screen (send_ai_voice_screen) or completing a phone screen form moves a `yes` application to `screen`; confirming a phone-screen draft with outcome advance or hold does the same, reject parks any pre-client application at `no`, and no_show never moves anyone. Requesting referee details (request_referee_details) from an application at `client_interview` moves it to `references`; requests at any other live stage still send without a stage move, and references completing never moves the stage. Every move fires the usual application.stage_changed webhook. Untriaged candidates (`funnel` / `maybe`) are never moved by a screen send, and the AI screen's score never moves anyone - stage moves stay human-driven.

  3. 7.0.0

    2026-08-14Breaking change

    the 9-stage agency model became the 12-stage model. Three stages joined the `stage` vocabulary: `funnel` (the landing bucket - every new application now starts here instead of `maybe`, and GET /stages reports it as the default), `screen` (the consultant's phone screen, between `yes` and `submitted`), and `references` (the reference check, between `client_interview` and `offer`). `maybe` now means a human's reviewed-but-undecided mark rather than awaiting triage; existing `maybe` rows were migrated to `funnel`. Every stage enum in the spec, GET /stages, and POST /applications/{id}/stage accept the new values. Clients that validate the stage enum strictly must add the three values; nothing else about the stage contract changed - there is still no transition graph.

  4. 6.2.0

    2026-08-14

    A job records how its salary is quoted. The Job object's `compensation` gains `period` (`year`, `day` or `hour`), and both POST /jobs and PATCH /jobs/{id} accept it. Omit it on create and the work type decides: `employment_type: contract` gives `hour`, anything else `year` - the same rule the product uses, so a contract role's rate is no longer created, stored and advertised as an annual salary. `day` is never inferred; send it to quote a day rate. PATCH never infers the period at all: changing employment_type does not re-label figures already on the job. Jobs created before this release hold no period and report `year`, which is how they have always displayed.

  5. 6.1.0

    2026-08-11

    The Lovelio Connect connect flow is declared in the spec instead of described in prose. A second security scheme, appOAuth2, carries the authorization and token URLs, the PKCE requirement (S256 only), and every scope a partner app may request with its plain-English meaning - so a generated client can drive the connection rather than reading it out of a paragraph. Nothing about the flow changed; it was already served at /api/oauth/authorize and /api/oauth/token. The URLs are the region's own, because an OAuth grant is regional tenant data, the same reason each region is its own server entry. Both the spec and the developer portal's runnable code samples now read one module, lib/connect/connectFlow.ts, so neither can describe a flow the server does not serve.

  6. 6.0.0

    2026-08-10Breaking change

    Quotas are readable. Three new endpoints under the new quotas:read scope: GET /quotas/plan (the agency's quota plan - cadence, currency, recognition basis, what counts), GET /quotas/periods (periods newest first; they auto-roll, so the current one always exists), and GET /quotas/periods/{id} where id is a qpd_ id or the literal `current` (the period's board - recognised credit-share money, placement counts and targets for the agency, each team and each consultant, plus the adjustments ledger). Figures come from the quota recognition engine, so recognition basis, credit splits, adjustments and locked-period snapshots are all respected and the API always agrees with the product. Money is a list of one line per currency and is never converted. The board is pruned to the key's data scope: a key that cannot see the whole agency gets its own teams and people, and neither the agency block nor agency_target. BREAKING: the batch actions ask_analytics and run_report now require analytics:read instead of clients:read - they answer across the whole book, including per-consultant money, so clients:read was too weak a gate. Re-issue any key that used those two actions with analytics:read. For Lovelio Connect apps, quotas:read is an elevated-risk scope (it reads what people earn and are measured against), so an app requesting it gets the elevated review tier.

  7. 5.3.0

    2026-08-09

    The BD patch drafts itself. An agency that has never described its patch no longer starts from nothing: update_bd_patch (clients:write) takes a third payload, { draft: true }, which reads the enrichment already in the account (the deep company-intelligence tier first, the signup website scrape as the fallback) and returns data.draft { profile, territory, evidence } - role and sector chips to remove, plus city / region / country territory options to pick one of. Removing a SECTOR chip states an exclusion, which is how exclusions get stated at all (no agency publishes what it will not do) and removes those employers from the map; removing a ROLE chip only leaves it undeclared, so the boards are no longer swept for it and it no longer counts as fit, without blocking any employer. Like the { statement } preview, the draft writes nothing; saving is still { apply: true, statement, profile }. data.draft is null when the account holds too little to draft from.

  8. 5.2.0

    2026-08-05

    Contacts enter the searchable pool. GET /candidates now includes people who exist only as client contacts - a hiring manager is a future candidate. Candidate objects gain `contact_only` (boolean): true while the person exists purely as a contact, flipping false the moment they enter a candidate process. New `contact_only` query param filters either way; omit it to list everyone. POST /candidates/search (AI search) includes contact people by default too - narrow with a person_role facet ('candidates only' / 'contacts only') in the query.

  9. 5.1.0

    2026-08-05

    Contacts are relationships to a person. Contact objects gain `person_id` (cnd_, nullable): the person behind the contact, the same id GET /candidates serves them under. A contact's `name` lives on that person record and is shared across every role they hold - PATCHing name on /clients/{id}/contacts/{contactId} renames the person everywhere they appear. POST /clients/{id}/contacts resolves the person first (a matching email, LinkedIn URL or phone links to an existing person); a person already on that client's contact list is returned as-is rather than duplicated (still 201). GET /candidates excludes people who exist only as client contacts - they join the candidate pool when they enter a process.

  10. 5.0.1

    2026-07-21

    POST /jobs spec corrected to match what the endpoint actually accepts (no behaviour change). Compensation is a nested `compensation` object ({ base_min, base_max, currency }), not the top-level salary_min / salary_max the spec used to show - those were silently ignored, creating jobs with no salary. `client_id` is now documented as required (it always was: omitting it returns 422). Added `employment_type` (full_time / part_time / contract) and `location.state`, both already honoured. The success-in-90-days field is named `success_in_90_days`; the spec's old `ninety_day_goal` was never read.

  11. 5.0.0

    2026-07-19Breaking change

    The custom automations rule engine retired (settings diet: its useful cases are covered by the curated Email Automations catalog and Workflow rules, and no tenant had ever created a rule). Removed: every /automations endpoint, the Automation schema, the automations:write scope, and the four batch actions create_automation, update_automation, toggle_automation, delete_automation. The custom_automation_rules email opt-in disappeared from the automations catalog with it.

  12. 4.1.0

    2026-07-19

    Describe-a-job confirm loosened: the client is now the ONLY mandatory gap. POST /jobs/from-description/{review_token}/confirm no longer answers 409 draft_has_gaps for missing team, location, compensation, or employment type - those fields are optional and editable on the created job. A draft without a client still refuses, and skip_job_draft_gap still cannot clear it.

  13. 4.0.0

    2026-07-19Breaking change

    Client terms became the one source of truth for fees. The four fee-variation batch actions (request_fee_variation, approve_fee_variation, decline_fee_variation, withdraw_fee_variation) were removed along with the whole approval workflow; the single replacement is set_client_fee_terms (clients:write), which sets a client's terms directly - { client_id, terms } or { client_id, revert: true } for back-to-standard - and is admin-only by default. Jobs now inherit the client's live terms: a job with no fee of its own prices off the client schedule at read time, so editing client terms flows to every open job instantly; only the admin-only per-job override writes a job's fee fields. The fee floor (floor_percent) and the below-floor flag are gone with the approvals. Fee schedule objects no longer carry floor_percent or source_request_id.

  14. 3.23.0

    2026-07-19

    The Marketplace became an opt-in module, off by default. Every /marketplace route now answers 403 MARKETPLACE_DISABLED until an agency admin turns the module on in Settings > Integrations (the Premium requirement is unchanged: 402 PREMIUM_REQUIRED still comes first). Agencies that already had a listing, intro, or deal were switched on automatically, so no active trade lost access.

  15. 3.22.0

    2026-07-16

    Tags. Flat labels on candidate records - no colours, no categories, just names. The Candidate object's tags field is now real: it has existed since 1.0 and returned an empty array for every candidate ever, and it now returns the labels on the record, alphabetically. POST /candidates accepts tags, and PATCH /candidates/{id} replaces the full set (an empty array clears them, omitting the field leaves them alone). Labels are deduplicated case-insensitively per account, so "Python" and "python" are one tag and it comes back with the spelling it was first created with; max 20 tags per candidate, 40 characters each. New batch action: update_candidate_tags, which takes either tags (replace the list) or add / remove (change it without knowing the rest). Search understands tags: "consultants tagged ex-McKinsey".

  16. 3.21.0

    2026-07-16

    Candidate preferences. The Candidate object gains preferred_roles, preferred_seniority (intern..c_suite), work_types (permanent, contract, temp, part_time), open_to_relocation, available_from (date), preferred_locations (gazetteer-resolved for search), plus salary_expectation_amount + salary_expectation_period derived from salary_expectation (day and hour rates are first-class; an annual figure is never fabricated from a rate). POST /candidates and PATCH /candidates/{id} accept all of them; preferred_locations replaces the full set on every write. New batch actions: update_candidate_preferences and extract_candidate_preferences (reads the candidate's notes and proposes a patch; apply: true writes the confirmed proposal). Null always means never captured.

  17. 3.20.0

    2026-07-14

    Pay and availability capture. The Candidate object gains salary_expectation (raw, verbatim), salary_expectation_annual + salary_currency (derived), notice_period + notice_period_days (derived; 0 = immediately available), work_rights + work_rights_status (derived: citizen, permanent_resident, visa_holder, requires_sponsorship), and years_experience now prefers the employment-ledger rollup (overlap-merged from dated employment) over the raw work-history sum. PATCH /candidates/{id} accepts the three raw fields; the normalizations derive automatically and cannot be set directly. Sources that fill these: CV parse (explicit statements only), interview call insights (the freshest, overwrites), and edits. Null always means never captured, not zero.

  18. 3.19.0

    2026-07-14Breaking change

    Job matching runs on the faceted search core - one engine for every candidate-finding surface. The match_candidates_to_job batch op's result changed shape: each match now carries a deterministic `band` (strong / good / partial), `coverage` ({ matched, total } requirements), per-requirement `evidence` rows (each with label, matched, source, detail, and an inferred flag - derived from the data, never generated), and a `narration` sentence written after ranking. The 0-100 `score`, `rationale` and `location_mismatch` fields are gone (a location mismatch now shows as an unmatched location evidence row). The result also returns the job's compiled requirement `facets`, the honest `total_count` of candidates matching every hard requirement, and a `search_log_id` - pass it back as `search_log_id` on create_application so placements attribute to the match run. New optional payload field `facet_state` re-runs the match with edited requirements.

  19. 3.18.2

    2026-07-14

    Marketplace deal lifecycle and API standards. PATCH /marketplace/deals/{id} gains action close (with a required reason): either side ends an unplaced deal, the reason goes to the other agency, and a new webhook event marketplace.deal.closed fires for both sides; placed deals never close this way. The MarketplaceDeal object gains placed_currency (the placement's real currency - placed money renders in this, while currency stays the listing's asking figure) and closed_at / closed_by / closed_reason. mark_paid and mark_received are now idempotent: the first stamp is the record, repeats are no-ops that never overwrite the timestamp or re-notify. Status codes joined the platform standard: the marketplace premium gate returns 402 with a pricing link (was 403), responding to an already-answered intro returns 409 (was 422), and a tenant that is not a party to an intro gets the same 404 as a missing id (no existence probe).

  20. 3.18.1

    2026-07-14

    Marketplace hardening. PATCH /marketplace/deals/{id} gains action add_to_pipeline (with job_id): the agency the candidate was revealed to turns them into a real candidate + application on one of its jobs and links the deal, so deal status tracks the pipeline on candidate-listing deals too. The prior_knowledge object returned by POST /marketplace/intros/{id}/respond is now fully documented and snake_case ({ known, matched_by, added_at, matched_candidate_id, note }); it is null on request intros pre-accept, where the verdict belongs to the requesting agency. Accepting an intro on a withdrawn, filled or expired listing is now refused, and withdrawing or filling a listing resolves its pending intros immediately (senders are notified with the real reason). Listing text fields are hard-capped (headline 200, chips 60 each, pitch 4,000, note 2,000 characters).

  21. 3.18.0

    2026-07-14

    The cross-agency split marketplace reaches the API (Premium plans). New endpoints under /marketplace: browse and share listings (GET/POST /marketplace/listings, GET/PATCH /marketplace/listings/{id}), make and answer intros (GET/POST /marketplace/intros, GET /marketplace/intros/{id}, POST /marketplace/intros/{id}/respond with check / accept / decline / withdraw), and read deals with the paid/received handshake (GET /marketplace/deals, GET/PATCH /marketplace/deals/{id}). New scopes marketplace:read and marketplace:write; the eight marketplace batch ops moved from the borrowed jobs/candidates/submissions/placements scopes to marketplace:write. Three new webhook events: marketplace.intro.received, marketplace.deal.formed, marketplace.deal.placed (deal events fire for both agencies). External ids: mkl_ (listing), mki_ (intro), mkd_ (deal). The posture rides the API unchanged: reveal-on-accept, pre-scrubbed snapshots only, Lovelio takes 0% and never touches the money.

  22. 3.17.0

    2026-07-10

    Spec (Float) client feedback reaches parity with submissions. The Spec object gains response_rating (the client's 1-5 rating, captured with their interest; null if not rated). Client questions on a spec are now a persisted Q&A thread rather than a one-shot notification: new batch op answer_spec_question (submissions:write) posts the consultant's reply and emails the contact the answer is waiting, mirroring answer_submission_question.

  23. 3.16.0

    2026-07-08

    Specs (Floats in AU/NZ) - the speculative submission: one FULLY ANONYMOUS candidate profile sent to one client contact with no job, the agency's terms of business attached as a PDF, and a no-login profile page where the contact responds one-click. New endpoints: GET/POST /specs, GET /specs/{id} (spc_ ids) under the submissions scopes. The candidate's name and current employer are scrubbed server-side from every client-facing field; a candidate can never be specced to their own employer. New batch ops: send_spec (submissions:write), chase_spec (the consultant-approved nudge on a quiet spec), convert_spec (jobs:write - interest becomes a real job + application at the submitted stage via the standard creation paths). New webhook events: spec.sent, spec.viewed, spec.responded, spec.converted. A placement of the candidate at the client within 12 months carries the introduction record back to the spec. The send_spec_pitch batch op is retired, absorbed by send_spec.

  24. 3.15.0

    2026-07-06

    The BD engine is queryable. New read endpoints: GET /bd/targets (territory map; filters status, market, min_score), GET /bd/targets/{id} (includes the reason-to-call event feed), GET /bd/leads (+ /{id}), GET /bd/patch (declared patch + territory state). Targets carry bdt_ ids and leads bdl_ ids, and the BD batch ops (pursue_bd_target, dismiss_bd_target, convert_bd_target, log_bd_target_touch, dismiss_bd_lead, convert_bd_lead) accept them alongside raw ids. Writes stay batch ops - one write path per action, every surface. Also: GET /webhooks/events returns the machine-readable event catalogue (every event listed is really emitted); created_after / created_before ISO date filters on GET /jobs, /candidates, /applications, /placements, /interviews (a malformed value is a 422, never ignored); GET /activities now returns cursor pagination meta and honours the standard base64 after cursor (a raw ISO timestamp still works); key-type semantics documented (sk_live_ production, sk_test_ development against the same workspace data, sk_trial_ expires 7 days after signup). The candidate_hired Slack notification type was removed - nothing ever emitted it; placement_created covers the win.

  25. 3.14.0

    2026-07-06

    Spec correctness pass. The webhook event catalogue is now the real, emitted set (account.enriched, application.received, application.scored, application.stage_changed, application.status_changed, candidate.enriched, email.sent, email.scheduled, email.cancelled, interview.scheduled, interview.cancelled, interview.outcome_recorded, job.created, job_ad.published, placement.created, placement.status_changed, reference.received, submission.created, submission.responded, *) - the earlier list named events that were never fired (application.created/updated, candidate.created/updated, interview.completed, job.updated/closed/daily_summary_ready, form_instance.completed, review_queue.created). POST /webhooks and PATCH /webhooks/{id} both validate against this set. Method corrections: POST (not PATCH) /applications/bulk-update; POST /health is documented (webhook test target); the phantom PATCH /job-ads/{id} (edit ad copy via PATCH /jobs/{id}/ad) and GET /accounts/{id}/api-keys/{keyId} entries were removed. GET /applications and GET /candidates now return cursor pagination meta and honour the `after` cursor (they previously ignored it).

  26. 3.13.0

    2026-07-06

    BD patch. The agency describes what it recruits in its own words (roles, regions, seniority, anything it never touches) and that statement becomes the declared half of the BD engine: declared desks count as lookalike fit before any placement exists, and exclusions remove targets and leads outright. New batch op update_bd_patch (clients:write), two-phase: call with { statement } to get data.preview (the AI's structured reading and a playback line, no write), then { apply: true, statement, profile: preview.profile } to save. Saving triggers an immediate territory re-score.

  27. 3.12.0

    2026-07-05

    BD reason-to-call. Territory targets accumulate a dated intelligence feed (market signals like a hiring spike or hiring resuming, candidate-corpus signals, and consultant-logged touches). New batch op log_bd_target_touch { target_id, note? } (clients:write): records that the CONSULTANT reached out (call, coffee, their own email) and resets the cadence clock. Lovelio never drafts or sends outreach - the log is the only automation.

  28. 3.11.0

    2026-07-05

    BD territory map. The BD radar now keeps a standing map of the agency's patch: every direct employer the market observatory has seen recently, scored 0-100 against the agency's own history (placements, jobs worked, salary bands - deterministic, nothing invented). Targets carry a right-to-win line and ad evidence; the human owns the status. Three new batch ops (clients:write): pursue_bd_target { target_id, stop? } (pin a target you are actively working; stop: true unpins), dismiss_bd_target { target_id } (forever - the target leaves the map and the employer is never suggested as a lead again) and convert_bd_target { target_id, website? } (creates the client record; enrichment and Client DNA kick off automatically).

  29. 3.10.0

    2026-07-04

    Business development leads. Lovelio's BD radar scans live job ads daily and surfaces a handful of targeting briefs per agency: companies that look like your best clients, hiring right now, with your own placement history as the proof. Briefs only - Lovelio never drafts or sends outreach; humans own the BD. Two new batch ops (clients:write): dismiss_bd_lead { lead_id } (forever - a dismissed employer is never suggested again) and convert_bd_lead { lead_id, website? } (creates the client record; enrichment and Client DNA kick off automatically).

  30. 3.9.0

    2026-07-04

    Video calls. New batch op start_video_call (interviews:write): creates a Lovelio video call room on an interview and emails the candidate a tokenised no-login join link (pass skip_email: true to get the link without emailing). The call is recorded (audio only) and transcribed live with per-speaker attribution; the recording and transcript land on the interview. Re-running while a link is still open re-uses it. Returns { room_id, candidate_join_url, expires_at, email_sent, reused }. Participants give explicit recording consent on the join screen before any device access.

  31. 3.8.0

    2026-07-04

    Spec pitching. New batch op send_spec_pitch (clients:write): sends the consultant-approved speculative (MPC) pitch email behind a spec_match talent-radar card - a hot candidate whose profile echoes a client's Client DNA, pitched to the client contact with no live role. Payload { review_item_id, client_contact_id?, email_subject, email_body }; the card resolves on send. Nothing is ever sent without a person approving the copy.

  32. 3.7.0

    2026-07-04Breaking change

    GET /jobs/{id}/weekly-digest is gone. The Monday-morning weekly job digest was retired along with the endpoint that mirrored it. For pipeline numbers, use GET /jobs/{id} stats or GET /analytics/funnel.

  33. 3.6.0

    2026-07-03

    Database matching + per-job AI screening. New batch op match_candidates_to_job (candidates:read): ranks the whole candidate database against a job (semantic match + AI re-rank) and returns up to 25 matches with a 0-100 score and a one-line rationale each - read-only, attach a match with create_application (pass search_query so the source is credited as Database Search). New batch op set_ai_screening (jobs:write): turns automatic AI voice screening on or off for a job and sets the screen's focus areas. When on, every inbound applicant whose AI verdict lands yes or maybe is invited to the voice screen automatically; candidates a consultant adds by hand are never auto-invited. The Job object gains ai_screen_enabled (boolean) and ai_screen_focus (array of strings, nullable).

  34. 3.5.0

    2026-07-03

    AI voice screen. New batch op send_ai_voice_screen (applications:write): emails the candidate a tokenised no-login link to a ~10 minute AI voice screening conversation in the browser. When the conversation ends the transcript is scored per question and the application gains a phone_screen_score (0-100). Re-sending while a link is still open re-uses the existing link.

  35. 3.4.0

    2026-07-02

    Teams are desks of consultants. Team membership moved off the retired employee spine onto the member record, so `team_ids` on a User now reflects the accepted member's team and is always [] for a user whose invite is pending. POST /accounts/{id}/users no longer accepts `team_id` (it returns a validation error): assign a team after the user accepts their invite, via PATCH /accounts/{id}/users/{userId} with `team_id` (send null to clear).

  36. 3.3.0

    2026-07-02Breaking change

    Placements replaced Offers. In agency perm recruitment the client makes and signs the offer with the candidate, so the offer-letter machinery (offer letters, e-signature portal, HM approval) is retired: every /offers* endpoint is gone, along with the Offer schema, the offers:read / offers:write scopes, the offer.* webhook events, and the offer_created / offer_updated activity types. In their place: a Placement is the record of the win - candidate, client, job, salary, fee (percent of first-year salary or fixed; fee.amount is the computed billing figure), start_date, and a guarantee lifecycle (pending_start, started, fell_off, completed). New endpoints GET/POST /placements and GET/PATCH /placements/{id} under placements:read / placements:write. New batch ops log_offer (marks the client's offer - moves the application to the offer stage, applications:write), create_placement, update_placement, mark_placement_status. New webhook events placement.created and placement.status_changed. Logging a placement against an application moves it to placed automatically.

  37. 3.2.0

    2026-07-02

    Submissions - the core agency loop. A submission is a shortlist of candidates on a job, sent to the job's client contact as a tokenised no-login link; the client requests interviews, passes with a reason, or asks questions, and every response updates the pipeline. New endpoints: GET/POST /submissions, GET /submissions/{id} under the new submissions:read / submissions:write scopes. Sending moves triage-stage candidates to submitted automatically. New batch ops create_submission and answer_submission_question. Interviews gained a `flavour` (internal, the agency's own screen, or client) and a nullable `submission_id` linking a client interview to the submission it came from; booking a client interview moves the application to client_interview. New webhook events: submission.created, submission.responded.

  38. 3.1.0

    2026-07-02

    Jobs belong to clients. The Job object gains `client` and `client_contact`; POST/PATCH /jobs accept `client_id` (cli_) and `client_contact_id` (cct_, must belong to the client); GET /jobs filters by `client_id`. Full REST surface for clients: GET/POST /clients, GET/PATCH/DELETE /clients/{id}, GET/POST /clients/{id}/contacts, PATCH/DELETE /clients/{id}/contacts/{contactId} under clients:read / clients:write. Creating a client with a website triggers enrichment and an automatic Client DNA first pass; assessments for that client's jobs score against it.

  39. 3.0.0

    2026-07-02Breaking change

    the 6-state triage model became the 9-stage agency model. The `stage` field now takes one of: maybe (default), yes, no, submitted, client_interview, offer, placed, rejected, withdrew. `hired` was renamed to `placed`; existing rows were migrated. Three client-side stages were added: submitted (sent to the client), client_interview, and offer. Actions drive stages: creating an offer moves the application to offer, an accepted offer moves it to placed. Job stats objects now include one count per stage and the `hired` count was renamed `placed`. Clients arrived: a client is a company the agency recruits for, a record inside the tenant. New batch ops create_client, update_client, delete_client, add_client_contact, update_client_contact, delete_client_contact under the new clients:write scope.

  40. 2.8.0

    2026-06-24

    Two-tier application source. `source` on the Application object is now a canonical category (Careers Page, Job Board, Referral, Talent Pool, Database Search, Social, Agency, Direct, Imported, Other) and a new `source_detail` field carries the granular sub-source (exact board, referrer, pool, or search). POST /applications accepts either a `source` slug (mapped to a category + detail) or the structured `source_category` + `source_detail` pair. The old free-form source enum (LinkedIn, Indeed, Job Ad, ...) is gone; those values now map to a category with the old value preserved as detail. Filtering GET /applications by `source` filters on the category.

  41. 2.5.0

    2026-06-19

    Talent pools gained an optional `cover_color` (one of cyan, green, lime, amber, orange, or null). It sets the dashboard cover-tile colour; null falls back to a name-derived colour. Returned by GET, settable on POST and PATCH.

  42. 2.1.0

    2026-06-12

    GET /stages returns the 6-state model (the old transition-graph response is gone). New batch ops: create_talent_pool, remove_from_talent_pool. POST /accounts/verify-email documented. The dead /form-instances/{id}/submit entry was removed - form submission happens via the candidate-facing magic-link flow, not the V1 API.

  43. 2.0.0

    2026-05-18Breaking change

    the 11-stage pipeline collapsed to a 6-state triage model. The `stage` field now takes one of: maybe (default), yes, no, rejected, hired, withdrew. AI verdict (`ai_verdict`) emits the same in-play vocabulary: yes, maybe, no. Old values (new, longlisted, shortlisted, interviews, checks, offer, hired_onboarding, hired_onboarded, to_be_rejected, advance, consider, reject) are no longer accepted; existing rows were migrated: longlisted/shortlisted/interviews/checks/offer -> yes, new -> maybe, to_be_rejected -> no, hired_onboarding/hired_onboarded -> hired. There is no transition graph - any state can move to any other state. Actions (phone screen, interview, references, offer) are decoupled from state; run them on any yes application. To reject: POST /applications/{id}/reject. To withdraw: POST /applications/{id}/withdraw. To change state: POST /applications/{id}/stage.