# Lovelio API reference

Recruitment automation API. Create jobs, manage candidates, run ad-hoc actions, integrate with your tools.

Generated from the OpenAPI 3.1 document at https://lovelio.ai/api/v1/openapi.json.
API version 7.2.0. 157 paths, 234 operations.

This is the plain-markdown copy of the reference rendered at https://lovelio.ai/docs/api.

## Fetch less than all of this

| URL | What you get |
| --- | --- |
| `https://lovelio.ai/docs/api.md` | Everything: this header plus every operation in full. |
| `https://lovelio.ai/docs/api.md?index=1` | This header plus the endpoint index. No per-operation detail. |
| `https://lovelio.ai/docs/api.md?tag=Jobs` | This header plus one section in full. Comma-separate for several: `?tag=Jobs,Applications`. |
| `https://lovelio.ai/api/v1/openapi.json` | The machine-readable source this file is generated from. |

Tags: System, Accounts, Jobs, Job Ads, Candidates, Candidate Imports, Applications, Interviews, Submissions, Placements, Quotas, Marketplace, Webhooks, Batch, Activities, Documents, Tasks, Outreach, Talent Pools, Review Queue, Forms, Referees, Integrations, Chat Integrations, Analytics, Calendar, Scheduled Emails, Workflow Rules, Email Templates, Stages, Distribution, Clients, Specs, Search, Business Development.

## Base URL

Docs live on lovelio.ai. The API does not. Every workspace belongs to a region and an API key only works on its own region's host. Calling `lovelio.ai/api/v1/...` returns a JSON error naming all three hosts rather than data.

| Region | Base URL |
| --- | --- |
| Production (US (N. California)) | `https://us.lovelio.ai/api/v1` |
| Production (EU (London)) | `https://eu.lovelio.ai/api/v1` |
| Production (ANZ (Sydney)) | `https://anz.lovelio.ai/api/v1` |

A workspace's exact host is shown in Settings > API keys.

## Authentication

```http
Authorization: Bearer sk_live_...
```

API key, sent as Authorization: Bearer <key>. Three key types: sk_live_ (production), sk_test_ (development - same workspace, same data, marked as a test key; use a separate workspace if you need isolated test data), and sk_trial_ (issued at signup, expires 7 days later - swap to a live key from Settings > API keys). Keys are server-generated, shown once at creation, and scoped per permission (or admin for everything). Lovelio Connect apps authenticate with the OAuth access token from the connect flow instead (Authorization: Bearer lc_at_...) - it carries exactly the scopes the agency approved, enforced on every request. Money is gated at FIELD level, not route level: the three scopes placements:financials:read, clients:financials:read and marketplace:financials:read unlock salary, fee, contract rates, expected GP, commission percents, the client fee schedule and split-fee deal amounts. Without them those fields return null and everything else on the record still comes through - a read never 403s for want of a money scope. admin satisfies all three. Requesting any of them puts a Connect app in the elevated review tier. See /docs/agents/build-an-integration.

## Pagination and shared parameters

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

| Name | In | Type | Description |
| --- | --- | --- | --- |
| `limit` | query | integer, 1-100, default 25 | Rows per page. Clamped to the 1-100 range rather than rejected, so asking for 500 returns 100 - read meta.has_more and page with the cursor. Unparseable values fall back to 25. |
| `after` | query | string | Cursor for forward pagination |
| `before` | query | string | Cursor for backward pagination |
| `created_after` | query | string (date-time) | Only rows created at or after this ISO 8601 date or datetime (inclusive). Malformed values are a 422, never silently ignored. |
| `created_before` | query | string (date-time) | Only rows created at or before this ISO 8601 date or datetime (inclusive). Malformed values are a 422, never silently ignored. |

## Rate limit headers

| Header | Type | Description |
| --- | --- | --- |
| `X-RateLimit-Limit` | integer | Requests allowed per minute |
| `X-RateLimit-Remaining` | integer | Requests remaining in window |
| `X-RateLimit-Reset` | integer | Unix timestamp when window resets |

## Guides

- https://lovelio.ai/docs/agents - what an agent can drive end to end
- https://lovelio.ai/docs/agents/top-10-tasks - runnable examples for the ten most common calls
- https://lovelio.ai/docs/agents/build-an-integration - sandbox agency, test API key, live webhook testing
- https://lovelio.ai/docs/agents/error-handling - error envelope, request IDs, retries, idempotency keys
- https://lovelio.ai/docs/agents/webhooks - subscribing and verifying HMAC signatures
- https://lovelio.ai/docs/agents/sdks - single-file TypeScript and Python clients
- https://lovelio.ai/docs/api/changelog - every version, including breaking changes
- https://lovelio.ai/llms-full.txt - all of the above inlined in one fetch

## Endpoint index

Filtered to: Marketplace.

### Marketplace

Cross-agency job and candidate splits: anonymised listings, reveal-on-accept intros, and the deal record both agencies read. Lovelio takes 0% and never touches the money. Needs an active Lovelio plan.

- `GET /marketplace/listings` - Browse marketplace listings
- `POST /marketplace/listings` - Share to the marketplace
- `GET /marketplace/listings/{id}` - Get marketplace listing
- `PATCH /marketplace/listings/{id}` - Manage marketplace listing
- `GET /marketplace/intros` - List marketplace intros
- `POST /marketplace/intros` - Make a marketplace intro
- `GET /marketplace/intros/{id}` - Get marketplace intro
- `POST /marketplace/intros/{id}/respond` - Respond to a marketplace intro
- `GET /marketplace/deals` - List marketplace deals
- `GET /marketplace/deals/{id}` - Get marketplace deal
- `PATCH /marketplace/deals/{id}` - Marketplace deal handshake

---

## Marketplace

Cross-agency job and candidate splits: anonymised listings, reveal-on-accept intros, and the deal record both agencies read. Lovelio takes 0% and never touches the money. Needs an active Lovelio plan.

### GET /marketplace/listings

**Browse marketplace listings**

By default: live anonymised listings from other agencies (the browse surface - pre-scrubbed snapshots only, never source records). Pass mine=true for your own listings at any status. Walk-in and demo tenants cannot browse. Requires marketplace:read. The Marketplace is an opt-in module: every /marketplace route answers 402 PREMIUM_REQUIRED without an active Lovelio plan and 403 MARKETPLACE_DISABLED until an admin turns the module on in Settings > Integrations.

Operation ID: `listMarketplaceListings`

**Query parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `kind` | string (`job`, `candidate`) | no |  |
| `mine` | boolean, default false | no | true = your own listings at any status. |
| `status` | string (`live`, `paused`, `filled`, `expired`, `withdrawn`) | no | Only meaningful with mine=true (browse serves live only). |
| `limit` | integer, max 100, default 25 | no |  |
| `after` | string | no | Cursor from meta.next_cursor |

**Responses**

| Code | Description | Body |
| --- | --- | --- |
| `200` | Paginated listings | { success, data: array of MarketplaceListing, meta } |

### POST /marketplace/listings

**Share to the marketplace**

Share a job or candidate to the marketplace, anonymised. The ONE write path applies regardless of door: real agencies only, the Split Terms gate (pass accept_terms true on your company's first share), deterministic identity scrub, fail-closed moderation, 30-day expiry, one live listing per source record. Candidate listings require candidate_aware true - the candidate must know they are being represented externally. Requires marketplace:write.

Operation ID: `createMarketplaceListing`

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

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `kind` | string (`job`, `candidate`) | yes |  |
| `job_id` | string | no | job_ id. Required for kind=job. |
| `candidate_id` | string | no | cnd_ id. Required for kind=candidate. |
| `headline` | string | yes |  |
| `chips` | array of string | no | Up to 6 fact chips. |
| `pitch` | string | yes |  |
| `location` | string | no |  |
| `salary_min` | number | no |  |
| `salary_max` | number | no |  |
| `currency` | string, default "AUD" | no |  |
| `fee_percent` | number | no | The declared fee basis. Prefills from the client's fee schedule in-app. |
| `owner_share_percent` | integer (`50`, `60`, `70`, `80`), default 50 | no | The split you keep. 50/50 is the industry norm. |
| `candidate_aware` | boolean | no | Candidate listings only - must be true. |
| `accept_terms` | boolean | no | Accept the current Lovelio Split Terms (required once per company, on first share or first intro). |

**Responses**

| Code | Description | Body |
| --- | --- | --- |
| `201` | Listing live on the marketplace | { success, data: MarketplaceListing } |

### GET /marketplace/listings/{id}

**Get marketplace listing**

Your own listing at any status; another agency's only while it is live.

Operation ID: `getMarketplaceListing`

**Path parameters**

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

**Responses**

| Code | Description | Body |
| --- | --- | --- |
| `200` | Listing | { success, data: MarketplaceListing } |
| `404` | Not found |  |

### PATCH /marketplace/listings/{id}

**Manage marketplace listing**

Lifecycle moves on your own listing: pause, resume, withdraw, filled, or renew (extends 30 days and re-attests the listing is still real - the anti-ghost-listing control). Requires marketplace:write.

Operation ID: `updateMarketplaceListing`

**Path parameters**

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

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

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `action` | string (`pause`, `resume`, `withdraw`, `filled`, `renew`) | yes |  |

**Responses**

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

### GET /marketplace/intros

**List marketplace intros**

direction=received (default) lists intros waiting on your listings; direction=sent lists intros your agency made.

Operation ID: `listMarketplaceIntros`

**Query parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `direction` | string (`received`, `sent`), default "received" | no |  |
| `kind` | string (`submission`, `request`) | no |  |
| `status` | string (`pending`, `accepted`, `declined`, `withdrawn`, `expired`) | no |  |
| `limit` | integer, max 100, default 25 | no |  |
| `after` | string | no | Cursor from meta.next_cursor |

**Responses**

| Code | Description | Body |
| --- | --- | --- |
| `200` | Paginated intros | { success, data: array of MarketplaceIntro, meta } |

### POST /marketplace/intros

**Make a marketplace intro**

Submit one of your candidates to a job listing (kind=submission - requires candidate_id, an anonymous profile, and attested_relationship true: you have interviewed them and they agreed to be represented), or request another agency's candidate listing (kind=request). The profile and note are scrubbed and moderated before they cross tenants; the candidate stays anonymous until the owner accepts. Requires marketplace:write.

Operation ID: `createMarketplaceIntro`

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

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `listing_id` | string | yes | mkl_ id. |
| `kind` | string (`submission`, `request`) | yes |  |
| `candidate_id` | string | no | cnd_ id in YOUR tenant (submissions only). |
| `headline` | string | no | Anonymised headline (submissions only). |
| `chips` | array of string | no |  |
| `pitch` | string | no | Anonymised pitch (submissions only). |
| `note` | string | no |  |
| `attested_relationship` | boolean | no | Submissions only - must be true. |
| `accept_terms` | boolean | no | Accept the current Lovelio Split Terms (required once per company). |

**Responses**

| Code | Description | Body |
| --- | --- | --- |
| `201` | Intro pending with the listing owner | { success, data: MarketplaceIntro } |

### GET /marketplace/intros/{id}

**Get marketplace intro**

Visible to the two sides only: the agency that sent it and the agency whose listing it is on.

Operation ID: `getMarketplaceIntro`

**Path parameters**

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

**Responses**

| Code | Description | Body |
| --- | --- | --- |
| `200` | Intro | { success, data: MarketplaceIntro } |
| `404` | Not found |  |

### POST /marketplace/intros/{id}/respond

**Respond to a marketplace intro**

Four moves. check (listing owner, read-only): run the prior-knowledge check - did your agency already know this candidate? - and get the verdict WITHOUT revealing anything; run it before accepting. accept (listing owner): the contract moment - the intro locks, the deal forms with the split and terms version snapshotted, identity reveals on the deal, and on submissions the candidate materialises in your pipeline at the shortlist stage. decline (listing owner): with an optional reason. withdraw (the sending agency): pull a pending intro back. Requires marketplace:write.

Operation ID: `respondToMarketplaceIntro`

**Path parameters**

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

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

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `action` | string (`check`, `accept`, `decline`, `withdraw`) | yes |  |
| `decline_reason` | string | no |  |

**Responses**

| Code | Description | Body |
| --- | --- | --- |
| `200` | check / decline / withdraw result | { success, data: object } |
| `201` | Accepted - the deal is formed | { success, data: object } |
| `402` | Active plan required |  |
| `403` | Not your move (e.g. only the sender withdraws) |  |
| `404` | Not found - unknown id, or your agency is not a party to this intro |  |
| `409` | Already responded - the intro is no longer pending |  |
| `422` | Validation error |  |

### GET /marketplace/deals

**List marketplace deals**

Deals where your agency is a party, rendered from your side (role, your_share_percent, your_share_amount).

Operation ID: `listMarketplaceDeals`

**Query parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `status` | string (`connected`, `in_progress`, `placed`, `dead`) | no |  |
| `role` | string (`owner`, `partner`) | no |  |
| `limit` | integer, max 100, default 25 | no |  |
| `after` | string | no | Cursor from meta.next_cursor |

**Responses**

| Code | Description | Body |
| --- | --- | --- |
| `200` | Paginated deals | { success, data: array of MarketplaceDeal, meta } |

### GET /marketplace/deals/{id}

**Get marketplace deal**

Visible to the two parties only.

Operation ID: `getMarketplaceDeal`

**Path parameters**

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

**Responses**

| Code | Description | Body |
| --- | --- | --- |
| `200` | Deal | { success, data: MarketplaceDeal } |
| `404` | Not found |  |

### PATCH /marketplace/deals/{id}

**Marketplace deal handshake**

Update a deal. add_to_pipeline (with job_id) turns the revealed candidate into a real candidate + application on one of your jobs and links the deal so its status tracks your pipeline - the door for candidate-listing deals, where you pick the job. mark_paid / mark_received is the handshake on a placed deal (idempotent - the first stamp is the record, repeats are no-ops): the placing agency marks the share paid, the receiving agency marks it received. close (with reason) ends an unplaced deal - the reason goes to the other agency and marketplace.deal.closed fires for both sides; a placed deal never closes this way. Facts both sides can see - Lovelio never touches the money itself. Requires marketplace:write.

Operation ID: `updateMarketplaceDeal`

**Path parameters**

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

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

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `action` | string (`add_to_pipeline`, `mark_paid`, `mark_received`, `close`) | yes |  |
| `job_id` | string | no | add_to_pipeline only: the receiving agency's job (job_...) to run the candidate at. |
| `reason` | string | no | close only, required: why the deal is ending - the other agency reads it. |

**Responses**

| Code | Description | Body |
| --- | --- | --- |
| `200` | Updated deal | { success, data: MarketplaceDeal } |
| `402` | Active plan required |  |
| `404` | Not found |  |
| `422` | Validation error |  |

## Schemas

Objects referenced by the operations above.

### MarketplaceDeal

The contract moment of a split: an immutable snapshot written when an intro is accepted (split, fee basis, salary band, terms version, revealed identity, 12-month introduction protection), then the SHARED money record once the placement lands - both agencies read identical numbers here. Lovelio takes no fee, moves no money, and arbitrates no disputes; the deal makes the numbers unambiguous.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | no |  |
| `intro_id` | string \| null | no |  |
| `listing_id` | string \| null | no |  |
| `listing_kind` | string (`job`, `candidate`) | no |  |
| `listing_headline` | string | no |  |
| `status` | string (`connected`, `in_progress`, `placed`, `dead`) | no | Derived from the real pipeline: client-side stages read as in_progress, a placement as placed, a dead application kills an unplaced deal. |
| `role` | string (`owner`, `partner`) | no | Your side of this deal: owner listed it, partner made the intro. |
| `counterpart_agency` | string | no | The other agency on the deal. |
| `owner_share_percent` | integer (`50`, `60`, `70`, `80`) | no |  |
| `your_share_percent` | integer | no |  |
| `fee_percent` | number \| null | no |  |
| `salary_min` | number \| null | no |  |
| `salary_max` | number \| null | no |  |
| `currency` | string \| null | no |  |
| `terms_version` | string | no | The Split Terms version both agencies accepted. |
| `revealed` | object \| null | no | The identity snapshot written once at accept: candidate name and contact, plus both consultants' contacts. |
| `revealed_at` | string (date-time) \| null | no |  |
| `protection_ends_at` | string (date-time) | no | The 12-month introduction protection window. |
| `placement_id` | string \| null | no | The owner's placement record (pla_). Null for the partner - the deal itself carries the shared money snapshot. |
| `placement_status` | string (`pending_start`, `started`, `fell_off`, `completed`, null) \| null | no |  |
| `placed_salary` | number \| null | no | Requires the marketplace:financials:read scope - null without it. |
| `placed_fee_amount` | number \| null | no | Requires the marketplace:financials:read scope - null without it. |
| `placed_currency` | string \| null | no | The placement's real currency - render placed money in this. `currency` stays the listing's asking currency. |
| `partner_share_amount` | number \| null | no | Requires the marketplace:financials:read scope - null without it. |
| `your_share_amount` | number \| null | no | Your side of the placed fee, per the locked split. Requires the marketplace:financials:read scope - null without it. |
| `placed_start_date` | string (date) \| null | no |  |
| `owner_marked_paid_at` | string (date-time) \| null | no | The paid/received handshake: the placing agency's stamp. |
| `partner_marked_received_at` | string (date-time) \| null | no | The paid/received handshake: the receiving agency's stamp. |
| `closed_at` | string (date-time) \| null | no | Set when an unplaced deal was deliberately closed. |
| `closed_by` | string (`you`, `counterpart`, null) \| null | no | Which side closed it, from your point of view. |
| `closed_reason` | string \| null | no |  |
| `created_at` | string (date-time) | no |  |

### MarketplaceIntro

A candidate submitted to a job listing, or a request on a candidate listing. Reveal-on-accept: until the listing owner accepts, the intro carries only the scrubbed anonymous profile - identity exists nowhere the other tenant can read.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | no |  |
| `listing_id` | string \| null | no |  |
| `listing_headline` | string | no |  |
| `kind` | string (`submission`, `request`) | no |  |
| `direction` | string (`received`, `sent`) | no | received = an intro on your listing; sent = an intro your agency made. |
| `status` | string (`pending`, `accepted`, `declined`, `withdrawn`, `expired`) | no |  |
| `from_agency` | object | no |  |
| `from_agency.name` | string | no |  |
| `from_agency.consultant` | string \| null | no |  |
| `candidate_id` | string \| null | no | Your own candidate record behind an intro you SENT (cnd_). Always null on received intros - reveal happens on the deal, not the intro. |
| `anonymous_profile` | object \| null | no |  |
| `anonymous_profile.headline` | string | no |  |
| `anonymous_profile.chips` | array of string | no |  |
| `anonymous_profile.pitch` | string | no |  |
| `note` | string \| null | no |  |
| `fit_summary` | string \| null | no | AI facts-to-check against your listing (received submissions only). Facts, never verdicts. |
| `attested_relationship` | boolean | no | The submitter attested they interviewed the candidate and the candidate agreed to be represented. |
| `decline_reason` | string \| null | no |  |
| `prior_knowledge` | boolean \| null | no | Stamped at accept time: whether the receiving agency already knew the candidate (checked BEFORE the reveal). |
| `responded_at` | string (date-time) \| null | no |  |
| `created_at` | string (date-time) | no |  |

### MarketplaceListing

An anonymised job or candidate on the cross-agency split marketplace. The client or candidate is never identified; the posting agency always is. Browse serves only these pre-scrubbed snapshots - never another tenant's source records. Listings hard-expire after 30 days (renewal re-attests the listing is still real).

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | no |  |
| `kind` | string (`job`, `candidate`) | no |  |
| `status` | string (`live`, `paused`, `filled`, `expired`, `withdrawn`) | no |  |
| `headline` | string | no | Anonymised headline, e.g. "Senior auditor, top-tier firm, Melbourne". |
| `chips` | array of string | no | Up to 6 anonymised fact chips. |
| `pitch` | string | no | The anonymised pitch. Deterministically scrubbed and moderated before it crosses tenants. |
| `location` | string \| null | no |  |
| `agency` | object | no | The posting agency - always visible (anonymity protects clients and candidates, never the trading agencies). |
| `agency.name` | string | no |  |
| `agency.location` | string \| null | no |  |
| `salary_min` | number \| null | no |  |
| `salary_max` | number \| null | no |  |
| `currency` | string \| null | no |  |
| `fee_percent` | number \| null | no | The declared fee basis (percent of first-year salary). |
| `owner_share_percent` | integer (`50`, `60`, `70`, `80`) | no | The split the owner keeps. Lovelio takes 0% - the fee is split between the two agencies, full stop. |
| `partner_share_percent` | integer | no | Always 100 - owner_share_percent. |
| `mine` | boolean | no | True when your agency owns this listing. |
| `source_job_id` | string \| null | no | The job behind your own listing (job_). Always null on other agencies' listings. |
| `source_candidate_id` | string \| null | no | The candidate behind your own listing (cnd_). Always null on other agencies' listings. |
| `expires_at` | string (date-time) | no |  |
| `created_at` | string (date-time) | no |  |
| `updated_at` | string (date-time) \| null | no |  |

### Meta

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