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

### Jobs

Job requisition management with AI enrichment

- `GET /jobs` - List jobs
- `POST /jobs` - Create job (async)
- `POST /jobs/from-description` - Draft a job from a natural-language description (streaming)
- `POST /jobs/from-description/{review_token}/confirm` - Confirm a staged draft and create the job (async)
- `GET /jobs/from-description/{review_token}` - Get the current state of a staged job draft
- `PATCH /jobs/from-description/{review_token}` - Update a staged job draft (fill gaps)
- `POST /jobs/from-description/{review_token}/revise` - Apply a free-text revision to a staged draft
- `GET /jobs/{id}/ad` - Get the latest job ad
- `PATCH /jobs/{id}/ad` - Update a job ad
- `POST /jobs/{id}/ad/rewrite` - AI-rewrite a job ad
- `GET /jobs/{id}/social-draft` - Get a job's LinkedIn + X post drafts
- `PATCH /jobs/{id}/social-draft` - Update a job's social post text
- `GET /jobs/{id}` - Get job
- `PATCH /jobs/{id}` - Update job
- `DELETE /jobs/{id}` - Delete job
- `POST /jobs/{id}/close` - Close job
- `POST /jobs/{id}/reopen` - Reopen job
- `GET /jobs/{id}/summary` - AI job summary
- `GET /jobs/{id}/ranking` - Stack-ranked interviewed candidates for a job
- `GET /jobs/{id}/share-bundle` - Get share-ready content bundle for a job

---

## Jobs

Job requisition management with AI enrichment

### GET /jobs

**List jobs**

Operation ID: `listJobs`

**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 (`active`, `on_hold`, `closed`, `filled`) | no |  |
| `client_id` | string | no | Filter to jobs for one client (cli_ id). |
| `team_id` | string | no |  |
| `title` | string | no | Case-insensitive substring match on the job title. |
| `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 list of jobs | { success, data: array of Job, meta } |

### POST /jobs

**Create job (async)**

Creates a job and triggers AI enrichment (description, criteria, interview questions). Returns 202 with a task ID to poll.

Operation ID: `createJob`

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

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `title` | string | yes |  |
| `mode` | string (`draft`, `active`) | no | Create as draft or active. Default active. |
| `source` | string | no | Provenance label stored on the create task. Defaults to "api". |
| `source_context` | string \| null | no | Free-text context stored alongside source. |
| `compensation_research` | boolean, default false | no | When true, the create task also runs compensation research for the role. |
| `client_id` | string | yes | Required. The client this job is for (cli_ id). Every job belongs to a client - for the agency's own internal hiring, pass the agency self-client id. Omitting it returns 422. |
| `client_contact_id` | string | no | Contact at the client (cct_ id). Must belong to client_id. |
| `employment_type` | string (`full_time`, `part_time`, `contract`) | no | Basis of the role. Omit to leave unset. Any other value returns 422. |
| `compensation` | object | no | Salary band for the role. |
| `compensation.base_min` | number \| null | no |  |
| `compensation.base_max` | number \| null | no |  |
| `compensation.currency` | string | no | ISO 4217 code. When omitted it is derived, in order, from the country in `location`, the client company's own location, then the agency's default currency. |
| `compensation.period` | string (`year`, `day`, `hour`) | no | How base_min/base_max are quoted. Omit and the work type decides: employment_type contract gives hour, anything else year. Day rates are never inferred - send day to get one. |
| `success_in_90_days` | string \| null | no | What great looks like 90 days in. Feeds AI enrichment of the job criteria. |
| `location` | object | no | Structured location. `type` is one of `on_site`, `hybrid`, `remote`. For remote roles, city/country/country_code are ignored. |
| `location.type` | string (`on_site`, `hybrid`, `remote`) | no |  |
| `location.city` | string \| null | no |  |
| `location.state` | string \| null | no |  |
| `location.country` | string \| null | no |  |
| `location.country_code` | string \| null | no | ISO 3166-1 alpha-2 |
| `team_id` | string | no |  |
| `hiring_manager_id` | string | no |  |
| `default_interview_format` | string (`video`, `phone`, `in_person`, `technical`), default "video" | no | Default interview format used when auto-booking interviews on stage transition. |
| `is_evergreen` | boolean, default false | no | Create as an always-open pipeline job, excluded from performance metrics. |

**Responses**

| Code | Description | Body |
| --- | --- | --- |
| `202` | Job creation queued. Returns a Task: `resource_id` is the created job's id (job_), `task_id` is the create_job task - poll `poll_url` for enrichment progress. | { success, data: Task } |

### POST /jobs/from-description

**Draft a job from a natural-language description (streaming)**

Takes a sentence or two describing a role ("senior backend engineer in London, 120k, lead the payments rewrite") and streams a structured JobDraft back via Server-Sent Events. Pass client_id to write the draft in that client's voice, grounded in the client profile and Client DNA; the client is never named or otherwise identified in the copy (agencies keep their client list confidential). The output is funnelled through the shared anti-AI-tics post-processor so the copy does not read like generic AI output. The response Content-Type is text/event-stream; each event is `data: <json>\n\n` where <json> is one of `{type:"meta",review_token,draft_id}`, `{type:"field_start",field}`, `{type:"token",field,text}`, `{type:"field_end",field,value}`, `{type:"complete",draft,review_token,lint:{offenders}}`, or `{type:"error",message}`. The stream terminates with `data: [DONE]\n\n`. This endpoint stages a draft only - call POST /jobs/from-description/{review_token}/confirm to promote it to a real job (which enqueues the same asynchronous create_job task as POST /jobs).

Operation ID: `draftJobFromDescription`

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

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `description` | string | yes | Natural-language description of the role. |
| `autopublish` | boolean, default false | no | If true, confirming the draft publishes the job ad to the careers page immediately. Defaults to false: the job ad is still generated, but stays in Draft status until published. |
| `source` | string (`dashboard`, `slack`, `mcp`, `api`), default "api" | no | Caller self-identification for attribution. Does not affect behaviour. |
| `client_id` | string | no | The client this job is for (cli_ id). The draft is written in the client's voice using the client profile and Client DNA, and the client is stored on the draft so /confirm does not need to pass it again. The client is never named in the generated copy. Every job needs a client by confirm time - supply it here (recommended) or via PATCH before confirming. |

**Responses**

| Code | Description | Body |
| --- | --- | --- |
| `200` | SSE stream of DraftEvent messages. The final `complete` event carries the full draft and a review_token to pass to /confirm. | string |
| `422` | Missing or invalid description, or client_id does not match a client in this workspace. |  |

### POST /jobs/from-description/{review_token}/confirm

**Confirm a staged draft and create the job (async)**

Promotes a draft staged by POST /jobs/from-description into a real job. Enqueues the same create_job task runner that POST /jobs uses, so assessment criteria, interview questions, and job ad generation happen asynchronously. Returns 202 Accepted with a task_id to poll. The caller may override the draft (for example after editing fields in a dashboard or Slack modal) by passing `draft` in the body; omit to use the streamed draft verbatim. Every job belongs to a client: supply client_id when staging the draft (POST /jobs/from-description) or via PATCH /jobs/from-description/{review_token} before confirming.

Operation ID: `confirmJobDraft`

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `review_token` | string | yes | The review_token emitted in the `meta` and `complete` SSE events from the draft endpoint. |

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

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `draft` | object | no | Optional edited draft to persist instead of the one originally streamed. |
| `draft.title` | string | no |  |
| `draft.summary` | string | no |  |
| `draft.responsibilities` | array of string | no |  |
| `draft.requirements` | array of string | no |  |
| `draft.nice_to_haves` | array of string | no |  |
| `draft.benefits` | array of string | no | Candidate-facing benefits (medical, leave, perks). Drawn only from the recruiter's description or the client's profile - never the agency's own benefits. |
| `draft.location` | string \| null | no | Denormalised display string ("Sydney, Australia" or "Remote"). Structured siblings on the draft object are location_type, location_city, location_country, location_country_code. |
| `draft.location_type` | string (`In Office`, `Hybrid`, `Remote`) \| null | no |  |
| `draft.location_city` | string \| null | no |  |
| `draft.location_country` | string \| null | no |  |
| `draft.location_country_code` | string \| null | no | ISO 3166-1 alpha-2 |
| `draft.compensation` | object \| null | no |  |
| `draft.compensation.base_min` | number \| null | no |  |
| `draft.compensation.base_max` | number \| null | no |  |
| `draft.compensation.currency` | string | no |  |
| `draft.compensation.period` | string (`Per Year`, `Per Day`, `Per Hour`) | no | How the figures are quoted on the draft. Omitted means the work type decides at confirm. |
| `draft.employment_type` | string (`full_time`, `part_time`, `contract`) \| null | no |  |
| `mode` | string (`draft`, `active`) | no | Create as draft or active. Defaults to the autopublish flag provided at draft time. Both modes generate a job ad; active publishes it to the careers page, draft leaves it in Draft status. |
| `team_id` | string | no | Optional team ID to attach (tem_...). |
| `hiring_manager_id` | string | no | Optional hiring manager user ID (usr_...). |

**Responses**

| Code | Description | Body |
| --- | --- | --- |
| `202` | Job creation queued. task_id returned in data. | { success, data: Task } |
| `409` | Draft has gaps - the client is missing (the only mandatory field since 4.1.0; team, location, compensation, and employment type are optional and editable on the created job). Standard error envelope: success false, error `{ code: "draft_has_gaps", message }`, and data `{ code, gaps: string[], review_token }` for machines. Set client_id via PATCH /jobs/from-description/{review_token} (updates.client_id), or pass client_id in this confirm body, and retry. There is no `force` bypass. |  |
| `422` | Draft missing, expired, or already confirmed. |  |

### GET /jobs/from-description/{review_token}

**Get the current state of a staged job draft**

Poll the streaming job draft while the describe-a-job task is running, or after it completes to inspect the final draft and any remaining gaps. Returns the draft fields produced so far, structured selections (team_id / hiring_manager_id), skipped gaps, a suggested team, the lint offenders list, and the task_status (`queued` | `processing` | `completed` | `failed`). Consumers typically poll until `task_status === "completed"` and `gaps` is empty before calling confirm.

Operation ID: `getJobDraft`

**Path parameters**

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

**Responses**

| Code | Description | Body |
| --- | --- | --- |
| `200` | Draft state | { success, data: object } |
| `404` | Draft not found. |  |

### PATCH /jobs/from-description/{review_token}

**Update a staged job draft (fill gaps)**

Fill one or more gaps on an in-progress draft. Every update triggers an AI summary rewrite so the draft reads coherently as structured fields are added. Pass only the fields you want to change in `updates`. Use `skip` to mark gaps as deliberately blank (the confirm endpoint only refuses while client_id is unfilled - it cannot be skipped).

Operation ID: `updateJobDraft`

**Path parameters**

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

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

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `updates` | object | no |  |
| `updates.title` | string | no |  |
| `updates.client_id` | string | no | Client (cli_ id) the job is for - the one mandatory gap. Must belong to the account. |
| `updates.client_name` | string | no | Optional display name override for the client. |
| `updates.team_id` | string | no |  |
| `updates.team_name` | string | no |  |
| `updates.hiring_manager_id` | string | no |  |
| `updates.hiring_manager_name` | string | no |  |
| `updates.location` | object | no |  |
| `updates.location.display` | string | no |  |
| `updates.location.type` | string (`on_site`, `hybrid`, `remote`) | no |  |
| `updates.location.city` | string \| null | no |  |
| `updates.location.country` | string \| null | no |  |
| `updates.location.country_code` | string \| null | no |  |
| `updates.compensation` | object | no |  |
| `updates.compensation.base_min` | number \| null | no |  |
| `updates.compensation.base_max` | number \| null | no |  |
| `updates.compensation.currency` | string | no |  |
| `updates.compensation.period` | string (`Per Year`, `Per Day`, `Per Hour`) | no | How the figures are quoted. Omit and the work type decides when the draft is confirmed. |
| `updates.employment_type` | string (`full_time`, `part_time`, `contract`) | no |  |
| `skip` | array of string | no | Gap fields to mark as skipped. |

**Responses**

| Code | Description | Body |
| --- | --- | --- |
| `200` | Updated draft + remaining gaps | { success, data: object } |
| `422` | Draft missing, expired, or already confirmed. |  |

### POST /jobs/from-description/{review_token}/revise

**Apply a free-text revision to a staged draft**

Apply a natural-language instruction ("make it punchier", "move to Sydney and bump salary to AUD 180k") to a staged draft. Applies the company brand voice + anti-AI-tics guardrails. Resolves team / hiring manager names against the company's lists so structured fields update alongside the prose.

Operation ID: `reviseJobDraft`

**Path parameters**

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

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

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `instruction` | string | yes | The change the user wants, in their own words. |

**Responses**

| Code | Description | Body |
| --- | --- | --- |
| `200` | Updated draft | { success, data: object } |
| `422` | Invalid instruction or draft in a non-revisable state. |  |

### GET /jobs/{id}/ad

**Get the latest job ad**

Fetch the most recent published job ad for a job. Every job created via the describe-a-job flow gets an auto-generated ad on the careers page; this endpoint reads it.

Operation ID: `getJobAd`

**Path parameters**

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

**Responses**

| Code | Description | Body |
| --- | --- | --- |
| `200` | Job ad | { success, data: object } |
| `404` | No ad exists for this job. |  |

### PATCH /jobs/{id}/ad

**Update a job ad**

Edit a job ad's title, description, or published state. Accepts any subset of the three fields.

Operation ID: `updateJobAd`

**Path parameters**

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

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

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `title` | string | no |  |
| `description` | string | no |  |
| `published` | boolean | no |  |

**Responses**

| Code | Description | Body |
| --- | --- | --- |
| `200` | Updated ad | object |
| `404` | No ad exists for this job. |  |

### POST /jobs/{id}/ad/rewrite

**AI-rewrite a job ad**

Apply a natural-language instruction to rewrite an existing ad. Applies the company brand voice + anti-AI-tics guardrails. Returns the new ad body; the DB is updated in place.

Operation ID: `rewriteJobAd`

**Path parameters**

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

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

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

**Responses**

| Code | Description | Body |
| --- | --- | --- |
| `200` | Rewritten ad | { success, data: object } |
| `404` | No ad exists for this job. |  |

### GET /jobs/{id}/social-draft

**Get a job's LinkedIn + X post drafts**

Fetch the auto-generated LinkedIn and X (Twitter) social post drafts for a job, plus the OG image URL used for unfurl previews.

Operation ID: `getSocialDraft`

**Path parameters**

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

**Responses**

| Code | Description | Body |
| --- | --- | --- |
| `200` | Social draft | { success, data: object } |
| `404` | Job not found. |  |

### PATCH /jobs/{id}/social-draft

**Update a job's social post text**

Edit the LinkedIn and/or X post text. og_image_url is preserved automatically.

Operation ID: `updateSocialDraft`

**Path parameters**

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

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

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `linkedin` | string | no |  |
| `twitter` | string | no |  |

**Responses**

| Code | Description | Body |
| --- | --- | --- |
| `200` | Updated social draft | object |
| `404` | Job not found. |  |

### GET /jobs/{id}

**Get job**

Operation ID: `getJob`

**Path parameters**

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

**Responses**

| Code | Description | Body |
| --- | --- | --- |
| `200` | Job details | { success, data: Job } |

### PATCH /jobs/{id}

**Update job**

Operation ID: `updateJob`

**Path parameters**

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

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

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `title` | string | no |  |
| `status` | string (`active`, `on_hold`, `closed`, `filled`) | no |  |
| `location` | object | no | Structured location. `type` is one of `on_site`, `hybrid`, `remote`. For remote roles, city/country/country_code are ignored. |
| `location.type` | string (`on_site`, `hybrid`, `remote`) | no |  |
| `location.city` | string \| null | no |  |
| `location.country` | string \| null | no |  |
| `location.country_code` | string \| null | no | ISO 3166-1 alpha-2 |
| `employment_type` | string (`full_time`, `part_time`, `contract`) | no |  |
| `compensation` | object | no |  |
| `compensation.base_min` | number | no |  |
| `compensation.base_max` | number | no |  |
| `compensation.currency` | string | no |  |
| `compensation.period` | string (`year`, `day`, `hour`) | no | How base_min/base_max are quoted. Only ever changed when sent - changing employment_type never re-labels figures already on the job. |
| `client_id` | string \| null | no | The client this job is for (cli_ id). Setting or clearing it also clears client_contact_id unless a matching contact is sent in the same request. |
| `client_contact_id` | string \| null | no | Contact at the client (cct_ id). Must belong to the job's client. |
| `team_id` | string \| null | no |  |
| `hiring_manager_id` | string \| null | no |  |
| `default_interview_format` | string (`video`, `phone`, `in_person`, `technical`) | no | Default interview format used when auto-booking interviews on stage transition. |
| `is_evergreen` | boolean | no | Mark or unmark the job as an always-open pipeline job (excluded from performance metrics). |

**Responses**

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

### DELETE /jobs/{id}

**Delete job**

Soft deletes a job. The record is hidden from all queries but not permanently removed. Requires jobs:write scope.

Operation ID: `deleteJob`

**Path parameters**

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

**Responses**

| Code | Description | Body |
| --- | --- | --- |
| `200` | Deleted | { deleted, id } |
| `404` | Job not found |  |

### POST /jobs/{id}/close

**Close job**

Operation ID: `closeJob`

**Path parameters**

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

**Responses**

| Code | Description | Body |
| --- | --- | --- |
| `200` | Job closed | { success, data: Job } |

### POST /jobs/{id}/reopen

**Reopen job**

Reopen a closed or filled job. Sets status to active. Requires jobs:write.

Operation ID: `reopenJob`

**Path parameters**

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

**Responses**

| Code | Description | Body |
| --- | --- | --- |
| `200` | Job reopened | { success, data: object } |
| `409` | Job is already active. |  |
| `422` | Plan job limit reached. |  |

### GET /jobs/{id}/summary

**AI job summary**

Operation ID: `getJobSummary`

**Path parameters**

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

**Responses**

| Code | Description | Body |
| --- | --- | --- |
| `200` | AI-generated role summary | { success, data: object } |

### GET /jobs/{id}/ranking

**Stack-ranked interviewed candidates for a job**

Returns every interviewed candidate on a job ordered by aggregate scorecard score (highest first). Aggregate score is the straight arithmetic mean of the candidate's form_submissions scores on this job. Per-criterion scores are normalised to 0-100 (pass/fail -> 100/0, scale 1-5 -> linear). Pure read, no side effects.

Operation ID: `getJobRanking`

**Path parameters**

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

**Responses**

| Code | Description | Body |
| --- | --- | --- |
| `200` | Ranked candidate list | { success, data: object } |
| `404` | Job not found |  |

### GET /jobs/{id}/share-bundle

**Get share-ready content bundle for a job**

Returns ready-to-share content: a pre-composed X post (under 260 chars), a LinkedIn post (300-400 chars), the public careers URL, an OG image URL, and a short email summary. All copy is adjusted to the company brand voice and passes the anti-AI-tics linter (no em dashes, no banned phrases). Response is cached server-side for 60 seconds. Use this to push share content to a customer Slack bot, an employee feed, or any programmatic amplification surface without logging into the dashboard.

Operation ID: `getJobShareBundle`

**Path parameters**

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

**Responses**

| Code | Description | Body |
| --- | --- | --- |
| `200` | Share bundle | { success, data: object } |
| `404` | Job not found |  |

## Schemas

Objects referenced by the operations above.

### Job

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | no |  |
| `account_id` | string | no |  |
| `title` | string | no |  |
| `status` | string (`active`, `on_hold`, `closed`, `filled`) | no | Jobs default to active on creation. Move freely between active, on_hold, and closed. filled is set automatically when a hire is accepted. |
| `client` | object \| null | no | The client this job is for. Every job created in-product gets one; jobs created via this API may omit it. |
| `client.id` | string | no |  |
| `client.name` | string \| null | no |  |
| `client_contact` | object \| null | no | The contact at the client for this job. Always belongs to the job's client. |
| `client_contact.id` | string | no |  |
| `client_contact.name` | string \| null | no |  |
| `team` | object \| null | no |  |
| `team.id` | string | no |  |
| `team.name` | string \| null | no |  |
| `hiring_manager` | object \| null | no |  |
| `hiring_manager.id` | string | no |  |
| `hiring_manager.name` | string \| null | no |  |
| `location` | object \| null | no | Structured job location. `type` is the workplace model. `city`, `country`, and `country_code` come from a Google Places city pick and are null for fully-remote roles. |
| `location.type` | string (`on_site`, `hybrid`, `remote`) | no |  |
| `location.city` | string \| null | no |  |
| `location.country` | string \| null | no |  |
| `location.country_code` | string \| null | no | ISO 3166-1 alpha-2 country code |
| `employment_type` | string (`full_time`, `part_time`, `contract`) \| null | no |  |
| `compensation` | object | no |  |
| `compensation.base_min` | number \| null | no |  |
| `compensation.base_max` | number \| null | no |  |
| `compensation.ote` | number \| null | no |  |
| `compensation.currency` | string | no |  |
| `compensation.period` | string (`year`, `day`, `hour`) | no | How base_min/base_max are quoted. Contract and temp roles default to hour, everything else to year; day is only ever set explicitly. Jobs created before 6.2.0 report year. |
| `description` | string \| null | no |  |
| `spec` | object \| null | no |  |
| `spec.summary` | string | no |  |
| `spec.responsibilities` | array of string | no |  |
| `spec.requirements` | array of string | no |  |
| `spec.nice_to_have` | array of string | no |  |
| `assessment_criteria` | array of object | no | AI-generated criteria for assessing candidates. |
| `interview_plan` | object \| null | no | AI-generated interview plan. |
| `job_ad` | string \| null | no | Associated job ad ID. |
| `share_links` | object \| null | no | Shareable URLs for the job. |
| `stats` | object \| null | no | Application statistics. List responses carry { total_applications }; GET /jobs/{id} adds one count per pipeline stage. |
| `target_start_date` | string (date) \| null | no |  |
| `compensation_research` | object \| null | no | Market compensation research data. |
| `default_interview_format` | string (`video`, `phone`, `in_person`, `technical`), default "video" | no | Default interview format used when scheduling. "video" auto-injects a Google Meet link when the interviewer has Google Calendar connected. "phone" puts the interviewer phone in the event description. "in_person" puts the job location in the description. |
| `is_evergreen` | boolean, default false | no | Always-open pipeline job ("people we always need"). Works like any open job (matching, ads, applications) but is excluded from performance metrics: billing forecast, client fill rate and momentum, active-job analytics, and no-applicant nudges. Settable on create and via PATCH. |
| `created_at` | string (date-time) | no |  |
| `updated_at` | string (date-time) | 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 |  |

### Task

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `task_id` | string | no |  |
| `status` | string (`queued`, `processing`, `completed`, `failed`) | no |  |
| `resource_id` | string \| null | no |  |
| `estimated_seconds` | integer | no |  |
| `poll_url` | string | no |  |
