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

### Calls

Recorded calls: the transcript line by line, who spoke, the write-up and the recording

- `GET /calls` - List calls
- `POST /calls` - Book a call
- `GET /calls/{id}` - Get call
- `DELETE /calls/{id}` - Delete call (they said no)
- `POST /calls/uploads` - Upload a call recording
- `POST /calls/{id}/upload-parts` - Send a call recording in parts
- `POST /calls/{id}/upload-complete` - Finish a call upload
- `POST /calls/{id}/link` - Link a phone call to a person
- `POST /calls/{id}/speakers` - Name a speaker

---

## Calls

Recorded calls: the transcript line by line, who spoke, the write-up and the recording

### GET /calls

**List calls**

Recorded calls, newest first. Requires calls:read. Each page is filtered to what the key can see after it is fetched, so a page can hold fewer than limit rows while has_more is true; keep following next_cursor.

Operation ID: `listCalls`

Scopes: `calls: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 | 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. |
| `candidate_id` | string | no | Filter by candidate (cnd_). |
| `client_contact_id` | string | no | Filter by client contact (cct_). |
| `job_id` | string | no | Filter by job (job_). |
| `application_id` | string | no | Filter by application (app_). |
| `interview_id` | string | no | Filter by interview (ivw_). |
| `client_id` | string | no | Filter by client (cli_). |
| `owner_id` | string | no | Filter by the consultant who owns the call (usr_). |
| `unlinked` | boolean | no | true: only phone calls that matched nobody by number and have no candidate or contact yet. Link each with POST /calls/{id}/link. |
| `recorded` | boolean | no | true: only calls with a recording or transcript, leaving out calls a consultant logged by hand. |
| `platform` | string | no | Comma-separated platforms: zoom, google_meet, microsoft_teams, lovelio, phone, in_person, other, aircall. Example: phone,aircall. |
| `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 calls | { success, data: array of Call, 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 |

### POST /calls

**Book a call**

Book a call with a candidate or client contact, or start one now. Requires calls:write and a key or connection that acts as a user: the call goes on that user's Google or Outlook calendar with a Meet or Teams link, and is recorded by the agency's call settings unless record says otherwise. Leave starts_at out to start now. Answers with the calendar event; the recorded call appears on GET /calls once it is booked. Supports Idempotency-Key.

Operation ID: `bookCall`

Scopes: `calls:write`

**Header parameters**

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

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

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `candidate_id` | string | no | Candidate (cnd_). One of candidate_id, client_contact_id, application_id or calendar_event_id is required. |
| `client_contact_id` | string | no | Client contact (cct_). |
| `application_id` | string | no | Application (app_). Books the call with its candidate. |
| `job_id` | string | no | Job (job_) the call is about. |
| `starts_at` | string (date-time) | no | Leave out to start now. |
| `duration_minutes` | integer, 5-240, default 30 | no |  |
| `title` | string | no | Defaults to "Call with <name>". |
| `notes` | string | no |  |
| `platform` | string (`google_meet`, `microsoft_teams`, `zoom`) | no | Which link to make. zoom needs the user to have connected Zoom. Falls back to one the calendar can make. |
| `record` | boolean | no | Record the call. Leave it out to follow the call settings. |
| `tell_them` | boolean | no | Say in the invite that the call is recorded. Leave it out to follow the call settings. |
| `call_type` | string (`candidate_screen`, `client_briefing`, `reference`, `interview_debrief`, `business_development`) | no |  |
| `ignore_conflicts` | boolean | no | Book even when the time clashes with another event. |

**Responses**

| Code | Description | Body |
| --- | --- | --- |
| `201` | Call booked | { success, data: CalendarEvent } |
| `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 |
| `409` | No calendar connected, or the time clashes Idempotency errors: IDEMPOTENCY_IN_PROGRESS means retry the identical request with the same key after Retry-After. IDEMPOTENCY_KEY_REUSED needs a fresh key for a distinct request. IDEMPOTENCY_OUTCOME_UNKNOWN must be reconciled before any new key is used. | Error |
| `422` | Validation error | 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 |
| `503` | Idempotency storage error: retry only the identical request with the same key. | Error |

### GET /calls/{id}

**Get call**

One call with its people, every line of the transcript, the write-up and a one-hour link to the recording. Requires calls:read.

Operation ID: `getCall`

Scopes: `calls:read`

**Path parameters**

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

**Responses**

| Code | Description | Body |
| --- | --- | --- |
| `200` | The call | { success, data: CallDetail } |
| `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` | Call not found | 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 |

### DELETE /calls/{id}

**Delete call (they said no)**

The other person did not agree to the recording. Deletes the call with its recording, transcript, write-up, next steps, clips and timeline entry, and the same source can never build it again: a late transcript, phone system retry or app upload for it is refused. A meeting notetaker still in the meeting leaves first. No undo. Requires calls:write; a key that acts as a user must be the consultant on the call, a team leader or an admin.

Operation ID: `discardCall`

Scopes: `calls:write`

**Path parameters**

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

**Responses**

| Code | Description | Body |
| --- | --- | --- |
| `200` | Deleted | { success, data: object } |
| `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` | Not the consultant on the call, a team leader or an admin | Error |
| `404` | Call not found | 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 |

### POST /calls/uploads

**Upload a call recording**

Step one of two. Puts a recorded call on one person and answers with where to send the file: PUT the file to upload.url with upload.headers before upload.expires_at, then POST /calls/{id}/upload-complete. MP4, MOV, WebM, M4A, AAC, MP3, WAV, OGG or FLAC, up to 2 GB. With parts true the file goes in parts while it is still being recorded: upload is null, parts.part_bytes is how long every part but the last must be, and POST /calls/{id}/upload-parts gives the URLs. Refused when the agency has call recording switched off. client_ref is your own id for the recording: a retry with the same client_ref answers with the same call, and upload is null once its file is in. An upload never completed is deleted after a day. Requires calls:write. Supports Idempotency-Key.

Operation ID: `startCallUpload`

Scopes: `calls:write`

**Header parameters**

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

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

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `candidate_id` | string | no | Candidate (cnd_). One of candidate_id, client_contact_id or application_id is required. |
| `client_contact_id` | string | no | Client contact (cct_). |
| `application_id` | string | no | Application (app_). Puts the call on its candidate and job. |
| `job_id` | string | no | Job (job_) the call is about. |
| `calendar_event_id` | string | no | A meeting (cal_) in your own diary. Puts the call on whoever is on it. Needs a key that acts as a user. |
| `parts` | boolean | no | true: send the file in parts through POST /calls/{id}/upload-parts instead of one PUT. |
| `content_type` | string | yes | The file's MIME type. The file itself is checked at upload-complete. |
| `client_ref` | string | no | Your own id for this recording. |
| `title` | string | no |  |
| `started_at` | string (date-time) | no | When the call happened. |
| `duration_seconds` | integer, min 1 | no |  |
| `platform` | string (`zoom`, `google_meet`, `microsoft_teams`, `phone`, `in_person`, `other`) | no | Where the call happened. Defaults to other. |
| `call_type` | string (`candidate_screen`, `client_briefing`, `reference`, `interview_debrief`, `business_development`) | no | Shapes the write-up. Defaults to candidate_screen for a candidate, client_briefing for a contact. |

**Responses**

| Code | Description | Body |
| --- | --- | --- |
| `201` | Ready for the file | { success, data: object } |
| `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` | Candidate, contact, application or job not found | Error |
| `405` | METHOD_NOT_ALLOWED: use one of the methods in the Allow header. | Error |
| `409` | Idempotency errors: IDEMPOTENCY_IN_PROGRESS means retry the identical request with the same key after Retry-After. IDEMPOTENCY_KEY_REUSED needs a fresh key for a distinct request. IDEMPOTENCY_OUTCOME_UNKNOWN must be reconciled before any new key is used. | Error |
| `422` | Validation error, a file type Lovelio cannot read, recording switched off, or a call deleted because they said no | 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 |
| `503` | Idempotency storage error: retry only the identical request with the same key. | Error |

### POST /calls/{id}/upload-parts

**Send a call recording in parts**

Signed URLs for parts of a recording started with parts true, so it can be sent while it is still being recorded. parts is the list of part numbers, from 1, at most 100 a time. PUT each part's bytes to its url with its headers before expires_at; sending a part again replaces it. Every part but the last is exactly part_bytes long. When all are sent, POST /calls/{id}/upload-complete with parts and bytes. stored true means the recording is already in and there is nothing to send. Requires calls:write. Same action as sign_call_upload_parts in the batch door.

Operation ID: `signCallUploadParts`

Scopes: `calls:write`

**Path parameters**

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

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

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `parts` | array of integer, min 1 | yes |  |

**Responses**

| Code | Description | Body |
| --- | --- | --- |
| `200` | A URL for each part | { success, data: object } |
| `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` | Call not found | Error |
| `405` | METHOD_NOT_ALLOWED: use one of the methods in the Allow header. | Error |
| `422` | Part numbers outside 1 to the most a recording can have, or more than 100 at once | 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 |

### POST /calls/{id}/upload-complete

**Finish a call upload**

Step two of two, after the file is PUT to upload.url. Lovelio reads the file: one that is not a recording, or is over 2 GB, is deleted and refused. A real one is transcribed in the background, with the write-up and next steps after it; GET /calls/{id} shows status ready when they are done. Calling it again once the file is in is safe. Sent in parts, pass parts and bytes: a part that is missing or the wrong length answers 409 with data.missing, the part numbers to send again. marks are moments to keep as clips. Requires calls:write.

Operation ID: `finishCallUpload`

Scopes: `calls:write`

**Path parameters**

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

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

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `parts` | integer, min 1 | no | Sent in parts: how many. |
| `bytes` | integer, min 1 | no | Sent in parts: the whole recording's length in bytes. |
| `marks` | array of object | no | Moments to keep as clips once the transcript is in. |
| `marks[].at_ms` | integer, min 0 | yes | Milliseconds from the start of the recording. |

**Responses**

| Code | Description | Body |
| --- | --- | --- |
| `202` | Accepted | { success, data: object } |
| `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` | Call not found | Error |
| `405` | METHOD_NOT_ALLOWED: use one of the methods in the Allow header. | Error |
| `409` | The file has not finished uploading, or parts are missing (data.missing) | Error |
| `422` | The file is not a recording, or is over 2 GB | 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 |

### POST /calls/{id}/link

**Link a phone call to a person**

Link a phone call that matched nobody by number (or matched two people) to the candidate or client contact on it. Requires calls:write. Pass exactly one of candidate_id or client_contact_id; a contact links their client too. Only a ready phone call with no candidate and no contact can be linked, and a linked call never moves. Linking names the other voice, writes the call on their timeline and writes up the call. Find these calls with GET /calls?unlinked=true. Same action as link_call_to_person in the batch door.

Operation ID: `linkCallToPerson`

Scopes: `calls:write`

**Path parameters**

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

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

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `candidate_id` | string | no | Candidate (cnd_). Pass this or client_contact_id. |
| `client_contact_id` | string | no | Client contact (cct_). Pass this or candidate_id. |
| `save_number` | boolean | no | true: also save the number the call came from onto the person. Only when they have no number yet; an existing number is never replaced. |

**Responses**

| Code | Description | Body |
| --- | --- | --- |
| `200` | The linked call. number_saved true when save_number put the number onto the person. | { success, data: Call } |
| `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` | Call, candidate or contact not found | Error |
| `405` | METHOD_NOT_ALLOWED: use one of the methods in the Allow header. | Error |
| `409` | The call is already linked, or is still being processed | Error |
| `422` | Not a phone call, or not exactly one of candidate_id and client_contact_id | 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 |

### POST /calls/{id}/speakers

**Name a speaker**

Say who a voice on the transcript is ("Speaker 1 is Sam"). Requires calls:write. participant_id is the speaker from GET /calls/{id}. Pass exactly one of candidate_id (the call's own candidate), client_contact_id (a contact at the call's client), user_id (a consultant on your team) or name (anyone else), or reset true to put back the name the recording gave. The same person may be given to more than one voice. The transcript, talk time, clips, quotes and search follow at once; a write-up nobody has edited is written again about a minute later, and an edited one is left as it is. Same action as name_call_speaker in the batch door.

Operation ID: `nameCallSpeaker`

Scopes: `calls:write`

**Path parameters**

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

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

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `participant_id` | string | yes | The speaker (cpt_), from participants on GET /calls/{id}. |
| `candidate_id` | string | no | The call's candidate (cnd_). |
| `client_contact_id` | string | no | A contact at the call's client (cct_). |
| `user_id` | string | no | A consultant on your team (usr_). |
| `name` | string | no | Anyone else, by name. |
| `reset` | boolean | no | true: put back the name the recording gave. |

**Responses**

| Code | Description | Body |
| --- | --- | --- |
| `200` | The call with its people and transcript, as GET /calls/{id}. | { success, data: CallDetail } |
| `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` | Call or speaker not found | Error |
| `405` | METHOD_NOT_ALLOWED: use one of the methods in the Allow header. | Error |
| `409` | The call has no transcript yet | Error |
| `422` | Not exactly one of candidate_id, client_contact_id, user_id, name or reset, or that person is not on this call | 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.

### CalendarEvent

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | no |  |
| `title` | string | no |  |
| `notes` | string \| null | no |  |
| `event_type` | string (`interview`, `phone_screen`, `reminder`, `deadline`, `meeting`) | no |  |
| `starts_at` | string (date-time) | no |  |
| `ends_at` | string (date-time) \| null | no |  |
| `all_day` | boolean | no |  |
| `application_id` | string \| null | no |  |
| `job_id` | string \| null | no |  |
| `candidate_id` | string \| null | no |  |
| `assigned_to` | string \| null | no | User ID the event is assigned to. |
| `created_by` | string \| null | no |  |
| `created_at` | string (date-time) | no |  |
| `updated_at` | string (date-time) | no |  |
| `meeting_url` | string \| null | no | The Google Meet or Microsoft Teams link, once the calendar has made it. |
| `video_platform` | string (`google_meet`, `microsoft_teams`, `zoom`, null) \| null | no |  |
| `record` | boolean \| null | no | The event's Record switch. Null follows the owner's call settings. |

### Call

One recorded call: a meeting the notetaker sat in, a phone call (Aircall), a recording someone uploaded or an AI voice screen. Linked to the candidate, job, application, interview, client and contact it was about.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | no |  |
| `source` | string (`notetaker`, `video_room`, `phone`, `upload`, `ai_screen`, `logged`, `desktop`) \| null | no | Where the call came from. upload is a recording uploaded on the web or through POST /calls/uploads. ai_screen is an older AI voice screen (retired). logged is a call a consultant wrote down: no recording, no transcript. desktop is recorded by Lovelio for desktop on the consultant's computer. Null for a source this API version does not know yet. |
| `platform` | string (`zoom`, `google_meet`, `microsoft_teams`, `lovelio`, `phone`, `aircall`, `in_person`, `other`) \| null | no | aircall is a phone call through Aircall (source phone). Null for a platform this API version does not know yet. |
| `call_type` | string \| null | no | What the call was for: candidate_screen, client_briefing, reference, interview_debrief, business_development, interview or client_interview. Shapes the write-up. Null when not set. |
| `title` | string \| null | no |  |
| `status` | string (`scheduled`, `live`, `processing`, `ready`, `failed`, `cancelled`) \| null | no | ready once the transcript is in. |
| `started_at` | string (date-time) \| null | no |  |
| `ended_at` | string (date-time) \| null | no |  |
| `duration_seconds` | integer \| null | no |  |
| `language` | string \| null | no |  |
| `owner_id` | string \| null | no | The consultant who owns the call (usr_). |
| `candidate_id` | string \| null | no |  |
| `candidate_name` | string \| null | no |  |
| `candidate_deleted` | boolean | no | True when the record was deleted: its id stays, and its name and contact details read null. |
| `job_id` | string \| null | no |  |
| `job_title` | string \| null | no |  |
| `application_id` | string \| null | no |  |
| `interview_id` | string \| null | no |  |
| `client_id` | string \| null | no |  |
| `client_contact_id` | string \| null | no |  |
| `recording` | object | no |  |
| `recording.status` | string (`none`, `copying`, `stored`, `failed`, `deleted`) | no | stored while the recording is kept; deleted after 24 months. |
| `recording.mime_type` | string \| null | no |  |
| `recording.delete_after` | string (date-time) \| null | no |  |
| `created_at` | string (date-time) | no |  |
| `updated_at` | string (date-time) | no |  |

### CallDetail

Type: Call

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

### Meta

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