# HireLayer Resume Parser

> HireLayer resume parser API reference: send a PDF, Word or image resume in one multipart request and get the candidate back as JSON, photo included, in 70 languages.

Source: https://hirelayer.co/api-docs/parse-resume · Markdown: https://hirelayer.co/api-docs/parse-resume.md

The HireLayer Resume Parser is a resume parsing REST API for developers. One multipart `POST /api/v4/resumes/parse` request with an `X-API-Key` header returns the candidate as JSON in the same HTTP response, under `resume`. It reads 15 file formats, scans and phone photos included, in 70 languages, for 1 credit per resume; the same file sent again is answered from storage for free. The [Free plan](https://hirelayer.co/auth/signup) includes 50 credits a month with no card.

> **Legacy: Already on the v3 or v2 parser?.** The [v3 parser](https://hirelayer.co/api-docs/legacy-parsers.md#parse-resume-v3) (`POST /api/v3/parser`) and the [v2 parser](https://hirelayer.co/api-docs/legacy-parsers.md#parse-resume-v2) (`POST /api/v2/parser`) keep working with their JSON unchanged. Their reference, and what changes in v4, are on the [legacy parsers](https://hirelayer.co/api-docs/legacy-parsers.md) page. New integrations use the v4 endpoint below.

- `info_candidate`: name, lower-cased email, E.164 phone number, location with coordinates, LinkedIn and GitHub URLs, job title, highest EQF education level and experience bracket.
- `work_experiences` and `educations`: every position and diploma with normalized dates, contract type and location.
- `languages`: each language with a CEFR-based level such as `Professional Working Proficiency (B2)`.
- `skills`: typed as `Hard skill`, `Soft skill` or `Software skill`, and resolved to the [HireLayer skills taxonomy](https://hirelayer.co/api-docs/skills.md) when a match exists.
- `info_resume`: the candidate photo cropped from the resume (`face_url`), the full extracted text and the resume language.

The parse is stored in the [resume database](https://hirelayer.co/api-docs/resumes.md) of the workspace under its `application_id`: score it with the [Resume Scorer](https://hirelayer.co/api-docs/score.md) or find it with [Talent Pool Search](https://hirelayer.co/api-docs/search-resumes.md) without sending the file again.

- **Endpoint:** `POST /api/v4/resumes/parse`
- **Input:** `multipart/form-data` · 15 file formats · 4.5 MB max
- **Output:** JSON, usually in 25 to 35 seconds
- **Price:** 1 credit; 0 when the file is already stored
- **Legacy versions:** [v3](https://hirelayer.co/api-docs/legacy-parsers.md#parse-resume-v3) and [v2](https://hirelayer.co/api-docs/legacy-parsers.md#parse-resume-v2), for existing integrations

## Parse a resume

`POST https://hirelayer.co/api/v4/resumes/parse`

Reads a resume file and returns it structured, stored in the workspace under an `application_id`.

- **Authentication:** `X-API-Key` header
- **Content type:** `multipart/form-data`
- **Billing:** 1 credit; 0 when the file is already stored
- **Client timeout:** at least 150 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 |
| --- | --- | --- | --- |
| `file` | `file` | Yes | PDF, DOC, DOCX, ODT, PPT, PPTX, ODP, XLS, RTF, TXT, HTML, Markdown, JPG/JPEG, PNG or BMP, 4.5 MB at most. |
| `application_id` | `string` | No | Letters, digits and `. _ : ~ -`, up to 200 characters. |
| `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`. |
| `webhook_url` | `string (URL)` | No | HTTPS URL on the standard port, without credentials, that receives the result, signed. |

### Example request

**cURL**

```bash
curl -X POST https://hirelayer.co/api/v4/resumes/parse \
  -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/resumes/parse",
        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["resume"]["info_candidate"])
```

**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/resumes/parse', {
  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.resume.info_candidate)
```

### Response `200`

Parsed (or found already stored).

| Field | Type | Description |
| --- | --- | --- |
| `application_id` | `string \| null` | The resume's id in the workspace, yours or derived from the file. `null` without storage. |
| `request_id` | `string` | Quote it when contacting support. |
| `stored` | `boolean` |  |
| `already_stored` | `boolean` | The same file was already stored; it was not parsed again. |
| `resume` | `ParsedResume` | The resume, with the fields and values of the legacy `/api/v3/parser` JSON. Only the envelope differs: `application_id` is at the top level of the v4 response, and `status` and `errors` give way to HTTP status codes. |
| `resume.info_resume` | `ResumeInfo` | The document. |
| `resume.info_resume.date_parsing` | `string` | Parsing time in UTC, `YYYY-MM-DDTHH:mm:ss`, without a zone suffix. |
| `resume.info_resume.language` | `string \| null` | Main language of the resume, uppercase ISO 639-1 code such as `EN` or `FR`. |
| `resume.info_resume.url` | `string \| null` | URL of the stored original file. Anyone with the link can open it. `null` with `do_not_store_data=true`, because the file is not stored. |
| `resume.info_resume.face_url` | `string \| null` | Candidate photo cropped from the resume. `null` when no face is found and for `.txt` files. |
| `resume.info_resume.face_url_expires_at` | `string \| null` | With `do_not_store_data=true`: expiry of the temporary `face_url`, 10 minutes after parsing, UTC `YYYY-MM-DDTHH:mm:ss`. Otherwise `null` and `face_url` does not expire. |
| `resume.info_resume.text` | `string` | Full extracted text, including OCR. May end with `[EXTRACTED_HYPERLINKS]` and `[PDF_ANNOTATION_TEXTS]` sections. |
| `resume.info_candidate` | `Candidate` | The candidate profile. |
| `resume.info_candidate.full_name` | `string \| null` | First and last name. |
| `resume.info_candidate.last_name` | `string \| null` | Last name. |
| `resume.info_candidate.first_name` | `string \| null` | First name. |
| `resume.info_candidate.email` | `string \| null` | Lower-cased email address. |
| `resume.info_candidate.phone_number` | `string \| null` | E.164 phone number, e.g. `+33612345678`. The country is inferred from the resume, France by default. A number written in the resume that is not valid for its country is still returned, in E.164 when its length fits, otherwise as written, with a warning. |
| `resume.info_candidate.birth_date` | `string \| null` | Date of birth, `YYYY-MM-DD`. A year alone becomes `YYYY-01-01`. |
| `resume.info_candidate.age` | `number \| null` | Age in years, as stated or inferred. |
| `resume.info_candidate.availability_now` | `boolean \| null` | Whether the candidate is available now. |
| `resume.info_candidate.availability_date` | `string \| null` | Date from which the candidate is available, `YYYY-MM-DD`. |
| `resume.info_candidate.driver_license` | `string[]` | Driving licences as written, e.g. `Permis B`. |
| `resume.info_candidate.job_title` | `string \| null` | Target or current job title. Falls back to the most recent position. |
| `resume.info_candidate.education_name` | `string \| null` | Highest qualification relevant to the job title. |
| `resume.info_candidate.education_level` | `string \| null` | Highest European Qualifications Framework (EQF) level. One of: `"Level 1"`, `"Level 2"`, `"Level 3"`, `"Level 4"`, `"Level 5"`, `"Level 6"`, `"Level 7"`, `"Level 8"`, `"Other"`. |
| `resume.info_candidate.experience_level` | `string \| null` | Total professional experience bracket. One of: `"0 to 1 year"`, `"1 to 3 years"`, `"3 to 5 years"`, `"5 to 10 years"`, `"More than 10 years"`. |
| `resume.info_candidate.linkedin_url` | `string \| null` | LinkedIn profile URL. |
| `resume.info_candidate.github_url` | `string \| null` | GitHub profile URL. |
| `resume.info_candidate.other_urls` | `string[]` | Other URLs in the resume (portfolio, website…). |
| `resume.info_candidate.location` | `Location` | Where the candidate lives. |
| `resume.info_candidate.location.country` | `string \| null` | Country name. |
| `resume.info_candidate.location.country_code` | `string \| null` | ISO 3166-1 alpha-2 country code, e.g. `FR`. |
| `resume.info_candidate.location.region` | `string \| null` | Region. |
| `resume.info_candidate.location.department` | `string \| null` | Department or county. |
| `resume.info_candidate.location.city` | `string \| null` | City. |
| `resume.info_candidate.location.postal_code` | `string \| null` | Postal code. |
| `resume.info_candidate.location.full_address` | `string \| null` | Address as a single line. |
| `resume.info_candidate.location.latitude` | `number \| null` | Latitude from geocoding. |
| `resume.info_candidate.location.longitude` | `number \| null` | Longitude from geocoding. |
| `resume.info_candidate.mobility` | `Mobility` | Where else the candidate can work. |
| `resume.info_candidate.mobility.can_work_in_other_cities` | `boolean \| null` | Whether the resume says the candidate can work elsewhere. `null` when not mentioned. |
| `resume.info_candidate.mobility.other_cities` | `MobilityCity[]` | Cities explicitly named as possible work locations. |
| `resume.info_candidate.mobility.other_cities[].city` | `string \| null` | City named in the resume. |
| `resume.info_candidate.mobility.other_cities[].country` | `string \| null` | Country, when stated or reliably inferred. |
| `resume.info_candidate.mobility.other_cities[].postal_code` | `string \| null` | Postal code, when present in the resume. |
| `resume.info_candidate.mobility.other_cities[].latitude` | `number \| null` | Latitude from geocoding. |
| `resume.info_candidate.mobility.other_cities[].longitude` | `number \| null` | Longitude from geocoding. |
| `resume.work_experiences` | `WorkExperience[]` |  |
| `resume.work_experiences[].company_name` | `string \| null` | Employer name. |
| `resume.work_experiences[].job_title` | `string \| null` | Job title for this position. |
| `resume.work_experiences[].description` | `string \| null` | Tasks, responsibilities and achievements. |
| `resume.work_experiences[].contract_type` | `string \| null` | Canonical contract type. Unrecognized values become `Other`. One of: `"Permanent contract"`, `"Fixed-term contract"`, `"Temporary assignment"`, `"Internship"`, `"Apprenticeship"`, `"Freelance"`, `"Volunteering"`, `"Other"`. |
| `resume.work_experiences[].start_date` | `string \| null` | Start date, `YYYY-MM-DD`. A year alone becomes `YYYY-01-01`; a month alone, its first day. |
| `resume.work_experiences[].end_date` | `string \| null` | End date, `YYYY-MM-DD`. `null` for an ongoing role. A year alone becomes `YYYY-12-31`; a month alone, its last day. |
| `resume.work_experiences[].currently_active` | `boolean \| null` | Whether this is a current position. |
| `resume.work_experiences[].work_experience_country` | `string \| null` | Country of the position. |
| `resume.work_experiences[].work_experience_country_code` | `string \| null` | ISO 3166-1 alpha-2 country code. |
| `resume.work_experiences[].work_experience_city` | `string \| null` | City of the position. |
| `resume.work_experiences[].work_experience_postal_code` | `string \| null` | Postal code of the position. |
| `resume.work_experiences[].experience_duration` | `number \| null` | Duration of this position in months, as extracted. |
| `resume.educations` | `Education[]` |  |
| `resume.educations[].degree_title` | `string \| null` | Name of the degree or programme. |
| `resume.educations[].school_name` | `string \| null` | School or institution. |
| `resume.educations[].description` | `string \| null` | Details of the programme. |
| `resume.educations[].degree_type` | `string \| null` | EQF level of the degree, expected to be one of `Level 1`…`Level 8` or `Other`. Not normalized: treat other strings as `Other`. |
| `resume.educations[].start_date` | `string \| null` | Start date, `YYYY-MM-DD`. |
| `resume.educations[].end_date` | `string \| null` | End date, `YYYY-MM-DD`. |
| `resume.educations[].currently_active` | `boolean \| null` | Whether the programme is in progress. |
| `resume.educations[].location` | `EducationLocation` | Location of the school. |
| `resume.educations[].location.country` | `string \| null` | Country name. |
| `resume.educations[].location.country_code` | `string \| null` | ISO 3166-1 alpha-2 country code, e.g. `FR`. |
| `resume.educations[].location.region` | `string \| null` | Region. |
| `resume.educations[].location.department` | `string \| null` | Department or county. |
| `resume.educations[].location.city` | `string \| null` | City. |
| `resume.educations[].location.postal_code` | `string \| null` | Postal code. |
| `resume.educations[].location.full_address` | `string \| null` | Address as a single line. |
| `resume.rome_jobs` | `RomeJob[]` | Occupations predicted from `info_candidate.job_title` with the French ROME taxonomy. |
| `resume.rome_jobs[].job_title` | `string` | Occupation label, in French. |
| `resume.rome_jobs[].job_code` | `string` | Occupation (appellation) code. |
| `resume.rome_jobs[].rome_title` | `string` | ROME job family label, in French. |
| `resume.rome_jobs[].rome_code` | `string` | ROME code, e.g. `M1805`. |
| `resume.rome_jobs[].prediction_score` | `number` | Confidence between 0.7 and 1. |
| `resume.languages` | `Language[]` |  |
| `resume.languages[].language` | `string` | Language name as written in the resume. |
| `resume.languages[].level` | `string \| null` | CEFR-based proficiency, or `null` when not stated. One of: `"Native or Bilingual (C2)"`, `"Full Professional Proficiency (C1)"`, `"Professional Working Proficiency (B2)"`, `"Limited Working Proficiency (B1)"`, `"Advanced Basic Proficiency (A2)"`, `"Introductory Proficiency (A1)"`. |
| `resume.skills` | `Skill[]` |  |
| `resume.skills[].skill_title` | `string` | Taxonomy skill name when `status` is `normalized`: in French for a resume in French, in English for a resume in any other language. Otherwise the text from the resume. One resume skill can give several normalized skills (`Word/Excel`). |
| `resume.skills[].skill_type` | `string` | One of: `"Hard skill"`, `"Soft skill"`, `"Software skill"`. |
| `resume.skills[].status` | `string` | `normalized`: resolved to the [HireLayer Skills taxonomy](https://hirelayer.co/api-docs/skills.md). `raw`: kept as written in the resume. One of: `"normalized"`, `"raw"`. |
| `resume.skills[].domain` | `string \| null` | Primary domain of the taxonomy skill, in the language of `skill_title`. `null` when `status` is `raw`. |
| `resume.skills[].subcategory` | `string \| null` | Primary family of the taxonomy skill, in the language of `skill_title`. `null` when `status` is `raw`. |
| `resume.certifications` | `string[]` |  |
| `resume.interests` | `string[]` |  |
| `warnings` | `string[]` | Notes about skipped steps (OCR, photo, geocoding, occupation codes) or about a document that may not be a resume. |
| `credits_used` | `integer` | Credits this call cost. ≥ 0. |

```json
{
  "application_id": "file_2c982f413a5e9e79563f8a1fcb5c7caa",
  "request_id": "bbf29879-8b02-4b0d-8f66-59bfb2f87662",
  "stored": true,
  "already_stored": false,
  "resume": {
    "info_resume": {
      "date_parsing": "2026-10-26T22:38:42",
      "language": "FR",
      "url": "https://files.hirelayer.co/originals/cv-paul-verse.pdf",
      "face_url": "https://files.hirelayer.co/faces/cv-paul-verse-face.jpg",
      "face_url_expires_at": null,
      "text": "FREELANCE\n\nIngénieur IA / Développeur full stack\n\nEXPÉRIENCES\n2024-25\n\nINGÉNIEUR IA - Novalys AI\nDéveloppement d’une plateforme SaaS intégrant\nplusieurs fonctionnalités basées sur l’IA générative.\nConception du produit de bout en bout : backend,\ninfrastructure cloud, orchestration des modèles et\nmonitoring des usages.\n- Stack API (serverless AWS) : API Gateway, Lambda, S3,\nDynamoDB, Terraform, Nod…"
    },
    "info_candidate": {
      "full_name": "PAUL VERSE",
      "last_name": "VERSE",
      "first_name": "PAUL",
      "email": "paul.verse@example.com",
      "phone_number": "+33600000000",
      "birth_date": null,
      "age": null,
      "availability_now": true,
      "availability_date": null,
      "driver_license": [],
      "job_title": "Ingénieur IA / Développeur full stack",
      "education_name": "Master 2 en Ecole d’ingénieur du numérique",
      "education_level": "Level 7",
      "experience_level": "5 to 10 years",
      "linkedin_url": "https://linkedin.com/in/paulverse",
      "github_url": "https://github.com/paulverse",
      "other_urls": [],
      "location": {
        "country": "France",
        "country_code": "FR",
        "region": "Ile-de-France",
        "department": null,
        "city": null,
        "postal_code": null,
        "full_address": null,
        "latitude": 48.922413,
        "longitude": 2.357223
      },
      "mobility": {
        "can_work_in_other_cities": null,
        "other_cities": []
      }
    },
    "work_experiences": [
      {
        "company_name": "Novalys AI",
        "job_title": "Ingénieur IA",
        "description": "Développement d’une plateforme SaaS intégrant plusieurs fonctionnalités basées sur l’IA générative. Conception du produit de bout en bout : backend, infrastructure cloud, orchestration des modèles et monitoring des usages. Stack API (serverless AWS) : API Gateway, Lambda, S3, DynamoDB, Terraform, Node.js. Stack SaaS : Next.js, React, serveur cloud géré avec Coolify (déploiement, monitoring). Réduction de 50 % de la latence moyenne sur les principales fonctionnalités IA. Solution scalable, 500k+ documents traités.",
        "contract_type": null,
        "start_date": "2024-01-01",
        "end_date": "2025-12-31",
        "currently_active": false,
        "work_experience_country": "France",
        "work_experience_country_code": "FR",
        "work_experience_city": null,
        "work_experience_postal_code": null,
        "experience_duration": 24
      },
      {
        "company_name": "COP1",
        "job_title": "Directeur du réseau",
        "description": "Gouvernance et direction bénévole d’une association d’aide aux jeunes et étudiants, présente dans 27 villes. 130 000 paniers/an, 3 M€ de budget annuel, 25 salariés. Développement d’une application web ERP personnalisée (Next.JS) intégrant l’intelligence artificielle et optimisant stocks, approvisionnement, suivi des véhicules et gestion des bénévoles. Expansion du réseau : de 20 à 27 antennes. Croissance des bénévoles : de 3 000 à 5 000 membres.",
        "contract_type": "Volunteering",
        "start_date": "2024-01-01",
        "end_date": "2025-12-31",
        "currently_active": false,
        "work_experience_country": "France",
        "work_experience_country_code": "FR",
        "work_experience_city": null,
        "work_experience_postal_code": null,
        "experience_duration": 24
      }
    ],
    "educations": [
      {
        "degree_title": "Master 2 en Ecole d’ingénieur du numérique",
        "school_name": "EFREI",
        "description": "BIG DATA & IA. Cours intégralement en Anglais.",
        "degree_type": "Level 7",
        "start_date": "2020-01-01",
        "end_date": "2025-12-31",
        "currently_active": false,
        "location": {
          "country": null,
          "country_code": null,
          "region": null,
          "department": null,
          "city": null,
          "postal_code": null,
          "full_address": null
        }
      }
    ],
    "rome_jobs": [
      {
        "job_title": "Développeur / Développeuse full-stack",
        "job_code": "38976",
        "rome_title": "Développeur / Développeuse web",
        "rome_code": "M1855",
        "prediction_score": 0.817
      },
      {
        "job_title": "Développeur / Développeuse IA",
        "job_code": "510526",
        "rome_title": "Ingénieur / Ingénieure en Intelligence Artificielle (IA)",
        "rome_code": "M1889",
        "prediction_score": 0.779
      }
    ],
    "languages": [
      {
        "language": "Anglais",
        "level": "Native or Bilingual (C2)"
      },
      {
        "language": "Allemand",
        "level": "Introductory Proficiency (A1)"
      }
    ],
    "skills": [
      {
        "skill_title": "Python",
        "skill_type": "Software skill",
        "status": "normalized",
        "domain": "Développement logiciel",
        "subcategory": "Langages de programmation"
      },
      {
        "skill_title": "Next.js",
        "skill_type": "Software skill",
        "status": "normalized",
        "domain": "Développement logiciel",
        "subcategory": "Développement frontend"
      },
      {
        "skill_title": "Vue.js",
        "skill_type": "Software skill",
        "status": "normalized",
        "domain": "Développement logiciel",
        "subcategory": "Développement frontend"
      },
      {
        "skill_title": "React",
        "skill_type": "Software skill",
        "status": "normalized",
        "domain": "Développement logiciel",
        "subcategory": "Développement frontend"
      },
      {
        "skill_title": "Node.js",
        "skill_type": "Software skill",
        "status": "normalized",
        "domain": "Développement logiciel",
        "subcategory": "Développement backend"
      }
    ],
    "certifications": [
      "AZURE - DATA SCIENTIST ASSOCIATE",
      "HUGGINGFACE - DEEP RL CERTIFICATION",
      "AWS - CLOUD PRACTITIONER"
    ],
    "interests": [
      "Sports",
      "Voyages",
      "Course, Trail (longue distance)",
      "8 pays visités seuls"
    ]
  },
  "warnings": [],
  "credits_used": 1
}
```

### Response `202`

Accepted. Sent with `webhook_url`; the result is posted there.

| Field | Type | Description |
| --- | --- | --- |
| `status` | `string` | Value: `"pending"`. |
| `request_id` | `string` |  |
| `application_id` | `string \| null` |  |
| `webhook_url` | `string (URL)` |  |

### With a webhook request

With `webhook_url`, the call answers `202` at once and the result is posted to that URL, signed: see [Webhooks](https://hirelayer.co/api-docs/webhooks.md).

**cURL**

```bash
curl -X POST https://hirelayer.co/api/v4/resumes/parse \
  -H "X-API-Key: $HIRELAYER_API_KEY" \
  -F "file=@resume.pdf;type=application/pdf" \
  -F "webhook_url=https://example.com/webhooks/hirelayer"
```

**Python**

```python
import os

import requests

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

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

**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'
)
form.append('webhook_url', 'https://example.com/webhooks/hirelayer')

const response = await fetch('https://hirelayer.co/api/v4/resumes/parse', {
  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.request_id)
```

```json
{
  "status": "pending",
  "request_id": "bbf29879-8b02-4b0d-8f66-59bfb2f87662",
  "application_id": "file_2c982f413a5e9e79563f8a1fcb5c7caa",
  "webhook_url": "https://example.com/webhooks/hirelayer"
}
```

### Errors

| Status | `error.code` | `error.message` | When | 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`. No part `file`. | Fix, then retry |
| `400` | [`invalid_request`](https://hirelayer.co/api-docs/errors.md#invalid_request) | `Send the body as multipart/form-data, with the file in the part file.` | The body is not `multipart/form-data`. | Fix, then retry |
| `400` | [`invalid_request`](https://hirelayer.co/api-docs/errors.md#invalid_request) | `Unknown field resume.` | `param`: `resume`. A part the call does not take. | Fix, then retry |
| `400` | [`invalid_request`](https://hirelayer.co/api-docs/errors.md#invalid_request) | `application_id cannot be score: that name is reserved for POST /api/v4/resumes/score.` | `param`: `application_id`. `application_id` is not a valid id, or is a name the paths reserve (`parse`, `score`, `search`). | Fix, then retry |
| `400` | [`invalid_request`](https://hirelayer.co/api-docs/errors.md#invalid_request) | `webhook_url must be an https URL on the standard port, without credentials.` | `param`: `webhook_url`. `webhook_url` is not a public HTTPS URL. | 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 |
| `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 call needs 1 credit and none is left. Nothing was processed or billed.` | No credit left for the parse. | 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 |
| `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 |
| `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 |
| `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": "file is required: send the resume in the multipart part file.",
    "param": "file",
    "doc_url": "https://hirelayer.co/api-docs/errors#invalid_request"
  },
  "request_id": "6f1c2a4e-1d2b-4c3a-9e8f-0a1b2c3d4e5f"
}
```

### Behavior

- **Credits:** 1. Sending a file already stored under the same id returns the stored resume with `already_stored: true` and costs 0.
- **Updating a resume:** send the new file with the resume's `application_id` (yours or a derived one such as `file_…`). The new parse replaces the stored resume, and the resume's scores become `stale`: they stay readable and are evaluated again the next time the resume is sent to `POST /resumes/score`.
- **Time:** about 25 to 35 seconds, 145 seconds at most; set a client timeout of 150 seconds. With `webhook_url`, the call answers `202` at once and the result is posted to that URL.
- **Without storage (`do_not_store_data=true`):** `application_id` is `null`, `info_resume.url` is `null`, `face_url` is valid 10 minutes. With `webhook_url`, the `202` names no `application_id` either, and the result waits until it is delivered, 24 hours at most.

## Candidate photo

Yes, the HireLayer resume parsing API also extracts the candidate’s photo. When a PDF, Word or image resume contains a photo, `resume.info_resume.face_url` links to the candidate’s face cropped from it as a separate image. There is no option to switch on and no extra credit: the photo comes with the same 1-credit parse.

- `face_url` is `null` when no face is found, and always for `.txt` files.
- A photo embedded in an HTML resume is returned the same way.
- By default the image is stored and `face_url` does not expire. With `do_not_store_data=true` (Enterprise plan) it is a temporary link valid 10 minutes, until `face_url_expires_at`: download the image if you need to keep it.

## Scanned resumes and languages

Yes, the HireLayer resume parser API handles scanned PDFs and resumes in multiple languages. A scanned PDF or a phone photo of a CV (JPG, PNG or BMP) is read with OCR and returns the same JSON as a text PDF, in any of the 70 supported languages, for the same single credit. There is no language parameter and no OCR option to switch on: send the file as it is.

- Scans take longer than the usual 25 to 35 seconds; keep a client timeout of 150 seconds.
- When no text can be read even with OCR, the call answers `422 resume_unreadable` and no credit is used.
- `info_resume.language` gives the main language of the resume as an uppercase ISO 639-1 code such as `EN`, `FR` or `DE`.
- Free-text values stay in the language of the resume, with no translation.

## 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 Job Criteria Extractor](https://hirelayer.co/api-docs/extract-criteria.md): HireLayer job description parsing API: send a job ad as text or a file and get its typed, weighted criteria back as JSON, for free.
