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

### Forms

Form templates, phone screen execution, and reference check delivery

- `GET /forms` - List forms
- `POST /forms` - Create form
- `GET /forms/{id}` - Get form with questions
- `PATCH /forms/{id}` - Update form
- `DELETE /forms/{id}` - Delete form
- `POST /forms/{id}/questions` - Add question to form
- `PATCH /forms/{id}/questions/{questionId}` - Update question
- `DELETE /forms/{id}/questions/{questionId}` - Delete question
- `GET /form-instances` - List form instances
- `GET /form-instances/{id}` - Get form instance
- `POST /form-instances/{id}/submit` - Save or complete a form instance
- `POST /form-instances/screening` - Prepare screening instance
- `POST /form-instances/reference` - Create reference instance
- `POST /form-instances/{id}/send` - Send reference form to referee

---

## Forms

Form templates, phone screen execution, and reference check delivery

### GET /forms

**List forms**

Pagination: Returns the complete form-template configuration for one agency. No after cursor is accepted.

Operation ID: `listForms`

Scopes: `forms:read`

**Responses**

| Code | Description | Body |
| --- | --- | --- |
| `200` | List of form templates | { success, data: array of Form, meta } |

### POST /forms

**Create form**

Operation ID: `createForm`

Scopes: `forms: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 |
| --- | --- | --- | --- |
| `name` | string | yes |  |
| `category` | string (`screening`, `assessment`, `interview`, `onboarding`, `compliance`) | yes |  |
| `description` | string | no |  |
| `ai_augmentation_enabled` | boolean | no |  |

**Responses**

| Code | Description | Body |
| --- | --- | --- |
| `201` | Form created | { success, data: Form } |

### GET /forms/{id}

**Get form with questions**

Operation ID: `getForm`

Scopes: `forms:read`

**Path parameters**

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

**Responses**

| Code | Description | Body |
| --- | --- | --- |
| `200` | Form with questions | { success, data: object } |

### PATCH /forms/{id}

**Update form**

Operation ID: `updateForm`

Scopes: `forms:write`

**Path parameters**

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

**Responses**

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

### DELETE /forms/{id}

**Delete form**

Operation ID: `deleteForm`

Scopes: `forms:write`

**Path parameters**

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

**Responses**

| Code | Description | Body |
| --- | --- | --- |
| `200` | Deleted | { success, deleted, id } |

### POST /forms/{id}/questions

**Add question to form**

Operation ID: `addFormQuestion`

Scopes: `forms: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`)

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `question_text` | string | yes |  |
| `question_type` | string (`text`, `rating`, `yes_no`, `multiple_choice`) | no |  |
| `options` | array of string | no |  |
| `scoring_weight` | number | no |  |
| `flag_mode` | string (`red`, `green`, `neutral`) | no |  |
| `required` | boolean | no |  |

**Responses**

| Code | Description | Body |
| --- | --- | --- |
| `201` | Question added | { success, data: FormQuestion } |

### PATCH /forms/{id}/questions/{questionId}

**Update question**

Operation ID: `updateFormQuestion`

Scopes: `forms:write`

**Path parameters**

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

**Responses**

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

### DELETE /forms/{id}/questions/{questionId}

**Delete question**

Operation ID: `deleteFormQuestion`

Scopes: `forms:write`

**Path parameters**

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

**Responses**

| Code | Description | Body |
| --- | --- | --- |
| `200` | Deleted | { success, deleted, id } |

### GET /form-instances

**List form instances**

Returns compact summaries of form instances for discovery and pagination. Use GET /v1/form-instances/{id} for full question and response data.

Operation ID: `listFormInstances`

Scopes: `forms:read`

**Query parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `instance_type` | string (`screening`, `reference`, `interview`) | no |  |
| `status` | string (`draft`, `sent`, `opened`, `started`, `completed`) | no |  |
| `application_id` | string | no |  |
| `referee_id` | string | no |  |
| `form_id` | string | no |  |
| `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` | List of form instances | { success, data: array of FormInstance, meta } |

### GET /form-instances/{id}

**Get form instance**

Returns a form instance with all questions and responses.

Operation ID: `getFormInstance`

Scopes: `forms:read`

**Path parameters**

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

**Responses**

| Code | Description | Body |
| --- | --- | --- |
| `200` | Form instance with questions and responses | { success, data: FormInstance } |

### POST /form-instances/{id}/submit

**Save or complete a form instance**

Writes responses through the product completion core. A screening form is scored, stores application.phone_screen_score and moves yes to screen without letting the score decide the next stage. A phone reference completes through the same reference set and summary path as an emailed response. Pass draft true to autosave a phone reference without completing it.

Operation ID: `submitFormInstance`

Scopes: `forms:write`

**Path parameters**

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

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

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `draft` | boolean, default false | no |  |
| `responses` | array of object | yes |  |
| `responses[].question_id` | string | yes | fiq_ id. |
| `responses[].response_text` | string \| null | no |  |
| `responses[].response_rating` | number \| null | no |  |
| `responses[].response_boolean` | boolean \| null | no |  |
| `responses[].response_choice` | string \| null | no |  |
| `responses[].ai_suggested_score` | number \| null | no |  |
| `responses[].final_score` | number \| null | no |  |
| `responses[].flag` | string \| null | no |  |

**Responses**

| Code | Description | Body |
| --- | --- | --- |
| `200` | Draft saved, reference completed or screening scored. data.kind identifies the result shape. | { success, data: object } |

### POST /form-instances/screening

**Prepare screening instance**

Loads, generates or regenerates the application phone screen through the shared product core. mode is a per-screen choice: ai writes 6-8 candidate-aware questions from the CV, assessment, Client DNA, job focus and brief; template copies the named screening form verbatim. Omit mode only to load an existing instance. Regeneration is refused after any response exists.

Operation ID: `createScreeningInstance`

Scopes: `forms: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 |
| --- | --- | --- | --- |
| `application_id` | string | yes |  |
| `mode` | string (`ai`, `template`) | no |  |
| `template_form_id` | string | no | frm_ id used when mode is template. |
| `regenerate` | boolean, default false | no |  |
| `brief` | string | no | Consultant guidance for AI question generation. |

**Responses**

| Code | Description | Body |
| --- | --- | --- |
| `201` | Screen ready with questions and existing responses. | { success, data: object } |
| `409` | Regeneration refused because answers exist. |  |
| `422` | No mode for a new screen, no usable template, or insufficient candidate information. |  |

### POST /form-instances/reference

**Create reference instance**

Creates a reference check form instance for a referee, ready to send. referee_id is the ref_ id returned by GET or POST /candidates/{id}/referees; Lovelio bridges that candidate referee into the questionnaire store automatically. Legacy ref_ ids from older integrations remain accepted. Unless the account uses its own template, Lovelio writes the questions from the assessment, interviews, client feedback and consultant notes, sized to a 10 to 15 minute call. The response says which source was used and how long the set should take.

Operation ID: `createReferenceInstance`

Scopes: `forms: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 |
| --- | --- | --- | --- |
| `application_id` | string | yes |  |
| `referee_id` | string | yes | ref_ id returned by the candidate referee endpoints. |
| `form_id` | string | no | Optional frm_ template. |
| `channel` | string (`email`, `phone`), default "email" | no |  |
| `mode` | string (`ai`, `template`) | no | Question source. Omit for the product default. |

**Responses**

| Code | Description | Body |
| --- | --- | --- |
| `201` | Reference instance created | { success, data: object } |

### POST /form-instances/{id}/send

**Send reference form to referee**

Generates a magic link and sends the reference form to the referee via email.

Operation ID: `sendFormInstance`

Scopes: `forms: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. |

**Responses**

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

## Schemas

Objects referenced by the operations above.

### Form

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | no |  |
| `name` | string | no |  |
| `category` | string (`screening`, `assessment`, `interview`, `onboarding`, `compliance`) | no |  |
| `description` | string \| null | no |  |
| `ai_augmentation_enabled` | boolean | no |  |
| `is_default` | boolean | no |  |
| `question_count` | integer | no |  |
| `created_at` | string (date-time) | no |  |

### FormInstance

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes |  |
| `form_id` | string | no |  |
| `instance_type` | string (`screening`, `reference`, `interview`) | yes |  |
| `status` | string (`draft`, `sent`, `opened`, `started`, `completed`) | yes |  |
| `job_application_id` | string \| null | no |  |
| `referee_id` | string \| null | no |  |
| `candidate_referee_id` | string \| null | no | The public ref_ record when this is a reference. |
| `interview_id` | string \| null | no |  |
| `channel` | string (`email`, `phone`, null) \| null | no |  |
| `taken_by` | string \| null | no | usr_ id of the consultant taking a phone reference. |
| `call_started_at` | string (date-time) \| null | no |  |
| `call_ended_at` | string (date-time) \| null | no |  |
| `sent_at` | string (date-time) \| null | no |  |
| `opened_at` | string (date-time) \| null | no |  |
| `completed_at` | string (date-time) \| null | no |  |
| `created_at` | string (date-time) | no |  |
| `questions` | array of FormInstanceQuestion | no |  |
| `responses` | array of FormResponse | no |  |

### FormQuestion

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | no |  |
| `order_index` | integer | no |  |
| `question_text` | string | no |  |
| `question_type` | string (`text`, `rating`, `yes_no`, `multiple_choice`) | no |  |
| `options` | array of string \| null | no |  |
| `scoring_weight` | number \| null | no |  |
| `flag_mode` | string (`red`, `green`, `neutral`) \| null | no |  |
| `required` | boolean | no |  |

### Meta

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