# HireLayer CV Extract

> Parse a resume file into structured candidate JSON with one multipart request: contact details, experience, education, languages and typed skills.

Source: https://onlineresumeparser.com/api-docs/extract · Markdown: https://onlineresumeparser.com/api-docs/extract.md

- **Endpoint:** `POST /api/v3/parser`
- **Input:** `multipart/form-data` · 13 file formats · about 4.5 MB max
- **Output:** Synchronous JSON, usually in about 35 s (145 s max)
- **Billing:** 1 credit per successful parse

## Parse a resume

`POST https://onlineresumeparser.com/api/v3/parser`

Upload one resume file and receive the structured candidate profile in the same response.

- **Authentication:** `X-API-Key` header
- **Content type:** `multipart/form-data`
- **Billing:** 1 credit per successful parse (HTTP 200)
- **Client timeout:** at least 150 seconds

### Request body

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `file` | `file` | Yes | The resume: PDF, DOC, DOCX, ODT, PPT, PPTX, ODP, XLS, RTF, TXT, JPG/JPEG, PNG or BMP. The type is detected from the content. Keep it under 4.5 MB. |
| `application_id` | `string` | No | Your own reference, echoed in `info_resume.application_id`. |
| `webhook_url` | `string (URL)` | No | HTTP(S) URL that also receives the result. The response stays synchronous. See [Webhook](#webhook). |
| `do_not_store_data` | `string` | No | `true`: the resume file is **not stored**. `info_resume.url` is then `null`, and the cropped photo is only reachable through a temporary link valid 10 minutes. Case-insensitive. Defaults to `false` (file stored). One of: `"true"`, `"false"`. |

### Example request

**cURL**

```bash
curl -X POST https://onlineresumeparser.com/api/v3/parser \
  -H "X-API-Key: $HIRELAYER_API_KEY" \
  -F "file=@resume.pdf;type=application/pdf" \
  -F "application_id=app_123"
```

**Python**

```python
import os

import requests

with open("resume.pdf", "rb") as file:
    response = requests.post(
        "https://onlineresumeparser.com/api/v3/parser",
        headers={"X-API-Key": os.environ["HIRELAYER_API_KEY"]},
        files={"file": ("resume.pdf", file, "application/pdf")},
        data={
            "application_id": "app_123",
        },
        timeout=150,
    )
response.raise_for_status()

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

**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('application_id', 'app_123')

const response = await fetch('https://onlineresumeparser.com/api/v3/parser', {
  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.info_candidate.full_name)
```

### Response `200`

Parsed resume.

| Field | Type | Description |
| --- | --- | --- |
| `status` | `string` | Always `success` on HTTP 200. Value: `"success"`. |
| `upstream_status` | `string` | May be absent. Present when an optional step was skipped (see `warnings`). The data is still usable. Value: `"partial"`. |
| `request_id` | `string` | Unique request ID. Quote it when contacting support. |
| `warnings` | `string[]` | Human-readable notes about skipped steps (OCR, photo, geocoding, occupation codes). Informational: do not parse. |
| `errors` | `string[]` | Always empty on HTTP 200. Failures use non-2xx statuses. |
| `info_resume` | `ResumeInfo` | The document. |
| `info_resume.application_id` | `string \| null` | Your `application_id`, or `null` when not sent. |
| `info_resume.date_parsing` | `string` | Parsing time in UTC, `YYYY-MM-DDTHH:mm:ss`, without a zone suffix. |
| `info_resume.language` | `string \| null` | Main language of the resume, uppercase ISO 639-1 code such as `EN` or `FR`. |
| `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. |
| `info_resume.face_url` | `string \| null` | Candidate photo cropped from the resume. `null` when no face is found and for `.txt` files. |
| `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. |
| `info_resume.text` | `string` | Full extracted text, including OCR. May end with `[EXTRACTED_HYPERLINKS]` and `[PDF_ANNOTATION_TEXTS]` sections. |
| `info_candidate` | `Candidate` | The candidate profile. |
| `info_candidate.full_name` | `string \| null` | First and last name. |
| `info_candidate.last_name` | `string \| null` | Last name. |
| `info_candidate.first_name` | `string \| null` | First name. |
| `info_candidate.email` | `string \| null` | Lower-cased email address. |
| `info_candidate.phone_number` | `string \| null` | E.164 phone number, e.g. `+33612345678`. The country is inferred from the resume, France by default. |
| `info_candidate.birth_date` | `string \| null` | Date of birth, `YYYY-MM-DD`. A year alone becomes `YYYY-01-01`. |
| `info_candidate.age` | `number \| null` | Age in years, as stated or inferred. |
| `info_candidate.availability_now` | `boolean \| null` | Whether the candidate is available now. |
| `info_candidate.availability_date` | `string \| null` | Date from which the candidate is available, `YYYY-MM-DD`. |
| `info_candidate.driver_license` | `string[]` | Driving licences as written, e.g. `Permis B`. |
| `info_candidate.job_title` | `string \| null` | Target or current job title. Falls back to the most recent position. |
| `info_candidate.education_name` | `string \| null` | Highest qualification relevant to the job title. |
| `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"`. |
| `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"`. |
| `info_candidate.linkedin_url` | `string \| null` | LinkedIn profile URL. |
| `info_candidate.github_url` | `string \| null` | GitHub profile URL. |
| `info_candidate.other_urls` | `string[]` | Other URLs in the resume (portfolio, website…). |
| `info_candidate.location` | `Location` | Where the candidate lives. |
| `info_candidate.location.country` | `string \| null` | Country name. |
| `info_candidate.location.country_code` | `string \| null` | ISO 3166-1 alpha-2 country code, e.g. `FR`. |
| `info_candidate.location.region` | `string \| null` | Region. |
| `info_candidate.location.department` | `string \| null` | Department or county. |
| `info_candidate.location.city` | `string \| null` | City. |
| `info_candidate.location.postal_code` | `string \| null` | Postal code. |
| `info_candidate.location.full_address` | `string \| null` | Address as a single line. |
| `info_candidate.location.latitude` | `number \| null` | Latitude from geocoding. |
| `info_candidate.location.longitude` | `number \| null` | Longitude from geocoding. |
| `info_candidate.mobility` | `Mobility` | Where else the candidate can work. |
| `info_candidate.mobility.can_work_in_other_cities` | `boolean \| null` | Whether the resume says the candidate can work elsewhere. `null` when not mentioned. |
| `info_candidate.mobility.other_cities` | `MobilityCity[]` | Cities explicitly named as possible work locations. |
| `info_candidate.mobility.other_cities[].city` | `string \| null` | City named in the resume. |
| `info_candidate.mobility.other_cities[].country` | `string \| null` | Country, when stated or reliably inferred. |
| `info_candidate.mobility.other_cities[].postal_code` | `string \| null` | Postal code, when present in the resume. |
| `info_candidate.mobility.other_cities[].latitude` | `number \| null` | Latitude from geocoding. |
| `info_candidate.mobility.other_cities[].longitude` | `number \| null` | Longitude from geocoding. |
| `work_experiences` | `WorkExperience[]` |  |
| `work_experiences[].company_name` | `string \| null` | Employer name. |
| `work_experiences[].job_title` | `string \| null` | Job title for this position. |
| `work_experiences[].description` | `string \| null` | Tasks, responsibilities and achievements. |
| `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"`. |
| `work_experiences[].start_date` | `string \| null` | Start date, `YYYY-MM-DD`. A year alone becomes `YYYY-01-01`; a month alone, its first day. |
| `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. |
| `work_experiences[].currently_active` | `boolean \| null` | Whether this is a current position. |
| `work_experiences[].work_experience_country` | `string \| null` | Country of the position. |
| `work_experiences[].work_experience_country_code` | `string \| null` | ISO 3166-1 alpha-2 country code. |
| `work_experiences[].work_experience_city` | `string \| null` | City of the position. |
| `work_experiences[].work_experience_postal_code` | `string \| null` | Postal code of the position. |
| `work_experiences[].experience_duration` | `number \| null` | Duration of this position in months, as extracted. |
| `educations` | `Education[]` |  |
| `educations[].degree_title` | `string \| null` | Name of the degree or programme. |
| `educations[].school_name` | `string \| null` | School or institution. |
| `educations[].description` | `string \| null` | Details of the programme. |
| `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`. |
| `educations[].start_date` | `string \| null` | Start date, `YYYY-MM-DD`. |
| `educations[].end_date` | `string \| null` | End date, `YYYY-MM-DD`. |
| `educations[].currently_active` | `boolean \| null` | Whether the programme is in progress. |
| `educations[].location` | `EducationLocation` | Location of the school. |
| `educations[].location.country` | `string \| null` | Country name. |
| `educations[].location.country_code` | `string \| null` | ISO 3166-1 alpha-2 country code, e.g. `FR`. |
| `educations[].location.region` | `string \| null` | Region. |
| `educations[].location.department` | `string \| null` | Department or county. |
| `educations[].location.city` | `string \| null` | City. |
| `educations[].location.postal_code` | `string \| null` | Postal code. |
| `educations[].location.full_address` | `string \| null` | Address as a single line. |
| `rome_jobs` | `RomeJob[]` | Occupations predicted from `info_candidate.job_title` with the French ROME taxonomy. |
| `rome_jobs[].job_title` | `string` | Occupation label, in French. |
| `rome_jobs[].job_code` | `string` | Occupation (appellation) code. |
| `rome_jobs[].rome_title` | `string` | ROME job family label, in French. |
| `rome_jobs[].rome_code` | `string` | ROME code, e.g. `M1805`. |
| `rome_jobs[].prediction_score` | `number` | Confidence between 0.7 and 1. |
| `languages` | `Language[]` |  |
| `languages[].language` | `string` | Language name as written in the 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)"`. |
| `skills` | `Skill[]` |  |
| `skills[].skill_title` | `string` | Catalog label when `status` is `normalized`, otherwise the text from the resume. |
| `skills[].skill_type` | `string` | One of: `"Hard skill"`, `"Soft skill"`, `"Software skill"`. |
| `skills[].status` | `string` | `normalized`: matched to the [HireLayer Skills catalog](https://onlineresumeparser.com/api-docs/skills.md). `raw`: no close catalog match. One of: `"normalized"`, `"raw"`. |
| `skills[].domain` | `string \| null` | Catalog domain. `null` when `status` is `raw`. |
| `skills[].subcategory` | `string \| null` | Catalog subcategory. `null` when `status` is `raw`. |
| `certifications` | `string[]` |  |
| `interests` | `string[]` |  |

```json
{
  "status": "success",
  "request_id": "6f1c2a9e-4b7d-4c3e-9a51-2f8d7e6b1c04",
  "warnings": [],
  "errors": [],
  "info_resume": {
    "application_id": "app_123",
    "date_parsing": "2026-10-01T09:30:12",
    "language": "EN",
    "url": "https://files.example.com/original/1790847012000-alex-morgan.pdf",
    "face_url": "https://files.example.com/faces/1790847012000-alex-morgan.jpg",
    "face_url_expires_at": null,
    "text": "Alex Morgan\nSenior Software Engineer\nParis, France\nalex.morgan@example.com\n\nSenior software engineer with 8 years of experience building web platforms…"
  },
  "info_candidate": {
    "full_name": "Alex Morgan",
    "last_name": "Morgan",
    "first_name": "Alex",
    "email": "alex.morgan@example.com",
    "phone_number": "+33612345678",
    "birth_date": "1992-04-18",
    "age": 34,
    "availability_now": false,
    "availability_date": "2026-11-01",
    "driver_license": [
      "Permis B"
    ],
    "job_title": "Senior Software Engineer",
    "education_name": "Master of Science in Computer Science",
    "education_level": "Level 7",
    "experience_level": "5 to 10 years",
    "linkedin_url": "https://www.linkedin.com/in/alex-morgan",
    "github_url": "https://github.com/alexmorgan",
    "other_urls": [
      "https://alexmorgan.dev"
    ],
    "location": {
      "country": "France",
      "country_code": "FR",
      "region": "Île-de-France",
      "department": "Paris",
      "city": "Paris",
      "postal_code": "75011",
      "full_address": "75011 Paris, France",
      "latitude": 48.8589,
      "longitude": 2.3801
    },
    "mobility": {
      "can_work_in_other_cities": true,
      "other_cities": [
        {
          "city": "Lyon",
          "country": "France",
          "postal_code": null,
          "latitude": 45.764,
          "longitude": 4.8357
        }
      ]
    }
  },
  "work_experiences": [
    {
      "company_name": "Northstar Labs",
      "job_title": "Senior Software Engineer",
      "description": "Lead a team of five engineers building a TypeScript and React SaaS platform.",
      "contract_type": "Permanent contract",
      "start_date": "2022-03-01",
      "end_date": null,
      "currently_active": true,
      "work_experience_country": "France",
      "work_experience_country_code": "FR",
      "work_experience_city": "Paris",
      "work_experience_postal_code": null,
      "experience_duration": 55
    },
    {
      "company_name": "Atelier Digital",
      "job_title": "Software Engineer",
      "description": "Built Node.js APIs and data pipelines for recruitment clients.",
      "contract_type": "Permanent contract",
      "start_date": "2018-09-01",
      "end_date": "2022-02-28",
      "currently_active": false,
      "work_experience_country": "France",
      "work_experience_country_code": "FR",
      "work_experience_city": "Paris",
      "work_experience_postal_code": null,
      "experience_duration": 42
    }
  ],
  "educations": [
    {
      "degree_title": "Master of Science in Computer Science",
      "school_name": "École Polytechnique",
      "description": "Distributed systems and software architecture.",
      "degree_type": "Level 7",
      "start_date": "2014-09-01",
      "end_date": "2016-06-30",
      "currently_active": false,
      "location": {
        "country": "France",
        "country_code": "FR",
        "region": "Île-de-France",
        "department": "Essonne",
        "city": "Palaiseau",
        "postal_code": "91120",
        "full_address": "Palaiseau, France"
      }
    }
  ],
  "rome_jobs": [
    {
      "job_title": "Ingénieur / Ingénieure logiciel",
      "job_code": "38971",
      "rome_title": "Études et développement informatique",
      "rome_code": "M1805",
      "prediction_score": 0.93
    }
  ],
  "languages": [
    {
      "language": "English",
      "level": "Native or Bilingual (C2)"
    },
    {
      "language": "French",
      "level": "Professional Working Proficiency (B2)"
    }
  ],
  "skills": [
    {
      "skill_title": "React",
      "skill_type": "Hard skill",
      "status": "normalized",
      "domain": "Technologie",
      "subcategory": "Languages & Frameworks"
    },
    {
      "skill_title": "Typescript",
      "skill_type": "Hard skill",
      "status": "normalized",
      "domain": "Technologie",
      "subcategory": "Languages & Frameworks"
    },
    {
      "skill_title": "Figma",
      "skill_type": "Software skill",
      "status": "normalized",
      "domain": "Design & Contenu",
      "subcategory": "Logiciel"
    },
    {
      "skill_title": "Technical leadership",
      "skill_type": "Soft skill",
      "status": "raw",
      "domain": null,
      "subcategory": null
    }
  ],
  "certifications": [
    "AWS Certified Developer – Associate"
  ],
  "interests": [
    "Open-source software",
    "Climbing"
  ]
}
```

### Errors

| Status | `error` | `code` | When | Retry |
| --- | --- | --- | --- | --- |
| `400` | `Missing required file field` | — | No multipart part named `file`. | Do not retry |
| `400` | `application_id must be a string when provided` | — | `application_id` was sent as a file part. | Do not retry |
| `400` | `The uploaded file is empty or invalid. Please check the file and try again.` | `INVALID_FILE` | Empty file, unsupported format, content that does not match its type, or a `do_not_store_data` value other than `true`/`false`. | Do not retry |
| `401` | `Missing API Key` | — | The `X-API-Key` header is absent. `Authorization: Bearer` is not accepted. | Do not retry |
| `401` | `Invalid API Key` | — | The key is malformed, unknown or revoked. Rotating a key revokes the previous one immediately. | Do not retry |
| `403` | `Insufficient credits available` | — | The account has no credit left. | Fix, then retry |
| `413` | `The uploaded file is too large to process. Please upload a smaller file.` | — | The encoded upload exceeds 6 MiB (a file of about 4.5 MB). | Do not retry |
| `415` | `Content-Type must be multipart/form-data` | — | The request is not `multipart/form-data`. | Do not retry |
| `422` | `This document does not appear to be a CV or resume. Please upload a CV or resume and try again.` | `DOCUMENT_NOT_A_RESUME` | The document is clearly not a resume (cover letter, ID, invoice…). | Do not retry |
| `422` | `The text could not be extracted from this document. Please verify that the file is readable and contains selectable text.` | `DOCUMENT_TEXT_EMPTY` | No text was found, even with OCR. | Do not retry |
| `422` | `The document could not be read. Please upload a valid, readable file.` | `DOCUMENT_UNREADABLE` | The file is corrupted or cannot be opened. | Do not retry |
| `422` | `This document contains too much text to process. Please try a shorter or simpler version.` | `DOCUMENT_TOO_LARGE` | More than 100,000 characters of text were extracted. | Do not retry |
| `500` | `Internal Server Error` | — | Unexpected failure. | Retry with backoff |
| `502` | `An error occurred while processing the document or extracting its text. Please try again later.` | `PARSER_UNAVAILABLE` | The parser failed or returned an invalid result. | Retry with backoff |
| `503` | `An error occurred while processing the document or extracting its text. Please try again later.` | `PARSER_UNAVAILABLE` | A processing step (text extraction, OCR, model) is temporarily unavailable. Headers: `Retry-After: 1`. | Retry with backoff |
| `503` | `Credit service temporarily unavailable` | — | Credits could not be checked or deducted. No credit is consumed, including when the operation itself had completed. Headers: `Retry-After: 1`. | Retry with backoff |
| `504` | `An error occurred while processing the document or extracting its text. Please try again later.` | `PARSER_UNAVAILABLE` | Parsing did not finish within 145 seconds. | Retry with backoff |

Example error (`400`):

```json
{
  "error": "The uploaded file is empty or invalid. Please check the file and try again.",
  "code": "INVALID_FILE"
}
```

### Behavior

- **Latency:** Synchronous. Parsing usually takes about 35 seconds; scans that need OCR take longer. The gateway waits up to 145 seconds, then returns `504`. Use a client timeout of at least 150 seconds.
- **Retries:** Retry `502`, `503` and `504` with exponential backoff and honour `Retry-After`. Never retry `4xx` unchanged. Failed requests are not charged.
- **Idempotency:** There is no idempotency key. A request your client abandons can still complete and be charged: do not use a client timeout shorter than the gateway timeout.
- **Partial results:** When an optional step is skipped (OCR of some pages, photo, geocoding, occupation codes), the response is still `200` with `upstream_status: "partial"` and a note in `warnings`.
- **Documents:** 13 formats (PDF, DOC, DOCX, ODT, PPT, PPTX, ODP, XLS, RTF, TXT, JPG/JPEG, PNG and BMP). All pages are read; scanned documents are OCR'd on their first 4 pages. Up to 100,000 extracted characters.
- **Languages:** Free-text values stay in the language of the resume (no translation). Enumerated fields use the fixed English values of the response schema. `rome_jobs` labels are in French.
- **Request ID:** Successful responses carry `request_id`; error responses carry the `x-parser-request-id` header. Quote them when contacting support.

> **Beta: Fast mode (beta).** A faster parsing mode is in beta and is enabled per account on request: [contact us](https://cal.com/resumeparser/demo-resume-parser). It has no request parameter, and its timing is not published yet.

## Webhook

When `webhook_url` is set, the result is also posted there as JSON, before the HTTP response is returned. Use it only as a convenience: the HTTP response remains the source of truth.

- Sent only for successful parses, with `Content-Type: application/json` and no signature header. Add a secret token to the URL and check it on receipt.
- Payload: the parsed resume. Unlike the HTTP response, `status` is `"partial"` when some steps were skipped, and `upstream_status` is absent.
- 2-second timeout per attempt, up to 3 attempts on timeouts, network errors, `429` and `5xx`. A webhook failure does not change the HTTP response.
- The webhook can arrive even when the HTTP request ends with an error (for example `403` when credits run out during the call). Reconcile with your own `application_id`.

## Data storage

With `do_not_store_data=true`, HireLayer does **not store the resume file**. The parsed data is returned in the response as usual.

|  | `do_not_store_data=false` (default) | `do_not_store_data=true` |
| --- | --- | --- |
| Original file | Stored. `info_resume.url` links to it. | **Not stored.** `info_resume.url` is `null`. |
| Candidate photo | Stored. `face_url` does not expire. | Temporary link valid 10 minutes (`face_url_expires_at`). |
| Parsed data | Returned in the response. | Returned in the response. |

> **Warning: Stored files are reachable by URL.** Anyone with `info_resume.url` or a non-expiring `face_url` can open the file. Treat these URLs as personal data, or send `do_not_store_data=true` and keep your own copy.

## Legacy V2

Existing integrations can keep using the asynchronous [V2 contract](https://onlineresumeparser.com/api-docs/extract-v2.md). New integrations must use V3.

## Next

- [HireLayer Job Extract](https://onlineresumeparser.com/api-docs/job-extract.md): Extract explicit, weighted and mandatory criteria from a job description, ready to send to HireLayer Match.
- [Build a screening pipeline](https://onlineresumeparser.com/api-docs/screening-pipeline.md): Chain HireLayer CV Extract, Job Extract, Rank and Match to parse resumes, score them against a job and explain the result.
