# 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: Talent Pools.

### Talent Pools

Talent pool membership and candidate rediscovery

- `GET /talent-pools` - List talent pools
- `POST /talent-pools` - Create a talent pool
- `GET /talent-pools/{id}` - Get talent pool
- `PATCH /talent-pools/{id}` - Update talent pool
- `DELETE /talent-pools/{id}` - Delete talent pool
- `GET /talent-pools/{id}/members` - List pool members
- `POST /talent-pools/{id}/members` - Add candidate to pool
- `DELETE /talent-pools/{id}/members/{memberId}` - Remove pool member
- `GET /talent-pools/{id}/requests` - List join requests
- `POST /talent-pools/{id}/requests/{requestId}/approve` - Approve join request
- `POST /talent-pools/{id}/requests/{requestId}/decline` - Decline join request
- `POST /public/talent-pools/{employerSlug}/{poolSlug}/join` - Ask to join a talent pool

---

## Talent Pools

Talent pool membership and candidate rediscovery

### GET /talent-pools

**List talent pools**

Operation ID: `listTalentPools`

Scopes: `talent_pools: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 |

**Responses**

| Code | Description | Body |
| --- | --- | --- |
| `200` | Paginated talent pools | { success, data: array of TalentPool, meta } |

### POST /talent-pools

**Create a talent pool**

Operation ID: `createTalentPool`

Scopes: `talent_pools: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`, required)

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | string | yes |  |
| `slug` | string | no |  |
| `eligibility` | string | no |  |
| `auto_add_enabled` | boolean | no |  |
| `min_job_fit` | integer \| null | no |  |
| `min_hm_fit` | integer \| null | no |  |
| `description` | string | no |  |
| `who_we_hire` | string | no |  |
| `video_url` | string | no |  |
| `is_published` | boolean | no |  |
| `keep_warm_enabled` | boolean | no |  |
| `cover_color` | string (`cyan`, `green`, `lime`, `amber`, `orange`) | no |  |

**Responses**

| Code | Description | Body |
| --- | --- | --- |
| `201` | Talent pool created | { success, data: TalentPool } |

### GET /talent-pools/{id}

**Get talent pool**

Operation ID: `getTalentPool`

Scopes: `talent_pools:read`

**Path parameters**

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

**Responses**

| Code | Description | Body |
| --- | --- | --- |
| `200` | Talent pool | { success, data: TalentPool } |

### PATCH /talent-pools/{id}

**Update talent pool**

Update any pool field including auto-add rules and is_published.

Operation ID: `updateTalentPool`

Scopes: `talent_pools:write`

**Path parameters**

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

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

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | string | no |  |
| `eligibility` | string \| null | no |  |
| `auto_add_enabled` | boolean | no |  |
| `min_job_fit` | integer \| null | no |  |
| `min_hm_fit` | integer \| null | no |  |
| `description` | string \| null | no |  |
| `who_we_hire` | string \| null | no |  |
| `video_url` | string \| null | no |  |
| `is_published` | boolean | no |  |
| `keep_warm_enabled` | boolean | no |  |
| `cover_color` | string (`cyan`, `green`, `lime`, `amber`, `orange`, null) \| null | no |  |

**Responses**

| Code | Description | Body |
| --- | --- | --- |
| `200` | Updated talent pool | { success, data: TalentPool } |

### DELETE /talent-pools/{id}

**Delete talent pool**

Permanently delete the pool. Its members, join requests, shares and scan verdicts go with it; the candidates themselves are never touched.

Operation ID: `deleteTalentPool`

Scopes: `talent_pools:write`

**Path parameters**

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

**Responses**

| Code | Description | Body |
| --- | --- | --- |
| `200` | Deleted | { success, data: object } |

### GET /talent-pools/{id}/members

**List pool members**

Operation ID: `listTalentPoolMembers`

Scopes: `talent_pools:read`

**Path parameters**

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

**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 |
| `q` | string | no | Search within the pool: case-insensitive match on member name, email, job title, or employer. |

**Responses**

| Code | Description | Body |
| --- | --- | --- |
| `200` | Paginated members with candidate details | { success, data: array of object, meta } |

### POST /talent-pools/{id}/members

**Add candidate to pool**

Idempotent: re-adding an existing member returns 200 with already_member=true.

Operation ID: `addTalentPoolMember`

Scopes: `talent_pools:write`

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | 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. |

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

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `candidate_id` | string | yes |  |

**Responses**

| Code | Description | Body |
| --- | --- | --- |
| `200` | Already a member |  |
| `201` | Added to the pool |  |

### DELETE /talent-pools/{id}/members/{memberId}

**Remove pool member**

Operation ID: `removeTalentPoolMember`

Scopes: `talent_pools:write`

**Path parameters**

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

**Responses**

| Code | Description | Body |
| --- | --- | --- |
| `200` | Removed |  |

### GET /talent-pools/{id}/requests

**List join requests**

Operation ID: `listTalentPoolJoinRequests`

Scopes: `talent_pools:read`

**Path parameters**

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

**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 |
| `status` | string (`pending`, `approved`, `declined`) | no |  |

**Responses**

| Code | Description | Body |
| --- | --- | --- |
| `200` | Paginated join requests | { success, data: array of object, meta } |

### POST /talent-pools/{id}/requests/{requestId}/approve

**Approve join request**

Creates or matches the candidate by email, kicks off the resume parse when a CV was attached, and adds them to the pool.

Operation ID: `approveTalentPoolJoinRequest`

Scopes: `talent_pools:write`

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes |  |
| `requestId` | 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 |
| --- | --- | --- |
| `200` | Approved |  |

### POST /talent-pools/{id}/requests/{requestId}/decline

**Decline join request**

Silent - the applicant is not emailed.

Operation ID: `declineTalentPoolJoinRequest`

Scopes: `talent_pools:write`

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes |  |
| `requestId` | 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 |
| --- | --- | --- |
| `200` | Declined |  |

### POST /public/talent-pools/{employerSlug}/{poolSlug}/join

**Ask to join a talent pool**

No authentication required. Stores a PENDING join request only - no candidate record is created until a recruiter approves. Asking again with the same email is a friendly no-op (a fresh 200), so the endpoint never leaks whether someone already asked. Rate limited per IP and pool.

Operation ID: `joinTalentPool`

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `employerSlug` | string | yes |  |
| `poolSlug` | string | yes |  |

**Request body** (`multipart/form-data`, required)

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `full_name` | string | yes |  |
| `email` | string (email) | yes |  |
| `consent` | string (`true`) | yes | Must be the string "true" - the person consents to the agency holding their details. |
| `linkedin_url` | string \| null | no |  |
| `cv` | string (binary) | no | Optional CV file: PDF or Word, 10MB max. |

**Responses**

| Code | Description | Body |
| --- | --- | --- |
| `200` | Request received | { success, data: object, meta } |
| `404` | Unknown employer or pool, or the pool is not published | Error |
| `422` | Missing or invalid field (error.field names it) | Error |
| `429` | Rate limited - read Retry-After | Error |

## Schemas

Objects referenced by the operations above.

### Error

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `success` | boolean | no |  |
| `data` | null | no |  |
| `meta` | Meta | no |  |
| `error` | object | no |  |
| `error.code` | string | no |  |
| `error.message` | string | no |  |
| `error.field` | string | no |  |
| `error.docs` | string (uri) | no |  |

### Meta

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

### TalentPool

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | no |  |
| `name` | string | no |  |
| `slug` | string | no |  |
| `eligibility` | string \| null | no | Plain-English description of who belongs. When auto_add_enabled is true this is the AI matcher input - judged semantically (related skills, adjacent titles, locations), not by keywords. |
| `auto_add_enabled` | boolean | no | When true, every new candidate is AI-matched against eligibility when their profile lands in the bank (and again on assessment), auto-adding on a match. Pools with a score floor only match assessed candidates. |
| `min_job_fit` | integer, 0-100 \| null | no | Optional AI assessment score floor gating the match. |
| `min_hm_fit` | integer, 0-100 \| null | no | Optional Client Fit score floor gating the match (the field name is historical). |
| `description` | string \| null | no |  |
| `who_we_hire` | string \| null | no |  |
| `video_url` | string \| null | no |  |
| `is_published` | boolean | no |  |
| `keep_warm_enabled` | boolean | no | When true, members get a monthly check-in email asking whether they are still looking (one-click answers: interested, tempted with a note, or not interested). Members who answer not interested are never emailed again. |
| `cover_color` | string (`cyan`, `green`, `lime`, `amber`, `orange`, null) \| null | no | Chosen cover-tile colour for the dashboard. Null falls back to a colour derived from the pool name. |
| `created_at` | string (date-time) | no |  |
| `updated_at` | string (date-time) | no |  |
