# HireLayer Job Recommendations

> Find the open jobs closest to a resume, sent as a stored id, a text or a file, in one call.

Source: https://hirelayer.co/api-docs/search-jobs · Markdown: https://hirelayer.co/api-docs/search-jobs.md

HireLayer Job Recommendations finds the open jobs of a workspace closest to a resume. One `POST /api/v4/jobs/search` call with a stored `application_id`, a resume text or a resume file returns up to 100 `job_id`s sorted by closeness, with a `similarity` each, for 1 credit per search (+1 when the resume comes as a file, parsed and stored).

Only jobs stored with `status: "open"` are searched. To explain a recommendation, score the resume against that job with the [Resume Scorer](https://hirelayer.co/api-docs/score.md), one call per job.

- **Endpoint:** `POST /api/v4/jobs/search`
- **Input:** An `application_id`, a resume text or a resume file
- **Output:** Job ids, closest first
- **Price:** 1 credit per search, +1 for a resume file

## Recommend jobs for a resume

`POST https://hirelayer.co/api/v4/jobs/search`

Finds the open stored jobs of the workspace closest to a resume. Proximity only: no evaluation, no explanation. Score the chosen ones with `POST /resumes/score`, one call per job.

- **Authentication:** `X-API-Key` header
- **Content type:** `application/json`, or `multipart/form-data` with files
- **Billing:** 1 credit per search, +1 for a resume file
- **Client timeout:** at least 60 seconds

### Headers

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `HireLayer-Workspace` | `string` | No | Your customer's workspace id. Case-insensitive, created on first use. Absent, calls use `default`. |

### Request body

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `application_id` | `string` | No | Letters, digits and `. _ : ~ -`, up to 200 characters. |
| `resume` | `object` | No | A resume not stored yet; it is stored under your id, or a `text_` id. |
| `resume.text` | `string` | Yes | 1–50,000 characters. |
| `resume.application_id` | `string` | No | Store the resume under your own id. |
| `limit` | `integer` | No | 1–100, default `10`. |
| `do_not_store_data` | `boolean` | No | Enterprise plan only, else `403 enterprise_plan_required`: process without storing anything. See [Storage](https://hirelayer.co/api-docs/authentication.md#storage). Default `false`. |

Send exactly one of `application_id` or `resume`.

### Example request

**cURL**

```bash
# Example ids: replace file_2c982f413a5e9e79563f8a1fcb5c7caa with an application_id returned by POST /api/v4/resumes/parse.
curl -X POST https://hirelayer.co/api/v4/jobs/search \
  -H "X-API-Key: $HIRELAYER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "application_id": "file_2c982f413a5e9e79563f8a1fcb5c7caa",
    "limit": 10
  }'
```

**Python**

```python
# Example ids: replace file_2c982f413a5e9e79563f8a1fcb5c7caa with an application_id returned by POST /api/v4/resumes/parse.
import os

import requests

response = requests.post(
    "https://hirelayer.co/api/v4/jobs/search",
    headers={"X-API-Key": os.environ["HIRELAYER_API_KEY"]},
    json={
        "application_id": "file_2c982f413a5e9e79563f8a1fcb5c7caa",
        "limit": 10,
    },
    timeout=60,
)
response.raise_for_status()

data = response.json()
print(data["results"])
```

**TypeScript**

```typescript
// Example ids: replace file_2c982f413a5e9e79563f8a1fcb5c7caa with an application_id returned by POST /api/v4/resumes/parse.
const response = await fetch('https://hirelayer.co/api/v4/jobs/search', {
  method: 'POST',
  headers: {
    'X-API-Key': process.env.HIRELAYER_API_KEY!,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    application_id: 'file_2c982f413a5e9e79563f8a1fcb5c7caa',
    limit: 10,
  }),
  signal: AbortSignal.timeout(60_000),
})

if (!response.ok) {
  throw new Error(`HireLayer ${response.status}: ${await response.text()}`)
}

const data = await response.json()
console.log(data.results)
```

### Response `200`

The closest open jobs, closest first.

| Field | Type | Description |
| --- | --- | --- |
| `request_id` | `string` | The call's id, also in the `HireLayer-Request-Id` header. Quote it when contacting support. |
| `application_id` | `string` |  |
| `results` | `object[]` |  |
| `results[].rank` | `integer` | ≥ 1. |
| `results[].job_id` | `string` |  |
| `results[].title` | `string \| null` |  |
| `results[].similarity` | `number` | 0–1. |
| `results[].already_scored` | `boolean` | The resume is already scored for this job, or being scored (`pending`); scoring it again costs 0 unless the score is `stale` or its `verdict` was reset. |
| `credits_used` | `integer` | Credits this call cost. ≥ 0. |

```json
{
  "request_id": "9d3f5a20-1c6e-4b78-a2f4-3e8b7c1d0f69",
  "application_id": "file_2c982f413a5e9e79563f8a1fcb5c7caa",
  "results": [
    {
      "rank": 1,
      "job_id": "job_30cfa454c97663f3e1801e5a61b87d74",
      "title": "Ingénieur IA / Développeur backend Node.js (H/F)",
      "similarity": 0.78,
      "already_scored": true
    },
    {
      "rank": 2,
      "job_id": "senior-ds-nlp",
      "title": "Senior Data Scientist – NLP (m/f)",
      "similarity": 0.74,
      "already_scored": false
    }
  ],
  "credits_used": 1
}
```

### From a resume file request

Send the resume as a file in the multipart field `file`: it is parsed and stored first, +1 credit.

**cURL**

```bash
curl -X POST https://hirelayer.co/api/v4/jobs/search \
  -H "X-API-Key: $HIRELAYER_API_KEY" \
  -F "file=@resume.pdf;type=application/pdf"
```

**Python**

```python
import os

import requests

with open("resume.pdf", "rb") as file:
    response = requests.post(
        "https://hirelayer.co/api/v4/jobs/search",
        headers={"X-API-Key": os.environ["HIRELAYER_API_KEY"]},
        files={"file": ("resume.pdf", file, "application/pdf")},
        timeout=150,
    )
response.raise_for_status()

data = response.json()
print(data["results"])
```

**TypeScript**

```typescript
import { readFile } from 'node:fs/promises'

const form = new FormData()
form.append(
  'file',
  new Blob([await readFile('resume.pdf')], { type: 'application/pdf' }),
  'resume.pdf'
)

const response = await fetch('https://hirelayer.co/api/v4/jobs/search', {
  method: 'POST',
  headers: {
    'X-API-Key': process.env.HIRELAYER_API_KEY!,
  },
  body: form,
  signal: AbortSignal.timeout(150_000),
})

if (!response.ok) {
  throw new Error(`HireLayer ${response.status}: ${await response.text()}`)
}

const data = await response.json()
console.log(data.results)
```

The response is the `200` above.

### Errors

| Status | `error.code` | `error.message` | When | Retry |
| --- | --- | --- | --- | --- |
| `400` | [`invalid_request`](https://hirelayer.co/api-docs/errors.md#invalid_request) | `application_id or resume is required.` | `param`: `application_id`. Neither `application_id` nor `resume`, or both. | Fix, then retry |
| `400` | [`invalid_request`](https://hirelayer.co/api-docs/errors.md#invalid_request) | `resume.text is required.` | `param`: `resume.text`. `resume` without its text. | Fix, then retry |
| `400` | [`invalid_request`](https://hirelayer.co/api-docs/errors.md#invalid_request) | `file is required: send the resume in the multipart part file.` | `param`: `file`. In multipart, no part `file`. | Fix, then retry |
| `400` | [`invalid_request`](https://hirelayer.co/api-docs/errors.md#invalid_request) | `limit must be an integer from 1 to 100.` | `param`: `limit`. `limit` out of range. | Fix, then retry |
| `400` | [`invalid_request`](https://hirelayer.co/api-docs/errors.md#invalid_request) | `application_id must contain letters, digits and . _ : ~ -, up to 200 characters.` | `param`: `application_id`. `application_id` is not a valid id, or is a name the paths reserve. | Fix, then retry |
| `400` | [`invalid_request`](https://hirelayer.co/api-docs/errors.md#invalid_request) | `Unknown field lang.` | `param`: `lang`. A field the call does not take. | Fix, then retry |
| `400` | [`invalid_request`](https://hirelayer.co/api-docs/errors.md#invalid_request) | `The request body is not valid JSON.` | The body is not valid JSON, not an object, or not sent as `application/json`. | Fix, then retry |
| `400` | [`invalid_request`](https://hirelayer.co/api-docs/errors.md#invalid_request) | `HireLayer-Workspace must contain letters, digits and . _ : ~ -, up to 200 characters.` | `param`: `HireLayer-Workspace`. `HireLayer-Workspace` is not a valid id. | Fix, then retry |
| `400` | [`storage_required`](https://hirelayer.co/api-docs/errors.md#storage_required) | `A search reads the stored resumes and jobs: it is not available without storage.` | `param`: `do_not_store_data`. `do_not_store_data: true` (Enterprise plan): a search needs storage. | Do not retry |
| `400` | [`storage_required`](https://hirelayer.co/api-docs/errors.md#storage_required) | `This workspace runs without storage (do_not_store_data): a search reads the stored resumes and jobs, so it is not available in it.` | `param`: `HireLayer-Workspace`. The workspace runs without storage. | Do not retry |
| `401` | [`missing_api_key`](https://hirelayer.co/api-docs/errors.md#missing_api_key) | `The X-API-Key header is missing.` | No `X-API-Key` header. | Fix, then retry |
| `401` | [`missing_api_key`](https://hirelayer.co/api-docs/errors.md#missing_api_key) | `Authorization: Bearer is not accepted: send the key in the X-API-Key header.` | The key sent as `Authorization: Bearer`: it goes in `X-API-Key`. | Fix, then retry |
| `401` | [`missing_api_key`](https://hirelayer.co/api-docs/errors.md#missing_api_key) | `The key goes in the X-API-Key header, not in the URL.` | The key sent in the URL: it goes in `X-API-Key`. | Fix, then retry |
| `401` | [`invalid_api_key`](https://hirelayer.co/api-docs/errors.md#invalid_api_key) | `Invalid API key: it is unknown, malformed or revoked.` | The key is unknown, malformed or revoked. | Fix, then retry |
| `403` | [`insufficient_credits`](https://hirelayer.co/api-docs/errors.md#insufficient_credits) | `This search parses 1 file and searches, 1 credit each: 2 credits, and 1 is left. Nothing was processed or billed.` | The credits left do not cover the search (and the parse of a file). | Fix, then retry |
| `403` | [`enterprise_plan_required`](https://hirelayer.co/api-docs/errors.md#enterprise_plan_required) | `do_not_store_data is available on the Enterprise plan only. Nothing was processed or billed.` | `param`: `do_not_store_data`. `do_not_store_data: true` below the Enterprise plan. | Do not retry |
| `404` | [`not_found`](https://hirelayer.co/api-docs/errors.md#not_found) | `No resume cand-42 in workspace default.` | `param`: `application_id`. No resume under `application_id` in the workspace. | Do not retry |
| `413` | [`payload_too_large`](https://hirelayer.co/api-docs/errors.md#payload_too_large) | `file must be 4.5 MB or less.` | `param`: `file`. The file is over 4.5 MB. | Fix, then retry |
| `413` | [`payload_too_large`](https://hirelayer.co/api-docs/errors.md#payload_too_large) | `resume.text must be 50,000 characters or less.` | `param`: `resume.text`. The resume text is over 50,000 characters. | Fix, then retry |
| `422` | [`resume_unreadable`](https://hirelayer.co/api-docs/errors.md#resume_unreadable) | `file cannot be read as a resume: it is empty, damaged or in an unsupported format. Nothing was billed.` | `param`: `file`. The file cannot be read as a resume. | Do not retry |
| `429` | [`rate_limited`](https://hirelayer.co/api-docs/errors.md#rate_limited) | `Too many files are being parsed at once for this account: retry after the Retry-After delay.` | Too many parses at once. Headers: `Retry-After: <seconds>`. | Retry with backoff |
| `500` | [`internal_error`](https://hirelayer.co/api-docs/errors.md#internal_error) | `Internal error: retry later. If it persists, contact support with the request_id.` | An unexpected error. | Retry with backoff |
| `502` | [`model_unavailable`](https://hirelayer.co/api-docs/errors.md#model_unavailable) | `The analysis did not complete after several attempts. Nothing was stored or billed: retry with backoff.` | The analysis did not complete. | Retry with backoff |
| `502` | [`internal_error`](https://hirelayer.co/api-docs/errors.md#internal_error) | `The service could not be reached: retry later.` | The service could not be reached. | Retry with backoff |
| `503` | [`internal_error`](https://hirelayer.co/api-docs/errors.md#internal_error) | `The credit service is briefly unavailable: retry in a second.` | The credit service is briefly unavailable. Headers: `Retry-After: <seconds>`. | Retry with backoff |
| `503` | [`internal_error`](https://hirelayer.co/api-docs/errors.md#internal_error) | `The resume was parsed but could not be stored, so there is nothing to search with: retry in a few seconds.` | `param`: `file`. The resume file was parsed but could not be stored yet. Headers: `Retry-After: <seconds>`. | Retry with backoff |
| `504` | [`internal_error`](https://hirelayer.co/api-docs/errors.md#internal_error) | `The service did not answer in time. The change may still be applied: read it before sending it again.` | No answer in time. | Retry with backoff |

Example error (`400`):

```json
{
  "error": {
    "code": "invalid_request",
    "message": "application_id or resume is required.",
    "param": "application_id",
    "doc_url": "https://hirelayer.co/api-docs/errors#invalid_request"
  },
  "request_id": "6f1c2a4e-1d2b-4c3a-9e8f-0a1b2c3d4e5f"
}
```

### Behavior

- **Credits:** 1 per search, +1 when the resume comes as a file (parsed and stored).
- **Without storage:** not available (`400 storage_required`), since there are no stored jobs.

## Next

- [HireLayer Resume Scorer](https://hirelayer.co/api-docs/score.md): HireLayer candidate matching and ranking API: score one or many resumes against a job, criterion by criterion, with evidence, a verdict and an optional precise ranking.
- [HireLayer Talent Pool Search](https://hirelayer.co/api-docs/search-resumes.md): Find the resumes closest to a job in your resume database, filtered by location and date added, in one call.
