# 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. 180 paths, 264 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, Calls, Placements, Contractor Documents, Quotas, Opportunities, 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, Candidate Evidence, Commands, Clients, Specs, Search.

## 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 14 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). The regional /api/mcp endpoint also accepts these keys through the Authorization header, with the same scopes and record access. Current key status and scopes are rechecked on every request. Lovelio Connect apps authenticate with the OAuth access token from the connect flow instead (Authorization: Bearer lc_at_...; personal MCP clients use mcp_at_...) - it carries exactly the scopes the agency approved, enforced on every request. Personal actions use the current membership role and record access; direct account administration requires an admin. Consultant OAuth scopes also follow the current agency rule. Additional scopes require fresh consent. 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. A key or connection that acts as a person also carries that person's money view: when their user type has "See everyone's billings and GP" switched off (Settings > Permissions), money fields come back null on every record that is not their own (a placement they own or share credit on, a job they own and its placements, their own listings and deals), totals count their own placements only, and a write to money on someone else's placement returns a validation error. A company key with no person behind it sees every figure its scopes allow. A fourth field gate, placements:contracting:read, unlocks the timesheet and payroll hand-off on a contract placement: the client's payment terms and how the contractor is paid (Australia: PAYG or ABN, and the ABN). 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 | meta.next_cursor from the previous page, passed unchanged. Leave it out for the first page. A value Lovelio did not issue is a 422 VALIDATION_ERROR, never page 1 again. |
| `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 one logical mutation, scoped to the account. Completed results expire after 24 hours; uncertain executions retain their safety reservation until reconciled. The key is claimed before side effects. Identical completed retries replay the saved response. Concurrent retries return 409 IDEMPOTENCY_IN_PROGRESS. Changed JSON, form fields, record ids, filenames, content types, or file bytes return 409 IDEMPOTENCY_KEY_REUSED. Random multipart boundaries do not affect matching. |

## 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 |
| `X-Idempotency-Operation-ID` | string (uuid) | Durable operation identity for replay or reconciliation. |

## 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: Opportunities.

### Opportunities

Job leads from watched careers pages, the floats Lovelio drafts for them, and key movements. Agents read and draft; only a person sends a float.

- `GET /opportunities/job-leads` - List job leads
- `GET /opportunities/job-leads/{id}` - Get a job lead
- `GET /opportunities/job-leads/{id}/floats/{adId}` - Get a float draft
- `GET /opportunities/key-movements` - List key movements

---

## Opportunities

Job leads from watched careers pages, the floats Lovelio drafts for them, and key movements. Agents read and draft; only a person sends a float.

### GET /opportunities/job-leads

**List job leads**

The companies one recruiter watches on the Opportunities page, most new jobs first: each careers page Lovelio reads overnight, its open and new jobs, and where its floats stand. Watch lists belong to one recruiter. Requires clients:read.

Pagination: Returns one recruiter's complete watch list, capped at 200 companies. No after cursor is accepted.

Operation ID: `listJobLeads`

Scopes: `clients:read`

**Query parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `user_id` | string | no | Whose watch list (usr_ id). Defaults to the user the key acts as; reading someone else's needs a key that sees the whole agency. |

**Responses**

| Code | Description | Body |
| --- | --- | --- |
| `200` | Watched companies, most new jobs first | { success, data: array of JobLead, meta } |
| `400` | Invalid request. Correct the named field or add the required Idempotency-Key. | Error |
| `401` | AUTHENTICATION_REQUIRED: supply the full key or token on the correct environment and region. The same code covers a credential that is no longer usable: a revoked key and an authorising user whose access an admin has paused. The message says which. | Error |
| `402` | PREMIUM_REQUIRED: the agency has no active plan, so the whole account is closed to people and agents alike until an admin signs in at lovelio.ai and turns it back on. Stop retrying and tell whoever runs the integration. | Error |
| `403` | Access refused. Check the error code and the required permissions. ACCOUNT_SUSPENDED means an admin at Lovelio has made the whole agency dark: nothing will work until that is lifted, and no credential change helps. CONNECTION_PENDING_APPROVAL means a consultant's connection is waiting for an admin at the agency to approve it: the credential is valid, so do not refresh or reconnect; the person gets an email when it is live. | Error |
| `405` | METHOD_NOT_ALLOWED: use one of the methods in the Allow header. | Error |
| `423` | RESTORE_IN_PROGRESS: the agency is being put back to an earlier time, which takes seconds. Nothing was written. Wait for the Retry-After header (60 seconds), then send the same request again. | Error |
| `429` | RATE_LIMIT_EXCEEDED: wait for Retry-After seconds before retrying. DAILY_LIMIT_REACHED: the key has accessed today's allowance of candidates. The allowance is set by the agency (200 a day by default for a key acting for a non-admin, and an agency can raise it, lower it, or choose to be told rather than stop); records already returned today do not count again, and the allowance resets with the person's day. | Error |
| `500` | INTERNAL_ERROR: retry with backoff. An idempotent POST must keep the same key. | Error |

### GET /opportunities/job-leads/{id}

**Get a job lead**

One watched company: the new ads with the people in your database who fit each one, ads outside your patch, and everything else still open. Draft a float for a new ad with draft_careers_float through POST /batch (payload { watch_id, site_job_id }, the cwt_ and cad_ ids here). Requires clients:read.

Operation ID: `getJobLead`

Scopes: `clients:read`

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes | Job lead id, starts cwt_. |

**Query parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `user_id` | string | no | Whose watch list (usr_ id). Defaults to the user the key acts as; reading someone else's needs a key that sees the whole agency. |

**Responses**

| Code | Description | Body |
| --- | --- | --- |
| `200` | The job lead with its ads | { success, data: JobLeadDetail, meta } |
| `400` | Invalid request. Correct the named field or add the required Idempotency-Key. | Error |
| `401` | AUTHENTICATION_REQUIRED: supply the full key or token on the correct environment and region. The same code covers a credential that is no longer usable: a revoked key and an authorising user whose access an admin has paused. The message says which. | Error |
| `402` | PREMIUM_REQUIRED: the agency has no active plan, so the whole account is closed to people and agents alike until an admin signs in at lovelio.ai and turns it back on. Stop retrying and tell whoever runs the integration. | Error |
| `403` | Access refused. Check the error code and the required permissions. ACCOUNT_SUSPENDED means an admin at Lovelio has made the whole agency dark: nothing will work until that is lifted, and no credential change helps. CONNECTION_PENDING_APPROVAL means a consultant's connection is waiting for an admin at the agency to approve it: the credential is valid, so do not refresh or reconnect; the person gets an email when it is live. | Error |
| `404` | Not on this recruiter's watch list | Error |
| `405` | METHOD_NOT_ALLOWED: use one of the methods in the Allow header. | Error |
| `423` | RESTORE_IN_PROGRESS: the agency is being put back to an earlier time, which takes seconds. Nothing was written. Wait for the Retry-After header (60 seconds), then send the same request again. | Error |
| `429` | RATE_LIMIT_EXCEEDED: wait for Retry-After seconds before retrying. DAILY_LIMIT_REACHED: the key has accessed today's allowance of candidates. The allowance is set by the agency (200 a day by default for a key acting for a non-admin, and an agency can raise it, lower it, or choose to be told rather than stop); records already returned today do not count again, and the allowance resets with the person's day. | Error |
| `500` | INTERNAL_ERROR: retry with backoff. An idempotent POST must keep the same key. | Error |

### GET /opportunities/job-leads/{id}/floats/{adId}

**Get a float draft**

The float for one new ad: the people who fit, who is picked (at most five), the anonymous profile for each, who it goes to and the email. Change it with update_careers_float or throw it away with discard_careers_float through POST /batch. Only a person sends a float, from the Opportunities page; once sent it is a spec (GET /specs). Requires clients:read.

Operation ID: `getCareersFloat`

Scopes: `clients:read`

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes | Job lead id, starts cwt_. |
| `adId` | string | yes | Careers ad id, starts cad_. |

**Query parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `user_id` | string | no | Whose watch list (usr_ id). Defaults to the user the key acts as; reading someone else's needs a key that sees the whole agency. |

**Responses**

| Code | Description | Body |
| --- | --- | --- |
| `200` | The float | { success, data: CareersFloat, meta } |
| `400` | Invalid request. Correct the named field or add the required Idempotency-Key. | Error |
| `401` | AUTHENTICATION_REQUIRED: supply the full key or token on the correct environment and region. The same code covers a credential that is no longer usable: a revoked key and an authorising user whose access an admin has paused. The message says which. | Error |
| `402` | PREMIUM_REQUIRED: the agency has no active plan, so the whole account is closed to people and agents alike until an admin signs in at lovelio.ai and turns it back on. Stop retrying and tell whoever runs the integration. | Error |
| `403` | Access refused. Check the error code and the required permissions. ACCOUNT_SUSPENDED means an admin at Lovelio has made the whole agency dark: nothing will work until that is lifted, and no credential change helps. CONNECTION_PENDING_APPROVAL means a consultant's connection is waiting for an admin at the agency to approve it: the credential is valid, so do not refresh or reconnect; the person gets an email when it is live. | Error |
| `404` | No float drafted for that ad yet | Error |
| `405` | METHOD_NOT_ALLOWED: use one of the methods in the Allow header. | Error |
| `423` | RESTORE_IN_PROGRESS: the agency is being put back to an earlier time, which takes seconds. Nothing was written. Wait for the Retry-After header (60 seconds), then send the same request again. | Error |
| `429` | RATE_LIMIT_EXCEEDED: wait for Retry-After seconds before retrying. DAILY_LIMIT_REACHED: the key has accessed today's allowance of candidates. The allowance is set by the agency (200 a day by default for a key acting for a non-admin, and an agency can raise it, lower it, or choose to be told rather than stop); records already returned today do not count again, and the allowance resets with the person's day. | Error |
| `500` | INTERNAL_ERROR: retry with backoff. An idempotent POST must keep the same key. | Error |

### GET /opportunities/key-movements

**List key movements**

Open key movements you may see, newest first: people you know who changed jobs, past placements on the move, clients hiring again. Each row says what happened, why it matters and links to the record. Requires clients:read.

Pagination: Returns the open key movements the caller may see, newest 300 at most; rows clear themselves once handled. No after cursor is accepted.

Operation ID: `listKeyMovements`

Scopes: `clients:read`

**Responses**

| Code | Description | Body |
| --- | --- | --- |
| `200` | Open key movements, newest first | { success, data: array of KeyMovement, meta } |
| `400` | Invalid request. Correct the named field or add the required Idempotency-Key. | Error |
| `401` | AUTHENTICATION_REQUIRED: supply the full key or token on the correct environment and region. The same code covers a credential that is no longer usable: a revoked key and an authorising user whose access an admin has paused. The message says which. | Error |
| `402` | PREMIUM_REQUIRED: the agency has no active plan, so the whole account is closed to people and agents alike until an admin signs in at lovelio.ai and turns it back on. Stop retrying and tell whoever runs the integration. | Error |
| `403` | Access refused. Check the error code and the required permissions. ACCOUNT_SUSPENDED means an admin at Lovelio has made the whole agency dark: nothing will work until that is lifted, and no credential change helps. CONNECTION_PENDING_APPROVAL means a consultant's connection is waiting for an admin at the agency to approve it: the credential is valid, so do not refresh or reconnect; the person gets an email when it is live. | Error |
| `405` | METHOD_NOT_ALLOWED: use one of the methods in the Allow header. | Error |
| `423` | RESTORE_IN_PROGRESS: the agency is being put back to an earlier time, which takes seconds. Nothing was written. Wait for the Retry-After header (60 seconds), then send the same request again. | Error |
| `429` | RATE_LIMIT_EXCEEDED: wait for Retry-After seconds before retrying. DAILY_LIMIT_REACHED: the key has accessed today's allowance of candidates. The allowance is set by the agency (200 a day by default for a key acting for a non-admin, and an agency can raise it, lower it, or choose to be told rather than stop); records already returned today do not count again, and the allowance resets with the person's day. | Error |
| `500` | INTERNAL_ERROR: retry with backoff. An idempotent POST must keep the same key. | Error |

## Schemas

Objects referenced by the operations above.

### CareersFloat

A float drafted for one new careers ad: anonymous profiles of up to five people, an email and who it goes to. Only a person sends it.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | no | Starts cfl_. |
| `job_lead_id` | string | no |  |
| `ad_id` | string | no |  |
| `status` | string (`shortlist`, `ready`, `sent`, `discarded`) | no | shortlist: people still being picked. ready: a draft waiting for a person to check and send. |
| `company` | string | no |  |
| `job_title` | string | no |  |
| `job_location` | string \| null | no |  |
| `is_client` | boolean | no |  |
| `drafted_at` | string (date-time) \| null | no |  |
| `suggestions` | array of object | no |  |
| `suggestions[].candidate_id` | string | no |  |
| `suggestions[].name` | string | no |  |
| `suggestions[].title` | string \| null | no |  |
| `suggestions[].employer` | string \| null | no |  |
| `suggestions[].location` | string \| null | no |  |
| `suggestions[].why` | string | no | Why this person fits the ad. |
| `excluded` | integer | no |  |
| `picked` | array of string | no | cnd_ ids, at most five. |
| `profiles` | object | no | Anonymous profile per picked candidate, keyed by cnd_ id. |
| `contacts` | array of object | no |  |
| `contacts[].id` | string | no |  |
| `contacts[].name` | string | no |  |
| `contacts[].title` | string \| null | no |  |
| `contacts[].email` | string | no |  |
| `contacts_without_email` | integer | no |  |
| `recipient_contact_id` | string \| null | no |  |
| `subject` | string | no |  |
| `greeting` | string | no |  |
| `body` | string | no |  |
| `sent_at` | string (date-time) \| null | no |  |
| `sent_to` | string \| null | no |  |
| `sent_count` | integer \| null | no |  |

### Error

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `success` | boolean (`false`) | yes |  |
| `data` | any | yes | Usually null. Some errors include structured recovery details, such as remaining draft fields. |
| `meta` | Meta | yes |  |
| `error` | object | yes |  |
| `error.code` | string | yes |  |
| `error.message` | string | yes |  |
| `error.field` | string | no |  |
| `error.docs` | string (uri) | yes |  |
| `error.request_id` | string | yes | Same request ID as meta.request_id. |

### JobLead

A company whose careers page one recruiter watches. Lovelio reads it overnight.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | no | Starts cwt_. |
| `company` | string | no |  |
| `domain` | string \| null | no |  |
| `careers_url` | string \| null | no |  |
| `client_id` | string \| null | no | Set when the company is already a client. |
| `added_by` | string | no |  |
| `watched_since` | string (date-time) \| null | no |  |
| `status` | string | no |  |
| `last_read_at` | string (date-time) \| null | no |  |
| `open_jobs` | integer | no |  |
| `new_jobs` | integer | no |  |
| `new_overnight` | integer | no |  |
| `new_job_titles` | array of string | no |  |
| `floats` | object \| null | no |  |
| `floats.best_fit` | integer \| null | no |  |
| `floats.fits_pending_for` | array of string | no | cad_ ids still being matched. |
| `floats.draft_ready` | boolean | no |  |
| `floats.last_sent` | object \| null | no |  |
| `floats.last_sent.who` | string | no | Only on colleague_sent. |
| `floats.last_sent.status` | string | no | Only on fits.float. |
| `floats.last_sent.count` | integer | no |  |
| `floats.last_sent.to` | string \| null | no |  |
| `floats.last_sent.sent_at` | string (date-time) \| null | no |  |
| `floats.colleague_sent` | object \| null | no |  |
| `floats.colleague_sent.who` | string | no | Only on colleague_sent. |
| `floats.colleague_sent.status` | string | no | Only on fits.float. |
| `floats.colleague_sent.count` | integer | no |  |
| `floats.colleague_sent.to` | string \| null | no |  |
| `floats.colleague_sent.sent_at` | string (date-time) \| null | no |  |

### JobLeadDetail

Type: JobLead

### KeyMovement

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | no | Starts kmv_. |
| `kind` | string | no |  |
| `created_at` | string (date-time) \| null | no |  |
| `person` | string | no |  |
| `what_happened` | string | no |  |
| `why_they_matter` | string \| null | no |  |
| `candidate_id` | string \| null | no |  |
| `client_contact_id` | string \| null | no |  |
| `application_id` | string \| null | no |  |
| `job_id` | string \| null | no |  |
| `client_id` | string \| null | no |  |
| `url` | string | no | Opens the record in Lovelio. |

### Meta

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