API reference

API changelog

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

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

  6. 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".

  7. 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.

  8. 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.

  9. 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.

  10. 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).

  11. 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).

  12. 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.

  13. 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.

  14. 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.

  15. 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.

  16. 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).

  17. 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.

  18. 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.

  19. 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).

  20. 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).

  21. 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.

  22. 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.

  23. 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.

  24. 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).

  25. 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.

  26. 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).

  27. 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.

  28. 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.

  29. 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.

  30. 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.

  31. 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.

  32. 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.

  33. 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.

  34. 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.