# 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 7.2.0. 157 paths, 234 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

Every list endpoint takes these. Cursor-based: read `meta.next_cursor` from a response and pass it back as `after`.

| 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 |
| `before` | query | string | Cursor for backward 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. |

## 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 - every version, including breaking changes
- https://lovelio.ai/llms-full.txt - all of the above inlined in one fetch

## Endpoint index

Filtered to: Placements.

### Placements

The record of the win: fee, start date, guarantee lifecycle

- `GET /placements` - List placements
- `POST /placements` - Log placement
- `GET /placements/{id}` - Get placement
- `PATCH /placements/{id}` - Update placement

---

## Placements

The record of the win: fee, start date, guarantee lifecycle

### GET /placements

**List placements**

Operation ID: `listPlacements`

**Query parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `client_id` | string | no | Filter by client (cli_). |
| `candidate_id` | string | no | Filter by candidate (cnd_). |
| `job_id` | string | no | Filter by job (job_). |
| `status` | string (`pending_start`, `started`, `fell_off`, `completed`) | 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 placements | { success, data: array of Placement, meta } |

### POST /placements

**Log placement**

Log a placement: the win. Every placement traces back to a job and a live application. Pass application_id (app_) to log it against the application - it moves to placed automatically. Or pass candidate_id + client_id: the candidate must already be live on exactly one of the client's OPEN jobs - Active or On Hold (pass job_id to disambiguate) - and the placement links to that application; otherwise the request fails validation. A placement on a Closed or Filled job is refused whichever way it is addressed: reopen the job first. Fee percent and guarantee default off the client's fee schedule (banded by salary, minimum fee applied) when omitted; fee.amount is computed at write time. Currency, when omitted, resolves in this order: the client's own fee terms, then the acting user's default_currency, then the agency's standard fee schedule, then the agency's country. A key with no acting user behind it skips the user step. Lovelio never converts between currencies. Requires placements:write.

Operation ID: `createPlacement`

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

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `application_id` | string | no | app_ id. Resolves candidate, client, and job. |
| `candidate_id` | string | no | cnd_ id. Required with client_id when no application_id. The candidate must be live on one of the client's open jobs. |
| `client_id` | string | no | cli_ id. |
| `job_id` | string | no | job_ id. Disambiguates when the candidate is on more than one of the client's jobs. |
| `salary` | number | no | First-year salary. |
| `currency` | string | no | ISO 4217 code. Omit to let it default (see the endpoint description). |
| `fee_type` | string (`percent`, `fixed`), default "percent" | no |  |
| `fee_percent` | number | no | Defaults to the salary's band on the client's fee schedule (minimum fee applied). |
| `fee_amount` | number | no | Fixed fee amount (fee_type fixed). |
| `placement_type` | string (`permanent`, `contract`) | no | Defaults from the job's employment_type: a contract job logs a contract placement. |
| `offer_date` | string (date) | no | When the offer was accepted. Defaults to today. |
| `contract_end_date` | string (date) | no | Contract placements: the initial term's end date. |
| `charge_rate` | number | no | Contract placements: what the client pays, per rate_period. |
| `pay_rate` | number | no | Contract placements: what the contractor gets, per rate_period. |
| `rate_period` | string (`hourly`, `daily`, `weekly`) | no |  |
| `expected_gp` | number | no | Contract placements: expected gross profit over the initial term. Defaults to margin x term off the rates; an explicit value wins. |
| `credits` | array of object | no | The credit split - one line per consultant, percents totalling 100. Defaults to the placement owner at 100%. |
| `credits[].user_id` | string | no |  |
| `credits[].percent` | number | no |  |
| `credits[].role` | string (`candidate_consultant`, `job_owner`, `business_development`, `other`) | no |  |
| `start_date` | string (date) | no |  |
| `guarantee_days` | integer | no | Defaults to the client's guarantee_days. |
| `notes` | string | no |  |

**Responses**

| Code | Description | Body |
| --- | --- | --- |
| `201` | Placement logged | { success, data: Placement } |

### GET /placements/{id}

**Get placement**

Operation ID: `getPlacement`

**Path parameters**

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

**Responses**

| Code | Description | Body |
| --- | --- | --- |
| `200` | Placement details | { success, data: Placement } |

### PATCH /placements/{id}

**Update placement**

Patch fields (salary, fee, start_date, guarantee_days, notes, contract terms), replace the credit split, or move the lifecycle status (pending_start / started / fell_off / completed). Status changes dispatch a placement.status_changed webhook. fee.amount is recomputed when any fee input moves; expected_gp is recomputed when a contract's rates or term move unless set directly. credits replaces the whole split and must total 100.

Operation ID: `updatePlacement`

**Path parameters**

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

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

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `status` | string (`pending_start`, `started`, `fell_off`, `completed`) | no |  |
| `salary` | number | no |  |
| `currency` | string | no |  |
| `fee_type` | string (`percent`, `fixed`) | no |  |
| `fee_percent` | number | no |  |
| `fee_amount` | number | no |  |
| `placement_type` | string (`permanent`, `contract`) | no |  |
| `offer_date` | string (date) | no |  |
| `contract_end_date` | string (date) | no |  |
| `charge_rate` | number | no |  |
| `pay_rate` | number | no |  |
| `rate_period` | string (`hourly`, `daily`, `weekly`) | no |  |
| `expected_gp` | number | no |  |
| `credits` | array of object | no | Replaces the whole split. Percents must total 100. |
| `credits[].user_id` | string | no |  |
| `credits[].percent` | number | no |  |
| `credits[].role` | string (`candidate_consultant`, `job_owner`, `business_development`, `other`) | no |  |
| `start_date` | string (date) | no |  |
| `guarantee_days` | integer | no |  |
| `notes` | string | no |  |

**Responses**

| Code | Description | Body |
| --- | --- | --- |
| `200` | Updated | { success, data: Placement } |

## Schemas

Objects referenced by the operations above.

### Meta

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

### Placement

The agency's win: a candidate placed with a client for a fee. Created against an application (moves it to placed) or logged directly with candidate_id + client_id.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | no |  |
| `candidate_id` | string | no |  |
| `candidate_name` | string \| null | no | The candidate's name, embedded so list consumers never need a lookup per row. |
| `client_id` | string | no |  |
| `client_name` | string \| null | no | The client's name, embedded so list consumers never need a lookup per row. |
| `job_id` | string \| null | no |  |
| `job_title` | string \| null | no |  |
| `application_id` | string \| null | no |  |
| `salary` | number \| null | no | First-year salary. Requires the placements:financials:read scope - null without it. |
| `currency` | string \| null | no | ISO 4217 code the placement was billed in. Fixed at write time and never re-derived - changing an agency or user default does not relabel existing placements. |
| `fee` | object | no | The placement fee. amount is the single billing figure: for percent fees it is salary * percent / 100, computed at write time; for fixed fees it is the amount as entered. The whole object is null without the placements:financials:read scope. |
| `fee.type` | string (`percent`, `fixed`) | no |  |
| `fee.percent` | number \| null | no |  |
| `fee.amount` | number \| null | no |  |
| `placement_type` | string (`permanent`, `contract`) | no | Perm or contract. Defaults from the job's employment_type on create. A perm placement's value is its fee amount; a contract placement's value is its expected GP. |
| `offer_date` | string (date) \| null | no | When the offer was accepted. Defaults to the day the placement is logged. |
| `contract` | object \| null | no | Contract terms - present only when placement_type is contract. expected_gp defaults to margin (charge_rate - pay_rate) times the working units between start_date and end_date, and can be set directly; an explicit value always wins. |
| `contract.end_date` | string (date) \| null | no |  |
| `contract.charge_rate` | number \| null | no | Requires the placements:financials:read scope - null without it. |
| `contract.pay_rate` | number \| null | no | Requires the placements:financials:read scope - null without it. |
| `contract.rate_period` | string (`hourly`, `daily`, `weekly`) \| null | no |  |
| `contract.expected_gp` | number \| null | no | Expected gross profit over the initial term, in the placement's currency. Requires the placements:financials:read scope - null without it. |
| `credits` | array of object | no | The credit split: who this win counts for, at what percent. Always totals 100. Defaults to the placement owner at 100%. user_id is a workspace user id. WHO is credited always comes through; `percent` is null without the placements:financials:read scope. |
| `credits[].user_id` | string | no |  |
| `credits[].role` | string (`candidate_consultant`, `job_owner`, `business_development`, `other`) \| null | no |  |
| `credits[].percent` | number | no |  |
| `start_date` | string (date) \| null | no |  |
| `guarantee_days` | integer \| null | no | Guarantee period in days from start_date. Defaults from the client's fee schedule when omitted on create. |
| `status` | string (`pending_start`, `started`, `fell_off`, `completed`) | no | Lifecycle: pending_start until the candidate starts, started while inside (or past) the guarantee, fell_off when they leave during the guarantee, completed when the guarantee is cleared. |
| `notes` | string \| null | no |  |
| `created_at` | string (date-time) | no |  |
| `updated_at` | string (date-time) | no |  |
