# Lovelio API reference

Recruitment automation API. Create jobs, manage candidates, run ad-hoc actions, integrate with your tools.

Generated from the OpenAPI 3.1 document at https://lovelio.ai/api/v1/openapi.json.
API version 1.0.0. 161 paths, 239 operations.

This is the plain-markdown copy of the reference rendered at https://lovelio.ai/docs/api.

## Fetch less than all of this

| URL | What you get |
| --- | --- |
| `https://lovelio.ai/docs/api.md` | Everything: this header plus every operation in full. |
| `https://lovelio.ai/docs/api.md?index=1` | This header plus the endpoint index. No per-operation detail. |
| `https://lovelio.ai/docs/api.md?tag=Jobs` | This header plus one section in full. Comma-separate for several: `?tag=Jobs,Applications`. |
| `https://lovelio.ai/api/v1/openapi.json` | The machine-readable source this file is generated from. |

Tags: System, Accounts, Jobs, Job Ads, Candidates, Candidate Imports, Applications, Interviews, Submissions, Placements, Quotas, Marketplace, Webhooks, Batch, Activities, Documents, Tasks, Outreach, Talent Pools, Review Queue, Forms, Referees, Integrations, Chat Integrations, Analytics, Calendar, Scheduled Emails, Workflow Rules, Email Templates, Stages, Distribution, Clients, Specs, Search, Business Development.

## Base URL

Docs live on lovelio.ai. The API does not. Every workspace belongs to a region and an API key only works on its own region's host. Calling `lovelio.ai/api/v1/...` returns a JSON error naming all three hosts rather than data.

| Region | Base URL |
| --- | --- |
| Production (US (N. California)) | `https://us.lovelio.ai/api/v1` |
| Production (EU (London)) | `https://eu.lovelio.ai/api/v1` |
| Production (ANZ (Sydney)) | `https://anz.lovelio.ai/api/v1` |

A workspace's exact host is shown in Settings > API keys.

## Authentication

```http
Authorization: Bearer sk_live_...
```

API key, sent as Authorization: Bearer <key>. Three key types: sk_live_ (production), sk_test_ (development - same workspace, same data, marked as a test key; use a separate workspace if you need isolated test data), and sk_trial_ (issued at signup, expires 7 days later - swap to a live key from Settings > API keys). Keys are server-generated, shown once at creation, and scoped per permission (or admin for everything). Lovelio Connect apps authenticate with the OAuth access token from the connect flow instead (Authorization: Bearer lc_at_...) - it carries exactly the scopes the agency approved, enforced on every request. Money is gated at FIELD level, not route level: the three scopes placements:financials:read, clients:financials:read and marketplace:financials:read unlock salary, fee, contract rates, expected GP, commission percents, the client fee schedule and split-fee deal amounts. Without them those fields return null and everything else on the record still comes through - a read never 403s for want of a money scope. admin satisfies all three. Requesting any of them puts a Connect app in the elevated review tier. See /docs/agents/build-an-integration.

## Pagination and shared parameters

Paginated list endpoints declare the parameters they support below. For cursor-based lists, read `meta.next_cursor` from a response and pass it back as `after`. Bounded catalogue and configuration lists return their full result and do not take a cursor.

| Name | In | Type | Description |
| --- | --- | --- | --- |
| `limit` | query | integer, 1-100, default 25 | Rows per page. Clamped to the 1-100 range rather than rejected, so asking for 500 returns 100 - read meta.has_more and page with the cursor. Unparseable values fall back to 25. |
| `after` | query | string | Cursor for forward pagination |
| `created_after` | query | string (date-time) | Only rows created at or after this ISO 8601 date or datetime (inclusive). Malformed values are a 422, never silently ignored. |
| `created_before` | query | string (date-time) | Only rows created at or before this ISO 8601 date or datetime (inclusive). Malformed values are a 422, never silently ignored. |
| `Idempotency-Key` | header | string | Unique key for this mutation. Repeating the same request with the same key within 24 hours returns the saved response without running it again. |

## Rate limit headers

| Header | Type | Description |
| --- | --- | --- |
| `X-RateLimit-Limit` | integer | Requests allowed per minute |
| `X-RateLimit-Remaining` | integer | Requests remaining in window |
| `X-RateLimit-Reset` | integer | Unix timestamp when window resets |

## Guides

- https://lovelio.ai/docs/agents - what an agent can drive end to end
- https://lovelio.ai/docs/agents/top-10-tasks - runnable examples for the ten most common calls
- https://lovelio.ai/docs/agents/build-an-integration - sandbox agency, test API key, live webhook testing
- https://lovelio.ai/docs/agents/error-handling - error envelope, request IDs, retries, idempotency keys
- https://lovelio.ai/docs/agents/webhooks - subscribing and verifying HMAC signatures
- https://lovelio.ai/docs/agents/sdks - single-file TypeScript and Python clients
- https://lovelio.ai/docs/api/changelog - public API release history
- https://lovelio.ai/llms-full.txt - all of the above inlined in one fetch

## Endpoint index

Filtered to: Applications.

### Applications

Application pipeline with AI assessment

- `GET /applications` - List applications
- `POST /applications` - Create application (async)
- `GET /applications/{id}` - Get application
- `PATCH /applications/{id}` - Update application
- `POST /applications/{id}/assessment` - Re-assess application
- `POST /applications/bulk-update` - Bulk update applications
- `POST /phone-screens/from-description` - Stage a phone-screen log
- `GET /phone-screens/from-description/{review_token}` - Get a staged phone-screen draft
- `PATCH /phone-screens/from-description/{review_token}` - Patch fields on a staged phone-screen draft
- `POST /phone-screens/from-description/{review_token}/confirm` - Confirm a phone-screen log
- `POST /applications/{id}/stage` - Transition application stage

---

## Applications

Application pipeline with AI assessment

### GET /applications

**List applications**

Operation ID: `listApplications`

Scopes: `applications:read`

**Query parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `limit` | integer, 1-100, default 25 | no | Rows per page. Clamped to the 1-100 range rather than rejected, so asking for 500 returns 100 - read meta.has_more and page with the cursor. Unparseable values fall back to 25. |
| `after` | string | no | Cursor for forward pagination |
| `job_id` | string | no |  |
| `candidate_id` | string | no |  |
| `stage` | string | no |  |
| `source` | string | no |  |
| `created_after` | string (date-time) | no | Only rows created at or after this ISO 8601 date or datetime (inclusive). Malformed values are a 422, never silently ignored. |
| `created_before` | string (date-time) | no | Only rows created at or before this ISO 8601 date or datetime (inclusive). Malformed values are a 422, never silently ignored. |

**Responses**

| Code | Description | Body |
| --- | --- | --- |
| `200` | Paginated applications with assessment scores | { success, data: array of Application, meta } |

### POST /applications

**Create application (async)**

Attaches an existing candidate to a job and triggers AI assessment. Returns 202.

Source attribution: pass either `source` (a slug we map to a category, e.g. linkedin, indeed, job_ad, referral, careers_page) or the structured `source_category` + `source_detail` pair for full control. `source_category` is one of: Careers Page, Job Board, Referral, Talent Pool, Database Search, Social, Agency, Direct, Imported, Other. `source_detail` is the granular sub-source (the exact board, referrer, pool, or search). Omitting source records the API origin (Other / API).

Operation ID: `createApplication`

Scopes: `applications:write`

**Header parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `Idempotency-Key` | string | yes | Unique key for this mutation. Repeating the same request with the same key within 24 hours returns the saved response without running it again. |

**Request body** (`application/json`)

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `job_id` | string | yes | External job id (job_...). |
| `candidate_id` | string | yes | External candidate id (cnd_...). |
| `source` | string \| null | no | Source slug (linkedin, indeed, job_ad, referral, careers_page, ...). Mapped to a category + detail. Ignored when source_category is supplied. |
| `source_category` | string (`Careers Page`, `Job Board`, `Referral`, `Talent Pool`, `Database Search`, `Social`, `Agency`, `Direct`, `Imported`, `Other`) \| null | no | Canonical source category. |
| `source_detail` | string \| null | no | Granular sub-source (exact board, referrer, pool, or search). |
| `cover_note` | string \| null | no |  |

**Responses**

| Code | Description | Body |
| --- | --- | --- |
| `202` | Application queued for assessment | { success, data: Task } |

### GET /applications/{id}

**Get application**

Operation ID: `getApplication`

Scopes: `applications:read`

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes |  |

**Responses**

| Code | Description | Body |
| --- | --- | --- |
| `200` | Application with assessment | { success, data: Application } |

### PATCH /applications/{id}

**Update application**

Update non-stage fields (note). The `stage`/`status` fields are not accepted. Returns 410 if either is present; use POST /applications/{id}/stage for a manual move or the reject_candidate / withdraw_application batch actions.

Operation ID: `updateApplication`

Scopes: `applications:write`

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes |  |

**Request body** (`application/json`)

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `note` | string | no | Free-text note to store against the application. |

**Responses**

| Code | Description | Body |
| --- | --- | --- |
| `200` | Updated | { success, data: Application } |
| `410` | Gone - stage/status field rejected. Use POST /applications/{id}/stage or POST /batch. |  |

### POST /applications/{id}/assessment

**Re-assess application**

Queues a fresh assessment after the candidate, job requirements or Client DNA changed. The existing assessment remains readable until the new result replaces it.

Operation ID: `reassessApplication`

Scopes: `applications:write`

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes | app_ id. |

**Header parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `Idempotency-Key` | string | yes | Unique key for this mutation. Repeating the same request with the same key within 24 hours returns the saved response without running it again. |

**Responses**

| Code | Description | Body |
| --- | --- | --- |
| `202` | Assessment queued. | { success, data: object } |

### POST /applications/bulk-update

**Bulk update applications**

Operation ID: `bulkUpdateApplications`

Scopes: `applications:write`

**Header parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `Idempotency-Key` | string | yes | Unique key for this mutation. Repeating the same request with the same key within 24 hours returns the saved response without running it again. |

**Responses**

| Code | Description | Body |
| --- | --- | --- |
| `200` | Bulk update results | { success, data: object } |

### POST /phone-screens/from-description

**Stage a phone-screen log**

Stage a phone-screen outcome log for an application. Phone screen is the simplest draft cluster in Lovelio - three fields (outcome, notes, optional call metadata) with no LLM or streaming. The confirm step atomically writes a timeline note and moves the stage: outcome='advance' / 'hold' move a 'yes' application to 'screen' (the screen happened); outcome='reject' moves any pre-client application (funnel / maybe / yes / screen) to 'no' (no email sent - Send Rejection is a separate action); outcome='no_show' never moves the stage, and an application already with the client is never pulled back.

Operation ID: `composePhoneScreenFromDescription`

Scopes: `applications:write`

**Request body** (`application/json`, required)

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `application_id` | string | yes | External application ID (app_ prefix) |
| `initial_outcome` | string (`advance`, `reject`, `hold`, `no_show`) | no | Pre-fill outcome on the draft. Caller can change it via PATCH before confirm. |
| `initial_notes` | string | no | Pre-fill notes on the draft. Required before confirm succeeds. |
| `call_at` | string | no | Optional ISO-8601 timestamp of the call. |
| `duration_min` | number | no | Optional call duration in minutes. |
| `source` | string (`slack`, `mcp`, `dashboard`, `api`) | no | Provenance tag stored on the draft row. |

**Responses**

| Code | Description | Body |
| --- | --- | --- |
| `201` | Draft staged. Returns review_token, current draft, and remaining gaps. | { success, data: object } |
| `400` | application_id missing or invalid, or body was not JSON. |  |
| `404` | Application not found in this company. |  |

### GET /phone-screens/from-description/{review_token}

**Get a staged phone-screen draft**

Read current state of a staged phone-screen draft: outcome, notes, call metadata, and remaining gaps.

Operation ID: `getPhoneScreenDraft`

Scopes: `applications:read`

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `review_token` | string | yes |  |

**Responses**

| Code | Description | Body |
| --- | --- | --- |
| `200` | Draft state | { success, data: object } |
| `404` | Draft not found or already deleted. |  |

### PATCH /phone-screens/from-description/{review_token}

**Patch fields on a staged phone-screen draft**

Update one or more fields on an in-flight phone-screen draft (outcome, notes, call_at, duration_min). Recomputes gaps.

Operation ID: `updatePhoneScreenDraft`

Scopes: `applications:write`

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `review_token` | string | yes |  |

**Request body** (`application/json`, required)

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `outcome` | string (`advance`, `reject`, `hold`, `no_show`) \| null | no |  |
| `notes` | string \| null | no |  |
| `call_at` | string \| null | no | ISO-8601 timestamp or null to clear |
| `duration_min` | number \| null | no |  |

**Responses**

| Code | Description | Body |
| --- | --- | --- |
| `200` | Updated draft + remaining gaps | { success, data: object } |
| `422` | Draft missing, expired, or already confirmed. |  |

### POST /phone-screens/from-description/{review_token}/confirm

**Confirm a phone-screen log**

Promote a staged phone-screen draft: atomically log a timeline note AND move the stage per the outcome ('advance' / 'hold' move yes to screen, 'reject' parks pre-client applications at 'no', 'no_show' never moves). Refuses with 409 `{ error: { type: 'draft_has_gaps', gaps: string[] } }` when outcome or notes is empty. Dispatches to the existing add_note + move_stage handlers so every side-effect (timeline entry, automation trigger, undo record) stays consistent.

Operation ID: `confirmPhoneScreenDraft`

Scopes: `applications:write`

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `review_token` | string | yes |  |

**Header parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `Idempotency-Key` | string | yes | Unique key for this mutation. Repeating the same request with the same key within 24 hours returns the saved response without running it again. |

**Responses**

| Code | Description | Body |
| --- | --- | --- |
| `201` | Phone screen logged. Response contains application_id, outcome, new_stage (nullable for hold / no_show), note_id. | { success, data: object } |
| `409` | Draft has gaps - outcome or notes is empty. Fill them via PATCH /phone-screens/from-description/{review_token}, then retry. |  |
| `422` | Draft missing, expired, or already confirmed. |  |

### POST /applications/{id}/stage

**Transition application stage**

Manually moves an application to any canonical pipeline stage. There is no transition graph. Client-side stages still require real submission evidence: send the candidate to the client through a submission before moving them to submitted, client_interview, references, or offer. The API records the source as api and attributes a person-bound key to its acting user; callers cannot spoof another channel.

Operation ID: `transitionApplicationStage`

Scopes: `applications:write`

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes | Application external ID (app_ prefix) |

**Header parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `Idempotency-Key` | string | yes | Unique key for this mutation. Repeating the same request with the same key within 24 hours returns the saved response without running it again. |

**Request body** (`application/json`, required)

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `stage` | string (`funnel`, `maybe`, `yes`, `no`, `screen`, `submitted`, `client_interview`, `references`, `offer`, `placed`, `rejected`, `withdrew`) | yes |  |
| `reason` | string | no | Optional audit note explaining the manual move. |

**Responses**

| Code | Description | Body |
| --- | --- | --- |
| `200` | Stage transition completed | { success, data: object } |
| `404` | Application not found |  |
| `422` | Invalid stage, or a client-side stage requested before submission evidence exists. |  |

## Schemas

Objects referenced by the operations above.

### Application

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | no |  |
| `job_id` | string \| null | no |  |
| `job_title` | string \| null | no |  |
| `candidate_id` | string | no |  |
| `candidate_name` | string \| null | no |  |
| `candidate_email` | string \| null | no |  |
| `candidate_phone` | string \| null | no |  |
| `candidate_location` | object \| null | no | Coarsened to suburb (or city) and state - never a street address. |
| `candidate_location.city` | string | no |  |
| `candidate_linkedin_url` | string \| null | no |  |
| `candidate_cv_url` | string \| null | no |  |
| `candidate_skills` | array of string | no |  |
| `current_title` | string \| null | no | Snapshot of the candidate current_title at the moment they applied. Never updated after insert. |
| `current_company` | string \| null | no | Snapshot of the candidate current_company at the moment they applied. Never updated after insert. |
| `stage` | string (`funnel`, `maybe`, `yes`, `no`, `screen`, `submitted`, `client_interview`, `references`, `offer`, `placed`, `rejected`, `withdrew`) | no |  |
| `status` | string | no | Deprecated alias for stage. Use stage instead. |
| `source` | string \| null | no | Canonical source category: Careers Page, Job Board, Referral, Talent Pool, Database Search, Social, Agency, Direct, Imported, or Other. |
| `source_detail` | string \| null | no | Granular sub-source within the category - the exact board, referrer, pool, or search query. |
| `assessment` | object \| null | no |  |
| `assessment.job_fit_score` | number, 0-100 | no |  |
| `assessment.hm_fit_score` | number, 0-100 \| null | no | Client Fit (alignment with the client's DNA). Null when the job's client has no completed Client DNA, or when the job fit landed at or below the company's job-fit reject line - Client Fit is only scored for candidates above that line, and fills in when a consultant first opens the assessment (or via the complete_assessment action). |
| `assessment.overall_recommendation` | string (`strong_hire`, `hire`, `hold`, `talent_pool`, `reject`) | no |  |
| `assessment.summary` | string \| null | no | Consultant summary. Empty string while deferred: summaries are written when the job fit clears the company's reject line, and otherwise on the first consultant look (or via the complete_assessment action). |
| `assessment.alternative_teams` | array of string | no |  |
| `assessment.alternative_roles` | array of string | no |  |
| `assessment.confidence_score` | number, 0-1 \| null | no | How complete and internally consistent the evidence was. |
| `assessment.confidence_factors` | array of string | no | Evidence factors that raised or lowered confidence. |
| `assessment.criteria_scores` | array of object | no |  |
| `assessment.criteria_scores[].criterion` | string \| null | no |  |
| `assessment.criteria_scores[].block` | string \| null | no |  |
| `assessment.criteria_scores[].scoring_type` | string \| null | no |  |
| `assessment.criteria_scores[].pass` | boolean \| null | no |  |
| `assessment.criteria_scores[].scale_score` | number \| null | no |  |
| `assessment.criteria_scores[].ai_reasoning` | string \| null | no |  |
| `assessment.version` | integer | no |  |
| `assessment.assessed_at` | string (date-time) \| null | no |  |
| `phone_screen_score` | number, 0-100 \| null | no |  |
| `parsed_profile` | ParsedProfile | no |  |
| `notes` | string \| null | no |  |
| `reference_summary` | string \| null | no | AI-consolidated summary across all referee responses. Populated when the final expected referee has submitted their form. |
| `reference_flags` | array of object \| null | no | Structured red flags detected in referee responses. Medium/high severity flags automatically enqueue a reference_red_flag review. |
| `reference_flags[].type` | string (`integrity`, `performance`, `interpersonal`, `other`) | no |  |
| `reference_flags[].referee` | string | no |  |
| `reference_flags[].quote` | string | no |  |
| `reference_flags[].severity` | string (`low`, `medium`, `high`) | no |  |
| `reference_summary_generated_at` | string (date-time) \| null | no |  |
| `expected_reference_count` | integer | no | How many referees Lovelio waits for before generating the reference summary. Defaults to 2 and is overwritten when the candidate submits their contacts. |
| `referee_details_submitted_at` | string (date-time) \| null | no | When the candidate submitted their referee contacts through the Request References form. |
| `applied_at` | string (date-time) | no |  |
| `status_updated_at` | string (date-time) | no |  |

### Meta

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `request_id` | string | no |  |
| `count` | integer | no |  |
| `has_more` | boolean | no |  |
| `next_cursor` | string \| null | no |  |

### Task

The 202 acknowledgement of an async operation. Poll poll_url, which returns a TaskStatus.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `task_id` | string | no |  |
| `status` | string (`queued`, `processing`, `completed`, `failed`) | no |  |
| `resource_id` | string \| null | no |  |
| `estimated_seconds` | integer | no |  |
| `poll_url` | string | no |  |
