# HireLayer API — full documentation > HireLayer provides five recruiting REST APIs behind one API key: Extract (resume file → structured candidate JSON), Job Extract (job description → weighted criteria), Match (one candidate vs. one job, criterion by criterion), Rank (up to 10 candidates for one job) and Skills (free-text skills → a taxonomy of 11,061 skills). Every call is synchronous (resume parsing can also answer asynchronously for automation platforms); each successful call costs 1 credit. Documentation version: 2026-10-01. Generated from the same source as https://hirelayer.co/api-docs. ## About HireLayer - HireLayer, formerly OnlineResumeParser, is a French company that publishes five recruiting APIs behind one API key: resume parsing (CV to JSON), job description parsing, candidate matching, candidate ranking and skills normalization. - HireLayer at hirelayer.co sells recruiting APIs to software companies, staffing firms and HR teams. It is not the HireLayer job-search app for candidates found at hirelayer.ai and in the app stores, which is a separate company. - Website: https://hirelayer.co. In production since 2024, with more than 1 million API requests processed and a parse success rate of about 99.7% on valid CVs. - Pricing (https://hirelayer.co/pricing): Free €0/month for 50 credits, Starter €24/month for 500 credits, Scale €89/month for 2,000 credits, Enterprise on quote. One-time credit packs: €29 for 500, €99 for 2,000, €449 for 10,000. 1 successful API call = 1 credit across every API. Calls that return an error use no credit. The Free plan (50 credits a month) needs no card. - Data: resumes are parsed in Paris, France, where files and extracted photos are stored; AI processing stays in the EU. With do_not_store_data=true the file is not stored. - Customers: Wink, an ATS used by 500+ companies, runs all five HireLayer APIs in its product. - Access: REST API with an X-API-Key header, an official MCP server for Claude, ChatGPT, Cursor and VS Code, and integrations for n8n, Make, Power Automate, Google Sheets and several ATS. ## Website - [Home](https://hirelayer.co/): overview of the five APIs, pricing and FAQ - [Pricing](https://hirelayer.co/pricing): plans, credit packs, cost per call and a cost comparison with other resume parsing vendors - [About HireLayer](https://hirelayer.co/about): company, where data is processed, customers and contact - [Wink customer story](https://hirelayer.co/customers/wink): how an ATS used by 500+ companies runs HireLayer - [MCP server](https://hirelayer.co/mcp): connect HireLayer to Claude, ChatGPT, Cursor or VS Code ## Products - [HireLayer CV Extract (Resume Parsing API)](https://hirelayer.co/products/resume-parsing-api): Parse PDF, DOCX and image resumes into structured JSON with one API call. Extract contact details, experience, education, CEFR language levels and typed skills. - [HireLayer Job Extract (Job Parsing API)](https://hirelayer.co/products/job-parsing-api): Parse job descriptions into weighted, matchable criteria. Each requirement gets an id, a 1–3 weight, a mandatory flag and a rationale for candidate matching. - [HireLayer Match (Candidate & Job Matching API)](https://hirelayer.co/products/candidate-job-matching-api): Match a candidate to a job with explainable results. Each criterion gets an ideal, potential, not_mentioned or not_valid status, an explanation and a weighted score. - [HireLayer Skills (Skills Matching API)](https://hirelayer.co/products/skills-matching-api): Normalize free-text skills with a Skills Matching API. Resolve a skill or a whole CV skills section to 11,061 taxonomy skills with stable ids, families and domains. - [HireLayer Rank (Candidate Ranking API)](https://hirelayer.co/products/candidate-ranking-api): Rank up to 10 candidates against one job description in a single API call. Get a 1-to-n ranking, a 0–1 score and a rationale for every candidate. ## Comparisons - [Resume parsing API comparison hub](https://hirelayer.co/compare): HireLayer compared with Affinda, RChilli, Textkernel, HrFlow.ai and DaXtra - [Affinda Alternative: HireLayer vs Affinda Resume Parser](https://hirelayer.co/compare/hirelayer-vs-affinda): HireLayer vs Affinda, checked October 2026: US$0.10 vs €0.045–0.058 per parsed CV, formats, page limits, job parsing, matching, self-hosting and a field-by-field migration map. - [RChilli Alternative & Pricing: HireLayer vs RChilli API](https://hirelayer.co/compare/hirelayer-vs-rchilli): HireLayer vs RChilli, checked October 2026: RChilli credit pricing ($75 for 500, +1 credit for OCR) vs HireLayer’s flat credit, API design, bulk parsing, matching and a field-by-field migration map. - [Textkernel & Sovren Alternative: HireLayer vs Tx Platform](https://hirelayer.co/compare/hirelayer-vs-textkernel): HireLayer vs Textkernel (formerly Sovren), checked October 2026: credit add-ons, Professional plan from $99, serial-only batches, SDKs, matching limits and a Tx Platform → HireLayer field map. - [HrFlow.ai Alternative: HireLayer vs HrFlow.ai Parsing API](https://hirelayer.co/compare/hirelayer-vs-hrflow): HireLayer vs HrFlow.ai, checked October 2026: stateless parsing vs Sources and Boards, real-time parsing, ROME taxonomies, connectors, pricing transparency and a profile-to-V3 field map. - [DaXtra Alternative: HireLayer vs DaXtra Resume Parser](https://hirelayer.co/compare/hirelayer-vs-daxtra): HireLayer vs DaXtra, checked October 2026: sales-led vs self-serve, DaxML XML vs JSON, schema versions, staffing integrations, audited matching and a DaxJson field map. ## Free tools - [Free online resume parser](https://hirelayer.co/tools/resume-parser): Upload a CV and get the candidate details, work history, education, skills and languages back as a readable profile and as JSON you can copy or download. - [ATS resume checker](https://hirelayer.co/tools/ats-resume-checker): Upload a CV and see which fields a resume parser can read, what is missing or ambiguous, and how to fix it. Includes a readability score with the formula shown. - [Resume parser demo](https://hirelayer.co/demo): Turn a resume into a structured candidate profile and inspect the JSON returned by the HireLayer resume parsing API. ## Guides - [Resume Parser Benchmark: HireLayer, Textkernel, RChilli and Affinda on 5 Test Resumes](https://hirelayer.co/blog/resume-parser-benchmark): Five test resumes through HireLayer, Textkernel, RChilli and Affinda: language levels, scans, infographics, contract types and skills, with the raw JSON. - [Language Proficiency Levels on a Resume: CEFR, LinkedIn and ILR Scales](https://hirelayer.co/blog/language-proficiency-levels-resume): How to list language levels on a resume: the CEFR levels A1–C2, LinkedIn and ILR labels with their usual CEFR reading, and IELTS, TOEFL and TOEIC scores converted. - [Free Resume Parsers in 2026: Online Tools, Free APIs and Open Source](https://hirelayer.co/blog/free-resume-parsers): Every free resume parser we could verify: online tools (HireLayer, Affinda, OpenResume), free API tiers and open-source libraries such as pyresparser, with their limits. - [Job Description Parsing APIs Compared: Fields or Hiring Criteria](https://hirelayer.co/blog/job-description-parsing-api-comparison): Compare eight job description parsing APIs: field parsers (Textkernel, RChilli, HrFlow.ai, Affinda, DaXtra) and criteria extraction for matching (HireLayer). - [Skills Taxonomy APIs Compared: ESCO, O*NET, Lightcast, ROME and More](https://hirelayer.co/blog/skills-taxonomy-api-comparison): Free taxonomies (ESCO, O*NET, ROME) and skills normalization APIs (HireLayer, Textkernel, Lightcast, RChilli, HrFlow.ai) compared on size, languages, licence and price. - [Candidate Matching APIs Compared: 8 Ways to Score Resumes Against a Job](https://hirelayer.co/blog/candidate-matching-api-comparison): Compare eight candidate matching APIs on how they score resumes against a job, what they explain and what a match costs: HireLayer, Textkernel, RChilli, Affinda and more. - [MCP Servers for Recruiting in 2026: Resume Parsing, Matching and ATS](https://hirelayer.co/blog/recruiting-mcp-servers): Which MCP servers let Claude, ChatGPT or Cursor parse resumes, rank candidates or update your ATS: HireLayer, RChilli, Affinda, Greenhouse, Ashby and more. - [Best Resume Parsing Software in 2026: 6 Tools Compared](https://hirelayer.co/blog/best-resume-parsing-software): Compare six resume parsing software vendors on price, output, connectors and security: HireLayer, Affinda, RChilli, Textkernel, DaXtra and HrFlow.ai. - [What Is a Resume Parser? How Resume Parsing Works, With an Example](https://hirelayer.co/blog/what-is-a-resume-parser): A resume parser turns a CV file into structured fields. See a real CV-to-JSON example, how parsing works step by step, why it fails and how to choose one. - [How to Parse a Resume to JSON in PHP with Guzzle or Laravel](https://hirelayer.co/blog/parse-resume-to-json-php): Send a CV to a resume parser API from PHP 8.2, map the JSON to readonly classes, handle every documented error code and retry only what is safe. - [How to Parse a Resume to JSON in C# with HttpClient (.NET 8)](https://hirelayer.co/blog/parse-resume-to-json-csharp): Upload a CV to a resume parser API with HttpClient and MultipartFormDataContent, map the JSON to C# records with System.Text.Json, and retry only what is safe. - [How to Parse a Resume to JSON in Java with java.net.http](https://hirelayer.co/blog/parse-resume-to-json-java): Send a CV to a resume parser API with the JDK HttpClient and a hand-built multipart body, map the JSON to Java records with Jackson, and retry only what is safe. - [EU AI Act Checklist for AI Resume Screening and Candidate Ranking](https://hirelayer.co/blog/eu-ai-act-recruitment-checklist): Key dates after the Digital Omnibus, how to tell whether a recruiting tool is high-risk, and what vendors and employers must have in place by 2 December 2027. - [How to Parse a Resume PDF into JSON with Python or Node.js](https://hirelayer.co/blog/resume-pdf-to-json-api): Compare local PDF extraction, OCR, and a resume parser API, then follow working Python and Node.js examples for turning a resume PDF into structured JSON. - [How to Integrate a Resume Parser API: Requests, Mapping and Errors](https://hirelayer.co/blog/resume-parser-api-integration-guide): Step-by-step resume parser API integration: send a file from your backend, map the JSON into your candidate model, handle errors and test before launch. - [Bulk Resume Parsing at Scale: Build a Reliable API Pipeline](https://hirelayer.co/blog/bulk-resume-parsing-api): Design a safe, observable pipeline for thousands of resumes with a one-file-per-request parser API: queues, retries, deduplication and cost controls. - [Resume Parser API Pricing: Compare Cost per 1,000 CVs](https://hirelayer.co/blog/resume-parser-api-pricing): Compare published resume parsing API price models and calculate HireLayer’s effective cost per 1,000 successful calls from current plans and credit packs. - [Resume Parser API Comparison: A Fair Benchmark for ATS Teams](https://hirelayer.co/blog/resume-parser-api-comparison): Compare public resume parser API specifications, then use a reproducible test plan to measure extraction quality, latency, failures, and cost on your own CVs. - [Resume Parsing Accuracy: How to Measure and Improve It With an API](https://hirelayer.co/blog/resume-parsing-accuracy): Measure resume parsing accuracy field by field, build a labeled test set from your own CVs, find what causes errors, and catch them in production. ## HireLayer integration rules - Base URL: `https://hirelayer.co/api`. HTTPS only. There is no official SDK: in code, call the REST API directly (Python `requests`, Node.js 18+ `fetch`); working examples for every API are in https://github.com/HireLayer/resume-parser-examples. The official MCP server (hosted at https://hirelayer.co/mcp with OAuth sign-in, or locally with npm `hirelayer-mcp`; source: https://github.com/HireLayer/hirelayer-mcp) is for AI assistants such as Claude, ChatGPT, Cursor or VS Code, not for application code. - Authentication: header `X-API-Key` on every request except `GET /api/v1/health`. Read the key from the `HIRELAYER_API_KEY` environment variable. Never hard-code it, log it, or send it from browser or mobile code. `Authorization: Bearer` is not supported. - Every call is synchronous; there are no jobs to poll. Only for platforms that cannot wait (Zapier), `POST /api/v3/parser` accepts `Prefer: respond-async` with a `webhook_url` and answers 202, then posts the outcome to the webhook. - Timeouts: use a client timeout of at least 150 s for `POST /api/v3/parser` and 65 s for `/api/v1/*`. - Retries: retry 5xx responses and connection failures at most 3 times with exponential backoff and jitter; honour `Retry-After`. Never retry 4xx responses unchanged. There is no idempotency key. - Errors: every error body has an `error` string. CV Extract errors add a `code` (`INVALID_MULTIPART_BODY`, `INVALID_FILE`, `DOCUMENT_NOT_A_RESUME`, `DOCUMENT_TEXT_EMPTY`, `DOCUMENT_UNREADABLE`, `DOCUMENT_TOO_LARGE`, `PARSER_UNAVAILABLE`). Job Extract, Match, Rank and Skills validation errors are `{"success": false, "error": "…"}` with status 400. - Credits: each successful call costs 1 credit, whatever the API. `403` means no credit left. - JSON bodies: Job Extract, Match, Rank and Skills reject unknown fields with 400. Job Extract, Match and Rank text fields are trimmed and limited to 50,000 characters. Skills (`POST /api/v1/skills/resolve`) takes one `text` (≤ 5,000 characters, a single skill or a whole skills section) and an optional `language` (`fr` by default, or `en`); store the returned skill `id`s, not the names. - CV Extract: send the file as the multipart field `file` (about 4.5 MB max). On 200, `status` is always `"success"`; `upstream_status: "partial"` means optional steps were skipped. `info_resume.text` can reach 100,000 characters: truncate it to 50,000 before sending it to Match or Rank. Send `do_not_store_data=true` when the resume file must not be stored: HireLayer then does not store it and `info_resume.url` is `null`. - Generated text (Job Extract labels and rationales, Match summaries and explanations, Rank rationales) is in French. - Do not invent fields, endpoints or parameters. When unsure, read the OpenAPI document or the Markdown reference. ## Endpoints - `POST /api/v3/parser` — HireLayer CV Extract: Upload one resume file and receive the structured candidate profile in the same response. (multipart/form-data, client timeout ≥ 150 s). Docs: https://hirelayer.co/api-docs/extract.md - `POST /api/v1/jobs/extract-criteria` — HireLayer Job Extract: Turn a job description into weighted criteria that can be checked against a resume. (JSON body, client timeout ≥ 65 s). Docs: https://hirelayer.co/api-docs/job-extract.md - `POST /api/v1/matching/job-candidate` — HireLayer Match: Evaluate one resume against each job criterion and get an explained 0–1 score. (JSON body, client timeout ≥ 65 s). Docs: https://hirelayer.co/api-docs/match.md - `POST /api/v1/matching/job-candidates/rank` — HireLayer Rank: Order up to 10 candidates for one job description, with a score and a rationale for each. (JSON body, client timeout ≥ 65 s). Docs: https://hirelayer.co/api-docs/rank.md - `POST /api/v1/skills/resolve` — HireLayer Skills: Turn free text, from one skill to a whole skills section, into taxonomy skills with their families and domains. (JSON body, client timeout ≥ 65 s). Docs: https://hirelayer.co/api-docs/skills.md - `GET /api/v1/skills` — HireLayer Skills: Download every skill of the taxonomy with its families, domains and broader skill. (no body, client timeout ≥ 65 s). Docs: https://hirelayer.co/api-docs/skills.md - `GET /api/v1/health` — Platform: Check that the Job Extract, Match, Skills and Rank service is up. No API key needed. (no body, client timeout ≥ 10 s, no API key). Docs: https://hirelayer.co/api-docs/health.md - `GET /api/v1/me` — Platform: Check that an API key is valid and see the account and credit balance it belongs to. Integrations use it to test a connection. (no body, client timeout ≥ 10 s). Docs: https://hirelayer.co/api-docs/health.md - `POST /api/v2/parser` — HireLayer CV Extract: Legacy asynchronous contract: the request is accepted with `202` and the result is posted to `webhook_url`. (multipart/form-data, client timeout ≥ 50 s, LEGACY, do not use for new code). Docs: https://hirelayer.co/api-docs/extract-v2.md ## References - Index for agents: https://hirelayer.co/llms.txt - Full documentation in one file: https://hirelayer.co/llms-full.txt - OpenAPI 3.1: https://hirelayer.co/openapi.json # Page: HireLayer API documentation > Five recruiting APIs behind one API key: parse resumes, extract job criteria, match and rank candidates, and normalize skills. Source: https://hirelayer.co/api-docs · Markdown: https://hirelayer.co/api-docs.md - [HireLayer CV Extract](https://hirelayer.co/api-docs/extract.md): Resume file → structured candidate JSON - [HireLayer Job Extract](https://hirelayer.co/api-docs/job-extract.md): Job description → weighted criteria - [HireLayer Match](https://hirelayer.co/api-docs/match.md): One candidate vs. one job, criterion by criterion - [HireLayer Rank](https://hirelayer.co/api-docs/rank.md): Up to 10 candidates ranked for one job - [HireLayer Skills](https://hirelayer.co/api-docs/skills.md): Free-text skills → skills taxonomy ## Start here - [Quickstart](https://hirelayer.co/api-docs/quickstart.md): Get a key and parse your first resume in five minutes. - [Screening pipeline](https://hirelayer.co/api-docs/screening-pipeline.md): Parse, extract criteria, rank and explain, end to end. - [Build with AI agents](https://hirelayer.co/api-docs/ai-agents.md): llms.txt, Markdown pages, OpenAPI and ready-made agent rules. - [Errors and retries](https://hirelayer.co/api-docs/errors.md): Every status code, error message and retry rule. ## Basics - **Base URL:** `https://hirelayer.co/api` - **Authentication:** `X-API-Key` header on every call except the health check - **Format:** JSON in and out; CV Extract takes a `multipart/form-data` file upload - **Versions:** Per product, in the path: `/v3` for CV Extract, `/v1` for the others - **Billing:** 1 successful API call = 1 credit across every API. Failed calls are free. - **Execution:** Synchronous: every result comes back in the HTTP response - **SDK:** None needed: plain HTTPS from any language ## All endpoints | Method | Path | Description | Reference | | --- | --- | --- | --- | | `POST` | `/api/v3/parser` | Upload one resume file and receive the structured candidate profile in the same response. | [CV Extract](https://hirelayer.co/api-docs/extract.md#parse-resume) | | `POST` | `/api/v1/jobs/extract-criteria` | Turn a job description into weighted criteria that can be checked against a resume. | [Job Extract](https://hirelayer.co/api-docs/job-extract.md#extract-criteria) | | `POST` | `/api/v1/matching/job-candidate` | Evaluate one resume against each job criterion and get an explained 0–1 score. | [Match](https://hirelayer.co/api-docs/match.md#match-job-candidate) | | `POST` | `/api/v1/matching/job-candidates/rank` | Order up to 10 candidates for one job description, with a score and a rationale for each. | [Rank](https://hirelayer.co/api-docs/rank.md#rank-job-candidates) | | `POST` | `/api/v1/skills/resolve` | Turn free text, from one skill to a whole skills section, into taxonomy skills with their families and domains. | [Skills](https://hirelayer.co/api-docs/skills.md#skills-resolve) | | `GET` | `/api/v1/skills` | Download every skill of the taxonomy with its families, domains and broader skill. | [Skills](https://hirelayer.co/api-docs/skills.md#skills-catalog) | | `GET` | `/api/v1/health` | Check that the Job Extract, Match, Skills and Rank service is up. No API key needed. | [Health check](https://hirelayer.co/api-docs/health.md#health) | | `GET` | `/api/v1/me` | Check that an API key is valid and see the account and credit balance it belongs to. Integrations use it to test a connection. | [Health check](https://hirelayer.co/api-docs/health.md#me) | | `POST` | `/api/v2/parser` | Legacy asynchronous contract: the request is accepted with `202` and the result is posted to `webhook_url`. (legacy) | [CV Extract V2](https://hirelayer.co/api-docs/extract-v2.md#parse-resume-v2) | ## Machine-readable docs Every page is also available as Markdown by adding `.md` to its URL. [`/llms.txt`](https://hirelayer.co/llms.txt) indexes them, [`/llms-full.txt`](https://hirelayer.co/llms-full.txt) contains all of them, and [`/openapi.json`](https://hirelayer.co/openapi.json) describes every endpoint. All are generated from the same source as these pages. ## Next - [Quickstart](https://hirelayer.co/api-docs/quickstart.md): Create an API key, check connectivity and parse a resume with cURL, Python or TypeScript in about five minutes. - [HireLayer CV Extract](https://hirelayer.co/api-docs/extract.md): Parse a resume file into structured candidate JSON with one multipart request: contact details, experience, education, languages and typed skills. --- # Page: Quickstart > Create an API key, check connectivity and parse a resume with cURL, Python or TypeScript in about five minutes. Source: https://hirelayer.co/api-docs/quickstart · Markdown: https://hirelayer.co/api-docs/quickstart.md ### 1. Create an API key [Sign up](https://hirelayer.co/auth/signup) (the Free plan includes 50 credits a month), then create a key in [Dashboard → API keys](https://hirelayer.co/dashboard/api-keys). Store it in an environment variable on your server: ```bash export HIRELAYER_API_KEY="sk_xxxxxx_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" ``` ### 2. Check connectivity The health check needs no key and costs nothing: **cURL** ```bash curl https://hirelayer.co/api/v1/health ``` **Python** ```python import requests response = requests.get( "https://hirelayer.co/api/v1/health", timeout=10, ) response.raise_for_status() data = response.json() print(data["status"]) ``` **TypeScript** ```typescript const response = await fetch('https://hirelayer.co/api/v1/health', { signal: AbortSignal.timeout(10_000), }) if (!response.ok) { throw new Error(`HireLayer ${response.status}: ${await response.text()}`) } const data = await response.json() console.log(data.status) ``` **Response** ```json { "status": "healthy", "service": "HireLayer API", "timestamp": "2026-10-01T09:30:12.417Z" } ``` ### 3. Parse a resume Send any resume file as `file`. Python needs `pip install requests`; TypeScript runs on Node.js 18+ with no dependency: save it as `parse.mts` and run `npx tsx parse.mts`. **cURL** ```bash curl -X POST https://hirelayer.co/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://hirelayer.co/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://hirelayer.co/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) ``` The call is synchronous and usually takes about 35 seconds. A successful parse costs 1 credit. ### 4. Read the result **Response (excerpt)** ```json { "status": "success", "request_id": "6f1c2a9e-4b7d-4c3e-9a51-2f8d7e6b1c04", "info_candidate": { "full_name": "Alex Morgan", "email": "alex.morgan@example.com", "job_title": "Senior Software Engineer", "experience_level": "5 to 10 years" }, "work_experiences": […], "skills": […] } ``` Every field is described in the [CV Extract reference](https://hirelayer.co/api-docs/extract.md#parse-resume-response). Errors return a non-2xx status with an `error` message: see [Errors and retries](https://hirelayer.co/api-docs/errors.md). ## Next steps - [Screening pipeline](https://hirelayer.co/api-docs/screening-pipeline.md): Chain Extract, Job Extract, Rank and Match. - [Build with AI agents](https://hirelayer.co/api-docs/ai-agents.md): Let Claude Code, Codex or Cursor write the integration. - [Examples on GitHub](https://github.com/HireLayer/resume-parser-examples): cURL, Python, Node.js, PHP and C# requests for all five APIs. ## Next - [Authentication](https://hirelayer.co/api-docs/authentication.md): Authenticate every request with an X-API-Key header. Create, rotate and revoke keys from the dashboard. - [HireLayer CV Extract](https://hirelayer.co/api-docs/extract.md): Parse a resume file into structured candidate JSON with one multipart request: contact details, experience, education, languages and typed skills. --- # Page: Authentication > Authenticate every request with an X-API-Key header. Create, rotate and revoke keys from the dashboard. Source: https://hirelayer.co/api-docs/authentication · Markdown: https://hirelayer.co/api-docs/authentication.md Send your key in the `X-API-Key` header of every request. Only the [health check](https://hirelayer.co/api-docs/health.md) is public. ```http POST /api/v1/jobs/extract-criteria HTTP/1.1 Host: hirelayer.co X-API-Key: sk_xxxxxx_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx Content-Type: application/json ``` - `Authorization: Bearer …` and keys in query strings are not accepted. - Keys look like `sk_` + 6 hex characters + `_` + 32 hex characters. A malformed key is rejected as invalid. - All keys of an account share its credit balance and work with every API. ## Manage keys Create, name, rotate and revoke keys in [Dashboard → API keys](https://hirelayer.co/dashboard/api-keys). > **Warning: Rotation revokes the old key immediately.** There is no grace period. To rotate without downtime, create a second key, deploy it, then revoke the first one. ## Keep keys secret - Call HireLayer from your backend. Never ship a key in browser, mobile or desktop code. - Read it from an environment variable such as `HIRELAYER_API_KEY` and keep it out of version control. - Use one key per environment or service, so you can revoke one without touching the others. ```env # .env — server-side only, never commit it HIRELAYER_API_KEY=sk_xxxxxx_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx ``` ## Authentication errors | Status | Body | Cause | | --- | --- | --- | | `401` | `{"error": "Missing API Key"}` | No `X-API-Key` header. | | `401` | `{"error": "Invalid API Key"}` | Malformed, unknown or revoked key. | | `403` | `{"error": "Insufficient credits available"}` | Valid key, but no credit left. See [Limits and credits](https://hirelayer.co/api-docs/limits.md). | ## Next - [Errors and retries](https://hirelayer.co/api-docs/errors.md): Error format, every status code and message, and when to retry a HireLayer API call. - [Limits and credits](https://hirelayer.co/api-docs/limits.md): How credits are consumed, request size and batch limits, timeouts and concurrency guidance for the HireLayer APIs. --- # Page: Errors and retries > Error format, every status code and message, and when to retry a HireLayer API call. Source: https://hirelayer.co/api-docs/errors · Markdown: https://hirelayer.co/api-docs/errors.md Successful calls return `2xx`. Every error returns a non-2xx status and a JSON body with an `error` string. Branch on the HTTP status first. **Gateway errors (authentication, credits, CV Extract)** ```json { "error": "This document does not appear to be a CV or resume. Please upload a CV or resume and try again.", "code": "DOCUMENT_NOT_A_RESUME" } ``` **Service errors (Job Extract, Match, Rank, Skills)** ```json { "success": false, "error": "job_text cannot be empty" } ``` - `error` is always present and human-readable. Every exact message is listed in the catalogue below. - `code` is present on CV Extract errors: `INVALID_MULTIPART_BODY`, `INVALID_FILE`, `DOCUMENT_NOT_A_RESUME`, `DOCUMENT_TEXT_EMPTY`, `DOCUMENT_UNREADABLE`, `DOCUMENT_TOO_LARGE`, `PARSER_UNAVAILABLE`. - CV Extract errors also carry an `x-parser-request-id` header. Log it. ## Status codes | Status | Meaning | Action | | --- | --- | --- | | `400` | Invalid request: missing field, wrong type, malformed multipart body, limit exceeded. | Fix the request. Do not retry. | | `401` | Missing or invalid API key. | Fix the key. Do not retry. | | `403` | No credit left. | Add credits or upgrade, then retry. | | `404` | Unknown path or wrong method. | Check the endpoint. | | `413` | Upload too large (CV Extract). | Send a smaller file. | | `415` | Wrong `Content-Type` (CV Extract). | Send `multipart/form-data`. | | `422` | The document cannot be parsed as a resume (CV Extract). | Do not retry the same file. | | `500` | Invalid JSON body, or an unexpected failure. | Check the body, then retry with backoff. | | `502` `503` `504` | Temporary failure or timeout. | Retry with backoff; honour `Retry-After`. | ## Retry policy - Retry `5xx` responses and connection failures, at most 3 times, with exponential backoff and jitter (about 1 s, 2 s, 4 s). - When a `Retry-After` header is present, wait that many seconds. - Never retry `4xx` unchanged. `403` can succeed once credits are added. - Failed calls consume no credit. There is no idempotency key: a call your client abandons can still complete and be charged, so keep client timeouts above the gateway timeouts (150 s for CV Extract, 65 s otherwise). **Reusable client** **Python** ```python # hirelayer.py — minimal client with retries. Requires: pip install requests import mimetypes import os import random import time import requests API_BASE = "https://hirelayer.co/api" class HireLayerError(Exception): def __init__(self, status, message, code=None, request_id=None): super().__init__(f"HireLayer {status}: {message}") self.status = status self.code = code self.request_id = request_id def _delay(attempt, retry_after): if retry_after and retry_after.isdigit(): return int(retry_after) return min(30, 2**attempt) + random.random() def call(method, path, *, json=None, files=None, data=None, timeout=65, max_retries=3): """Call HireLayer. Retries 5xx and connection failures, raises HireLayerError otherwise.""" headers = {"X-API-Key": os.environ["HIRELAYER_API_KEY"]} for attempt in range(max_retries + 1): try: response = requests.request( method, API_BASE + path, headers=headers, json=json, files=files, data=data, timeout=timeout, ) except requests.ConnectionError: if attempt == max_retries: raise time.sleep(_delay(attempt, None)) continue if response.ok: return response.json() if response.status_code >= 500 and attempt < max_retries: time.sleep(_delay(attempt, response.headers.get("Retry-After"))) continue try: body = response.json() except ValueError: body = {} raise HireLayerError( response.status_code, body.get("error", response.reason), body.get("code"), response.headers.get("x-parser-request-id"), ) def parse_resume(path, application_id=None): content_type = mimetypes.guess_type(path)[0] or "application/octet-stream" with open(path, "rb") as file: content = file.read() # bytes can be re-sent on retry return call( "POST", "/v3/parser", files={"file": (os.path.basename(path), content, content_type)}, data={"application_id": application_id} if application_id else None, timeout=150, ) ``` **TypeScript** ```typescript // hirelayer.mts — minimal client with retries. Node.js 18+, run with tsx. const API_BASE = 'https://hirelayer.co/api' export class HireLayerError extends Error { status: number code?: string requestId?: string constructor(status: number, message: string, code?: string, requestId?: string) { super(`HireLayer ${status}: ${message}`) this.status = status this.code = code this.requestId = requestId } } const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)) function retryDelay(attempt: number, retryAfter: string | null) { const seconds = Number(retryAfter) if (retryAfter && Number.isInteger(seconds)) return seconds * 1000 return Math.min(30_000, 1000 * 2 ** attempt) + Math.random() * 1000 } /** Calls HireLayer. Retries 5xx responses, throws HireLayerError otherwise. */ export async function callHireLayer( path: string, options: { json?: unknown; form?: FormData; timeoutMs?: number } = {}, maxRetries = 3 ): Promise { const headers: Record = { 'X-API-Key': process.env.HIRELAYER_API_KEY!, } let body: string | FormData | undefined = options.form if (options.json !== undefined) { headers['Content-Type'] = 'application/json' body = JSON.stringify(options.json) } for (let attempt = 0; ; attempt++) { const response = await fetch(API_BASE + path, { method: body === undefined ? 'GET' : 'POST', headers, body, signal: AbortSignal.timeout(options.timeoutMs ?? 65_000), }) if (response.ok) return (await response.json()) as T if (response.status >= 500 && attempt < maxRetries) { await sleep(retryDelay(attempt, response.headers.get('Retry-After'))) continue } const payload = await response.json().catch(() => ({})) throw new HireLayerError( response.status, payload.error ?? response.statusText, payload.code, response.headers.get('x-parser-request-id') ?? undefined ) } } ``` ## Error catalogue Exact `error` messages, per endpoint. ### `POST /api/v3/parser` | Status | `error` | `code` | Retry | | --- | --- | --- | --- | | `400` | `The request body is not valid multipart/form-data. Check the boundary and the Content-Disposition header of each part.` | `INVALID_MULTIPART_BODY` | Do not retry | | `400` | `Missing required file field` | — | Do not retry | | `400` | `Prefer: respond-async requires a webhook_url field with an HTTPS URL` | — | Do not retry | | `400` | `application_id must be a string when provided` | — | Do not retry | | `400` | `The uploaded file is empty or invalid. Please check the file and try again.` | `INVALID_FILE` | Do not retry | | `401` | `Missing API Key` | — | Do not retry | | `401` | `Invalid API Key` | — | Do not retry | | `403` | `Insufficient credits available` | — | Fix, then retry | | `413` | `The uploaded file is too large to process. Please upload a smaller file.` | — | Do not retry | | `415` | `Content-Type must be 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` | 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` | Do not retry | | `422` | `The document could not be read. Please upload a valid, readable file.` | `DOCUMENT_UNREADABLE` | Do not retry | | `422` | `This document contains too much text to process. Please try a shorter or simpler version.` | `DOCUMENT_TOO_LARGE` | Do not retry | | `500` | `Internal Server Error` | — | Retry with backoff | | `502` | `An error occurred while processing the document or extracting its text. Please try again later.` | `PARSER_UNAVAILABLE` | Retry with backoff | | `503` | `An error occurred while processing the document or extracting its text. Please try again later.` | `PARSER_UNAVAILABLE` | Retry with backoff | | `503` | `Credit service temporarily unavailable` | — | Retry with backoff | | `504` | `An error occurred while processing the document or extracting its text. Please try again later.` | `PARSER_UNAVAILABLE` | Retry with backoff | ### `POST /api/v1/jobs/extract-criteria` | Status | `error` | `code` | Retry | | --- | --- | --- | --- | | `400` | `Request body must be a JSON object` | — | Do not retry | | `400` | `The request contains unsupported fields` | — | Do not retry | | `400` | `The 'job_text' field is required` | — | Do not retry | | `400` | `job_text cannot be empty` | — | Do not retry | | `400` | `job_text must be 50000 characters or less` | — | Do not retry | | `401` | `Missing API Key` | — | Do not retry | | `401` | `Invalid API Key` | — | Do not retry | | `403` | `Insufficient credits available` | — | Fix, then retry | | `500` | `Internal server error` | — | Retry with backoff | | `500` | `Internal server error. Please try again later.` | — | Retry with backoff | | `502` | `Upstream API unavailable` | — | Retry with backoff | | `502` | `The AI processing step failed. Please try again later.` | — | Retry with backoff | | `503` | `Credit service temporarily unavailable` | — | Retry with backoff | ### `POST /api/v1/matching/job-candidate` | Status | `error` | `code` | Retry | | --- | --- | --- | --- | | `400` | `Request body must be a JSON object` | — | Do not retry | | `400` | `The request contains unsupported fields` | — | Do not retry | | `400` | `The 'job_text' field is required` | — | Do not retry | | `400` | `job_text cannot be empty` | — | Do not retry | | `400` | `job_text must be 50000 characters or less` | — | Do not retry | | `400` | `The 'candidate_text' field is required` | — | Do not retry | | `400` | `candidate_text cannot be empty` | — | Do not retry | | `400` | `candidate_text must be 50000 characters or less` | — | Do not retry | | `400` | `The 'matching_criteria' field must be an array` | — | Do not retry | | `400` | `matching_criteria contains an invalid criterion` | — | Do not retry | | `401` | `Missing API Key` | — | Do not retry | | `401` | `Invalid API Key` | — | Do not retry | | `403` | `Insufficient credits available` | — | Fix, then retry | | `500` | `Internal server error` | — | Retry with backoff | | `500` | `Internal server error. Please try again later.` | — | Retry with backoff | | `502` | `Upstream API unavailable` | — | Retry with backoff | | `502` | `The AI processing step failed. Please try again later.` | — | Retry with backoff | | `503` | `Credit service temporarily unavailable` | — | Retry with backoff | ### `POST /api/v1/matching/job-candidates/rank` | Status | `error` | `code` | Retry | | --- | --- | --- | --- | | `400` | `Request body must be a JSON object` | — | Do not retry | | `400` | `The request contains unsupported fields` | — | Do not retry | | `400` | `The 'job_text' field is required` | — | Do not retry | | `400` | `job_text cannot be empty` | — | Do not retry | | `400` | `job_text must be 50000 characters or less` | — | Do not retry | | `400` | `The 'candidates' field must be an array` | — | Do not retry | | `400` | `candidates cannot be empty` | — | Do not retry | | `400` | `candidates must contain 10 candidates or fewer` | — | Do not retry | | `400` | `candidates contains an invalid candidate` | — | Do not retry | | `400` | `candidate_text must be 50000 characters or less` | — | Do not retry | | `400` | `candidates contains duplicate ids` | — | Do not retry | | `401` | `Missing API Key` | — | Do not retry | | `401` | `Invalid API Key` | — | Do not retry | | `403` | `Insufficient credits available` | — | Fix, then retry | | `500` | `Internal server error` | — | Retry with backoff | | `500` | `Internal server error. Please try again later.` | — | Retry with backoff | | `502` | `Upstream API unavailable` | — | Retry with backoff | | `502` | `The AI processing step failed. Please try again later.` | — | Retry with backoff | | `503` | `Credit service temporarily unavailable` | — | Retry with backoff | ### `POST /api/v1/skills/resolve` | Status | `error` | `code` | Retry | | --- | --- | --- | --- | | `400` | `Request body must be a JSON object` | — | Do not retry | | `400` | `Unknown field(s): unexpected_field. Accepted: 'text' and optional 'language'` | — | Do not retry | | `400` | `The 'text' field (string) is required` | — | Do not retry | | `400` | `Text cannot be empty` | — | Do not retry | | `400` | `'text' must be at most 5000 characters` | — | Do not retry | | `400` | `'language' must be 'fr' or 'en'` | — | Do not retry | | `401` | `Missing API Key` | — | Do not retry | | `401` | `Invalid API Key` | — | Do not retry | | `403` | `Insufficient credits available` | — | Fix, then retry | | `500` | `Internal server error` | — | Retry with backoff | | `500` | `Internal server error. Please try again later.` | — | Retry with backoff | | `502` | `Upstream API unavailable` | — | Retry with backoff | | `503` | `Credit service temporarily unavailable` | — | Retry with backoff | ### `GET /api/v1/skills` | Status | `error` | `code` | Retry | | --- | --- | --- | --- | | `400` | `'language' must be 'fr' or 'en'` | — | Do not retry | | `401` | `Missing API Key` | — | Do not retry | | `401` | `Invalid API Key` | — | Do not retry | | `403` | `Insufficient credits available` | — | Fix, then retry | | `500` | `Internal server error` | — | Retry with backoff | | `502` | `Upstream API unavailable` | — | Retry with backoff | | `503` | `Credit service temporarily unavailable` | — | Retry with backoff | ### `GET /api/v1/health` | Status | `error` | `code` | Retry | | --- | --- | --- | --- | | `502` | `Upstream API unavailable` | — | Retry with backoff | ### `GET /api/v1/me` | Status | `error` | `code` | Retry | | --- | --- | --- | --- | | `401` | `Missing API Key` | — | Do not retry | | `401` | `Invalid API Key` | — | Do not retry | | `500` | `Internal server error` | — | Retry with backoff | ### `POST /api/v2/parser` | Status | `error` | `code` | Retry | | --- | --- | --- | --- | | `400` | `An error occurred while processing the document or extracting its text. Please try again later.` | — | Do not retry | | `401` | `Missing API Key` | — | Do not retry | | `401` | `Invalid API Key` | — | Do not retry | | `403` | `Insufficient credits available` | — | Fix, then retry | | `415` | `Content-Type must be multipart/form-data` | — | Do not retry | | `503` | `Credit service temporarily unavailable` | — | Retry with backoff | ## Next - [Limits and credits](https://hirelayer.co/api-docs/limits.md): How credits are consumed, request size and batch limits, timeouts and concurrency guidance for the HireLayer APIs. - [Build a screening pipeline](https://hirelayer.co/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. --- # Page: Limits and credits > How credits are consumed, request size and batch limits, timeouts and concurrency guidance for the HireLayer APIs. Source: https://hirelayer.co/api-docs/limits · Markdown: https://hirelayer.co/api-docs/limits.md ## Credits - 1 successful API call = 1 credit across every API. One Rank call costs 1 credit for up to 10 candidates; one Skills call costs 1 credit for a whole text of up to 5,000 characters. - Failed calls (non-2xx) are free. The health check is free. - Exception: legacy CV Extract V2 charges when the request is accepted (`202`). - Plans: Free: 50 credits/month · Starter: 500 credits/month · Scale: 2,000 credits/month; Enterprise on request. Credit packs are available from the dashboard. - Without credits, calls return `403` with `"error": "Insufficient credits available"`. ## Request limits | Product | Limit | | --- | --- | | CV Extract | One file per call, about 4.5 MB (6 MiB encoded) · 100,000 extracted characters · OCR on the first 4 pages of scans | | Job Extract | `job_text` ≤ 50,000 characters | | Match | `job_text` and `candidate_text` ≤ 50,000 characters each · no fixed criteria limit | | Rank | 1–10 candidates · each text ≤ 50,000 characters · unique `id`s | | Skills | `text` ≤ 5,000 characters | | All JSON endpoints | Body ≤ 10 MB | ## Timeouts | Endpoint | Gateway timeout | Client timeout to use | | --- | --- | --- | | `POST /api/v3/parser` | 145 s (`504`) | ≥ 150 s | | `/api/v1/*` | 60 s (`502`) | ≥ 65 s | | `POST /api/v2/parser` (legacy) | 45 s | ≥ 50 s | ## Rate limits and concurrency No per-second rate limit is enforced, but capacity is shared. Start with a few concurrent requests, increase gradually while watching latency, and back off on `502`, `503` and `504`. For sustained high volumes or bulk imports, [contact us](https://cal.com/resumeparser/demo) first. ## Next - [Errors and retries](https://hirelayer.co/api-docs/errors.md): Error format, every status code and message, and when to retry a HireLayer API call. - [Build a screening pipeline](https://hirelayer.co/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. --- # Page: 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://hirelayer.co/api-docs/extract · Markdown: https://hirelayer.co/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://hirelayer.co/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 unless you send `Prefer: respond-async` (HTTPS URL required). See [Webhook](#webhook) and [Asynchronous mode](#async). | | `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://hirelayer.co/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://hirelayer.co/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://hirelayer.co/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` | Taxonomy skill name, in French, when `status` is `normalized`; otherwise the text from the resume. One resume skill can give several normalized skills (`Word/Excel`). | | `skills[].skill_type` | `string` | One of: `"Hard skill"`, `"Soft skill"`, `"Software skill"`. | | `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"`. | | `skills[].domain` | `string \| null` | Primary domain of the taxonomy skill, in French. `null` when `status` is `raw`. | | `skills[].subcategory` | `string \| null` | Primary family of the taxonomy skill, in French. `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": "Software skill", "status": "normalized", "domain": "Développement logiciel", "subcategory": "Développement frontend" }, { "skill_title": "TypeScript", "skill_type": "Software skill", "status": "normalized", "domain": "Développement logiciel", "subcategory": "Langages de programmation" }, { "skill_title": "Figma", "skill_type": "Software skill", "status": "normalized", "domain": "Design, Création & Médias", "subcategory": "UX/UI design" }, { "skill_title": "Leadership technique", "skill_type": "Soft skill", "status": "normalized", "domain": "Management, Projet & Stratégie", "subcategory": "Management d'équipe" }, { "skill_title": "Developer experience", "skill_type": "Hard skill", "status": "raw", "domain": null, "subcategory": null } ], "certifications": [ "AWS Certified Developer – Associate" ], "interests": [ "Open-source software", "Climbing" ] } ``` ### Response `202` Accepted in [asynchronous mode](#async): the outcome is posted to `webhook_url`. | Field | Type | Description | | --- | --- | --- | | `status` | `string` | Value: `"accepted"`. | | `application_id` | `string \| null` | Your `application_id`, or `null` when not sent. | ```json { "status": "accepted", "application_id": "app_123" } ``` ### Errors | Status | `error` | `code` | When | Retry | | --- | --- | --- | --- | --- | | `400` | `The request body is not valid multipart/form-data. Check the boundary and the Content-Disposition header of each part.` | `INVALID_MULTIPART_BODY` | The body cannot be read as `multipart/form-data`: the `boundary` is missing or does not match the body, the body is truncated, or a part has no `Content-Disposition: form-data` header with a `name`. Unquoted parameter values and `filename*` (as sent by .NET `MultipartFormDataContent`) are accepted. | Do not retry | | `400` | `Missing required file field` | — | No multipart part named `file`. | Do not retry | | `400` | `Prefer: respond-async requires a webhook_url field with an HTTPS URL` | — | The request asks for the [asynchronous mode](#async) without a valid HTTPS `webhook_url`. | 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 request body is not valid multipart/form-data. Check the boundary and the Content-Disposition header of each part.", "code": "INVALID_MULTIPART_BODY" } ``` ### 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. - **Asynchronous mode:** Send `Prefer: respond-async` with a `webhook_url` to get `202 Accepted` right away, for clients that cannot wait (Zapier actions stop after 30 seconds). See [Asynchronous mode](#async). - **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). 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`. ## Asynchronous mode Some automation platforms stop waiting after 30 seconds. Add the header `Prefer: respond-async` and a `webhook_url` (HTTPS) to receive `202 Accepted` with `Preference-Applied: respond-async`, then the outcome on the webhook. - Success: the parsed resume, exactly as described in [Webhook](#webhook). 1 credit is charged, as for a synchronous parse. - Failure: `{"status": "error", "http_status": 422, "error": "…", "code": "…", "request_id": "…", "application_id": "…"}`, with the status and message the synchronous call would have returned. Failures are not charged. - Errors found in the first 1.5 seconds (missing or invalid key, credits, missing file) are still returned synchronously with their usual status. - Use your own `application_id` to match each webhook with its request. Deliveries follow the webhook rules: 2-second timeout, up to 3 attempts. **Asynchronous request** ```bash curl -X POST https://hirelayer.co/api/v3/parser \ -H "X-API-Key: $HIRELAYER_API_KEY" \ -H "Prefer: respond-async" \ -F "file=@resume.pdf" \ -F "application_id=app_123" \ -F "webhook_url=https://example.com/hooks/hirelayer?token=YOUR_SECRET" ``` ## 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://hirelayer.co/api-docs/extract-v2.md). New integrations must use V3. ## Next - [HireLayer Job Extract](https://hirelayer.co/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://hirelayer.co/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. --- # Page: HireLayer Job Extract > Extract explicit, weighted and mandatory criteria from a job description, ready to send to HireLayer Match. Source: https://hirelayer.co/api-docs/job-extract · Markdown: https://hirelayer.co/api-docs/job-extract.md - **Endpoint:** `POST /api/v1/jobs/extract-criteria` - **Input:** JSON · `job_text` up to 50,000 characters - **Output:** Weighted criteria with a French label and rationale - **Billing:** 1 credit per successful call ## Extract job criteria `POST https://hirelayer.co/api/v1/jobs/extract-criteria` Turn a job description into weighted criteria that can be checked against a resume. - **Authentication:** `X-API-Key` header - **Content type:** `application/json` - **Billing:** 1 credit per successful call - **Client timeout:** at least 65 seconds ### Request body | Field | Type | Required | Description | | --- | --- | --- | --- | | `job_text` | `string` | Yes | Full job description, in any language. Trimmed before validation. 1–50,000 characters. | ### Example request **cURL** ```bash curl -X POST https://hirelayer.co/api/v1/jobs/extract-criteria \ -H "X-API-Key: $HIRELAYER_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "job_text": "Senior Frontend Engineer, Paris (hybrid). You will build our recruiting platform with React and TypeScript. Requirements: 5+ years of frontend development, strong React and TypeScript skills, fluent English. Nice to have: experience with Next.js." }' ``` **Python** ```python import os import requests response = requests.post( "https://hirelayer.co/api/v1/jobs/extract-criteria", headers={"X-API-Key": os.environ["HIRELAYER_API_KEY"]}, json={ "job_text": "Senior Frontend Engineer, Paris (hybrid). You will build our recruiting platform with React and TypeScript. Requirements: 5+ years of frontend development, strong React and TypeScript skills, fluent English. Nice to have: experience with Next.js.", }, timeout=65, ) response.raise_for_status() data = response.json() print(data["matching_criteria"]) ``` **TypeScript** ```typescript const response = await fetch('https://hirelayer.co/api/v1/jobs/extract-criteria', { method: 'POST', headers: { 'X-API-Key': process.env.HIRELAYER_API_KEY!, 'Content-Type': 'application/json', }, body: JSON.stringify({ job_text: 'Senior Frontend Engineer, Paris (hybrid). You will build our recruiting platform with React and TypeScript. Requirements: 5+ years of frontend development, strong React and TypeScript skills, fluent English. Nice to have: experience with Next.js.', }), signal: AbortSignal.timeout(65_000), }) if (!response.ok) { throw new Error(`HireLayer ${response.status}: ${await response.text()}`) } const data = await response.json() console.log(data.matching_criteria) ``` ### Response `200` Extracted criteria. | Field | Type | Description | | --- | --- | --- | | `matching_criteria` | `MatchingCriterion[]` | Criteria sorted by `weight`, highest first, with IDs `crit_1`…`crit_n` in that order. Can be empty. | | `matching_criteria[].id` | `string` | Criterion ID. Job Extract generates `crit_1`…`crit_n`; Match accepts any non-empty string. Non-empty. | | `matching_criteria[].label` | `string` | What is evaluated, in a short phrase. Non-empty. | | `matching_criteria[].weight` | `integer` | Importance: `3` essential, `2` important, `1` nice to have. 1–3. | | `matching_criteria[].is_mandatory` | `boolean` | Whether the job states it as a hard requirement. Informational: it does not change the Match score. | | `matching_criteria[].rationale` | `string` | Why the criterion matters for the job. Non-empty. | ```json { "matching_criteria": [ { "id": "crit_1", "label": "Maîtrise de React", "weight": 3, "is_mandatory": true, "rationale": "React est explicitement exigé pour le poste." }, { "id": "crit_2", "label": "Maîtrise de TypeScript", "weight": 3, "is_mandatory": true, "rationale": "TypeScript est explicitement exigé pour le poste." }, { "id": "crit_3", "label": "Au moins 5 ans d’expérience en développement frontend", "weight": 3, "is_mandatory": true, "rationale": "L’offre demande plus de cinq ans d’expérience frontend." }, { "id": "crit_4", "label": "Anglais courant", "weight": 2, "is_mandatory": true, "rationale": "Un anglais courant est demandé." }, { "id": "crit_5", "label": "Expérience avec Next.js", "weight": 1, "is_mandatory": false, "rationale": "Next.js est présenté comme un atout." } ] } ``` ### Errors | Status | `error` | `code` | When | Retry | | --- | --- | --- | --- | --- | | `400` | `Request body must be a JSON object` | — | The body is missing, is a JSON array, or `Content-Type` is not `application/json`. | Do not retry | | `400` | `The request contains unsupported fields` | — | The body contains a field that is not documented for this endpoint. | Do not retry | | `400` | `The 'job_text' field is required` | — | `job_text` is missing or not a string. | Do not retry | | `400` | `job_text cannot be empty` | — | `job_text` is empty after trimming. | Do not retry | | `400` | `job_text must be 50000 characters or less` | — | `job_text` is longer than 50,000 characters after trimming. | 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. `availableCredits` is the current balance. | Fix, then retry | | `500` | `Internal server error` | — | Unexpected gateway failure. | Retry with backoff | | `500` | `Internal server error. Please try again later.` | — | The body is not valid JSON, is a JSON primitive or exceeds 10 MB, or the service failed unexpectedly. Check the payload: if it is valid, retry. | Retry with backoff | | `502` | `Upstream API unavailable` | — | The service did not answer within 60 seconds, or could not be reached. | Retry with backoff | | `502` | `The AI processing step failed. Please try again later.` | — | Criteria extraction failed or returned an invalid result after the service's internal retries. | 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. | Retry with backoff | Example error (`400`): ```json { "success": false, "error": "Request body must be a JSON object" } ``` ### Behavior - **Latency:** Synchronous. The gateway waits up to 60 seconds, then returns `502 Upstream API unavailable`. Use a client timeout of at least 65 seconds. - **Retries:** Transient model errors are retried by the service before it answers. Retry `502` and `503` with exponential backoff; never retry `4xx` unchanged. Failed requests are not charged. - **Validation:** Validation stops at the first error, so one call reports one problem. Text fields are trimmed before their length is checked. - **Output:** Only requirements a resume can prove are returned; the endpoint does not score candidates. `label` and `rationale` are written in French. The number of criteria is not fixed and can be zero. - **Stability:** Two calls with the same text can return slightly different criteria. Extract once per job, let a recruiter review the list, and store it with the job. > **Tip: Send the criteria to Match unchanged.** Each criterion has exactly the shape `matching_criteria` expects in [HireLayer Match](https://hirelayer.co/api-docs/match.md). You can also edit, remove or write criteria yourself. ## Next - [HireLayer Match](https://hirelayer.co/api-docs/match.md): Evaluate a resume against job criteria, criterion by criterion, with an explained status and a weighted 0–1 score. - [Build a screening pipeline](https://hirelayer.co/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. --- # Page: HireLayer Match > Evaluate a resume against job criteria, criterion by criterion, with an explained status and a weighted 0–1 score. Source: https://hirelayer.co/api-docs/match · Markdown: https://hirelayer.co/api-docs/match.md - **Endpoint:** `POST /api/v1/matching/job-candidate` - **Input:** JSON · job text, resume text and criteria - **Output:** Score from 0 to 1, a status and an explanation per criterion - **Billing:** 1 credit per successful call ## Match a candidate to a job `POST https://hirelayer.co/api/v1/matching/job-candidate` Evaluate one resume against each job criterion and get an explained 0–1 score. - **Authentication:** `X-API-Key` header - **Content type:** `application/json` - **Billing:** 1 credit per successful call - **Client timeout:** at least 65 seconds ### Request body | Field | Type | Required | Description | | --- | --- | --- | --- | | `job_text` | `string` | Yes | Job description. Trimmed before validation. 1–50,000 characters. | | `candidate_text` | `string` | Yes | Candidate resume as plain text, e.g. `info_resume.text` from HireLayer CV Extract. Trimmed before validation. 1–50,000 characters. | | `matching_criteria` | `MatchingCriterion[]` | Yes | Criteria to evaluate, usually from HireLayer Job Extract. Can be empty. No other criterion field is accepted. | | `matching_criteria[].id` | `string` | Yes | Criterion ID. Job Extract generates `crit_1`…`crit_n`; Match accepts any non-empty string. Non-empty. | | `matching_criteria[].label` | `string` | Yes | What is evaluated, in a short phrase. Non-empty. | | `matching_criteria[].weight` | `integer` | Yes | Importance: `3` essential, `2` important, `1` nice to have. 1–3. | | `matching_criteria[].is_mandatory` | `boolean` | Yes | Whether the job states it as a hard requirement. Informational: it does not change the Match score. | | `matching_criteria[].rationale` | `string` | Yes | Why the criterion matters for the job. Non-empty. | ### Example request **cURL** ```bash curl -X POST https://hirelayer.co/api/v1/matching/job-candidate \ -H "X-API-Key: $HIRELAYER_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "job_text": "Senior Frontend Engineer, Paris (hybrid). You will build our recruiting platform with React and TypeScript. Requirements: 5+ years of frontend development, strong React and TypeScript skills, fluent English. Nice to have: experience with Next.js.", "candidate_text": "Alex Morgan, Senior Software Engineer in Paris. 8 years of experience. Since 2022, leads a team building a React and TypeScript SaaS platform at Northstar Labs. Previously built Node.js APIs. Professional English.", "matching_criteria": [ { "id": "crit_1", "label": "Maîtrise de React", "weight": 3, "is_mandatory": true, "rationale": "React est explicitement exigé pour le poste." }, { "id": "crit_2", "label": "Maîtrise de TypeScript", "weight": 3, "is_mandatory": true, "rationale": "TypeScript est explicitement exigé pour le poste." }, { "id": "crit_3", "label": "Au moins 5 ans d’expérience en développement frontend", "weight": 3, "is_mandatory": true, "rationale": "L’offre demande plus de cinq ans d’expérience frontend." }, { "id": "crit_4", "label": "Anglais courant", "weight": 2, "is_mandatory": true, "rationale": "Un anglais courant est demandé." }, { "id": "crit_5", "label": "Expérience avec Next.js", "weight": 1, "is_mandatory": false, "rationale": "Next.js est présenté comme un atout." } ] }' ``` **Python** ```python import os import requests response = requests.post( "https://hirelayer.co/api/v1/matching/job-candidate", headers={"X-API-Key": os.environ["HIRELAYER_API_KEY"]}, json={ "job_text": "Senior Frontend Engineer, Paris (hybrid). You will build our recruiting platform with React and TypeScript. Requirements: 5+ years of frontend development, strong React and TypeScript skills, fluent English. Nice to have: experience with Next.js.", "candidate_text": "Alex Morgan, Senior Software Engineer in Paris. 8 years of experience. Since 2022, leads a team building a React and TypeScript SaaS platform at Northstar Labs. Previously built Node.js APIs. Professional English.", "matching_criteria": [ { "id": "crit_1", "label": "Maîtrise de React", "weight": 3, "is_mandatory": True, "rationale": "React est explicitement exigé pour le poste.", }, { "id": "crit_2", "label": "Maîtrise de TypeScript", "weight": 3, "is_mandatory": True, "rationale": "TypeScript est explicitement exigé pour le poste.", }, { "id": "crit_3", "label": "Au moins 5 ans d’expérience en développement frontend", "weight": 3, "is_mandatory": True, "rationale": "L’offre demande plus de cinq ans d’expérience frontend.", }, { "id": "crit_4", "label": "Anglais courant", "weight": 2, "is_mandatory": True, "rationale": "Un anglais courant est demandé.", }, { "id": "crit_5", "label": "Expérience avec Next.js", "weight": 1, "is_mandatory": False, "rationale": "Next.js est présenté comme un atout.", }, ], }, timeout=65, ) response.raise_for_status() data = response.json() print(data["score"]) ``` **TypeScript** ```typescript const response = await fetch('https://hirelayer.co/api/v1/matching/job-candidate', { method: 'POST', headers: { 'X-API-Key': process.env.HIRELAYER_API_KEY!, 'Content-Type': 'application/json', }, body: JSON.stringify({ job_text: 'Senior Frontend Engineer, Paris (hybrid). You will build our recruiting platform with React and TypeScript. Requirements: 5+ years of frontend development, strong React and TypeScript skills, fluent English. Nice to have: experience with Next.js.', candidate_text: 'Alex Morgan, Senior Software Engineer in Paris. 8 years of experience. Since 2022, leads a team building a React and TypeScript SaaS platform at Northstar Labs. Previously built Node.js APIs. Professional English.', matching_criteria: [ { id: 'crit_1', label: 'Maîtrise de React', weight: 3, is_mandatory: true, rationale: 'React est explicitement exigé pour le poste.', }, { id: 'crit_2', label: 'Maîtrise de TypeScript', weight: 3, is_mandatory: true, rationale: 'TypeScript est explicitement exigé pour le poste.', }, { id: 'crit_3', label: 'Au moins 5 ans d’expérience en développement frontend', weight: 3, is_mandatory: true, rationale: 'L’offre demande plus de cinq ans d’expérience frontend.', }, { id: 'crit_4', label: 'Anglais courant', weight: 2, is_mandatory: true, rationale: 'Un anglais courant est demandé.', }, { id: 'crit_5', label: 'Expérience avec Next.js', weight: 1, is_mandatory: false, rationale: 'Next.js est présenté comme un atout.', }, ], }), signal: AbortSignal.timeout(65_000), }) if (!response.ok) { throw new Error(`HireLayer ${response.status}: ${await response.text()}`) } const data = await response.json() console.log(data.score) ``` ### Response `200` Evaluation of every criterion. | Field | Type | Description | | --- | --- | --- | | `score` | `number` | Weighted average of the criterion statuses. Not rounded. 0–1. | | `summary` | `string` | Overall assessment, in French. | | `evaluated_criteria` | `EvaluatedCriterion[]` | Every input criterion, in input order, with its evaluation. | | `evaluated_criteria[].id` | `string` | Criterion ID. Job Extract generates `crit_1`…`crit_n`; Match accepts any non-empty string. Non-empty. | | `evaluated_criteria[].label` | `string` | What is evaluated, in a short phrase. Non-empty. | | `evaluated_criteria[].weight` | `integer` | Importance: `3` essential, `2` important, `1` nice to have. 1–3. | | `evaluated_criteria[].is_mandatory` | `boolean` | Whether the job states it as a hard requirement. Informational: it does not change the Match score. | | `evaluated_criteria[].rationale` | `string` | Why the criterion matters for the job. Non-empty. | | `evaluated_criteria[].match_status` | `string` | `ideal`: clearly met · `potential`: partly or indirectly met · `not_mentioned`: the resume says nothing · `not_valid`: contradicted. One of: `"ideal"`, `"potential"`, `"not_valid"`, `"not_mentioned"`. | | `evaluated_criteria[].match_explanation` | `string` | Evidence from the resume, in French. | ```json { "score": 0.8916666666666666, "summary": "Profil très aligné : React, TypeScript et l’expérience demandée sont démontrés. Le niveau d’anglais reste à confirmer.", "evaluated_criteria": [ { "id": "crit_1", "label": "Maîtrise de React", "weight": 3, "is_mandatory": true, "rationale": "React est explicitement exigé pour le poste.", "match_status": "ideal", "match_explanation": "Le CV décrit une équipe React dirigée depuis 2022 sur une plateforme en production." }, { "id": "crit_2", "label": "Maîtrise de TypeScript", "weight": 3, "is_mandatory": true, "rationale": "TypeScript est explicitement exigé pour le poste.", "match_status": "ideal", "match_explanation": "La plateforme actuelle est développée en TypeScript." }, { "id": "crit_3", "label": "Au moins 5 ans d’expérience en développement frontend", "weight": 3, "is_mandatory": true, "rationale": "L’offre demande plus de cinq ans d’expérience frontend.", "match_status": "ideal", "match_explanation": "Le candidat cumule huit ans d’expérience en développement." }, { "id": "crit_4", "label": "Anglais courant", "weight": 2, "is_mandatory": true, "rationale": "Un anglais courant est demandé.", "match_status": "potential", "match_explanation": "Le CV mentionne un anglais professionnel, sans préciser un niveau courant." }, { "id": "crit_5", "label": "Expérience avec Next.js", "weight": 1, "is_mandatory": false, "rationale": "Next.js est présenté comme un atout.", "match_status": "not_mentioned", "match_explanation": "Le CV ne mentionne pas Next.js." } ] } ``` ### Errors | Status | `error` | `code` | When | Retry | | --- | --- | --- | --- | --- | | `400` | `Request body must be a JSON object` | — | The body is missing, is a JSON array, or `Content-Type` is not `application/json`. | Do not retry | | `400` | `The request contains unsupported fields` | — | The body contains a field that is not documented for this endpoint. | Do not retry | | `400` | `The 'job_text' field is required` | — | `job_text` is missing or not a string. | Do not retry | | `400` | `job_text cannot be empty` | — | `job_text` is empty after trimming. | Do not retry | | `400` | `job_text must be 50000 characters or less` | — | `job_text` is longer than 50,000 characters after trimming. | Do not retry | | `400` | `The 'candidate_text' field is required` | — | `candidate_text` is missing or not a string. | Do not retry | | `400` | `candidate_text cannot be empty` | — | `candidate_text` is empty after trimming. | Do not retry | | `400` | `candidate_text must be 50000 characters or less` | — | `candidate_text` is longer than 50,000 characters after trimming. | Do not retry | | `400` | `The 'matching_criteria' field must be an array` | — | `matching_criteria` is missing or not an array. | Do not retry | | `400` | `matching_criteria contains an invalid criterion` | — | A criterion has a missing, empty or extra field, or a `weight` outside 1–3. | 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. `availableCredits` is the current balance. | Fix, then retry | | `500` | `Internal server error` | — | Unexpected gateway failure. | Retry with backoff | | `500` | `Internal server error. Please try again later.` | — | The body is not valid JSON, is a JSON primitive or exceeds 10 MB, or the service failed unexpectedly. Check the payload: if it is valid, retry. | Retry with backoff | | `502` | `Upstream API unavailable` | — | The service did not answer within 60 seconds, or could not be reached. | Retry with backoff | | `502` | `The AI processing step failed. Please try again later.` | — | Matching failed or returned an invalid result after the service's internal retries. | 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. | Retry with backoff | Example error (`400`): ```json { "success": false, "error": "Request body must be a JSON object" } ``` ### Behavior - **Latency:** Synchronous. The gateway waits up to 60 seconds, then returns `502 Upstream API unavailable`. Use a client timeout of at least 65 seconds. - **Retries:** Transient model errors are retried by the service before it answers. Retry `502` and `503` with exponential backoff; never retry `4xx` unchanged. Failed requests are not charged. - **Validation:** Validation stops at the first error, so one call reports one problem. Text fields are trimmed before their length is checked. - **Score:** `score = Σ(weight × value) / Σ weight` with `ideal` = 1, `potential` = 0.6, `not_mentioned` = 0.5 and `not_valid` = 0. Every criterion counts in the denominator; `is_mandatory` has no effect on the score. - **Empty criteria:** With `"matching_criteria": []` the model is not called and the response is exactly `{"score": 0, "summary": "Aucun critère à évaluer.", "evaluated_criteria": []}`. - **Hard requirements:** To reject candidates who miss a mandatory criterion, check `is_mandatory` and `match_status` in your code: the score alone does not do it. > **Tip: Comparing several candidates?.** Use [HireLayer Rank](https://hirelayer.co/api-docs/rank.md) to order up to 10 candidates for one job in a single call. Match gives the detailed, per-criterion explanation for one candidate. ## Next - [HireLayer Rank](https://hirelayer.co/api-docs/rank.md): Rank up to 10 candidates against one job description in a single call, with a score and a rationale for each. - [Build a screening pipeline](https://hirelayer.co/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. --- # Page: HireLayer Rank > Rank up to 10 candidates against one job description in a single call, with a score and a rationale for each. Source: https://hirelayer.co/api-docs/rank · Markdown: https://hirelayer.co/api-docs/rank.md - **Endpoint:** `POST /api/v1/matching/job-candidates/rank` - **Input:** JSON · job text and 1–10 candidates - **Output:** Ranks `1`…`n`, scores and French rationales - **Billing:** 1 credit per successful call ## Rank candidates for a job `POST https://hirelayer.co/api/v1/matching/job-candidates/rank` Order up to 10 candidates for one job description, with a score and a rationale for each. - **Authentication:** `X-API-Key` header - **Content type:** `application/json` - **Billing:** 1 credit per successful call, whatever the number of candidates - **Client timeout:** at least 65 seconds ### Request body | Field | Type | Required | Description | | --- | --- | --- | --- | | `job_text` | `string` | Yes | Job description. Trimmed before validation. 1–50,000 characters. | | `candidates` | `RankingCandidate[]` | Yes | Candidates to rank against the same job. 1–10 items. | | `candidates[].id` | `string` | Yes | Your candidate ID. Unique within the request after trimming. Non-empty. | | `candidates[].candidate_text` | `string` | Yes | Resume as plain text. Trimmed before validation. 1–50,000 characters. | ### Example request **cURL** ```bash curl -X POST https://hirelayer.co/api/v1/matching/job-candidates/rank \ -H "X-API-Key: $HIRELAYER_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "job_text": "Senior Frontend Engineer, Paris (hybrid). You will build our recruiting platform with React and TypeScript. Requirements: 5+ years of frontend development, strong React and TypeScript skills, fluent English. Nice to have: experience with Next.js.", "candidates": [ { "id": "candidate_1", "candidate_text": "Alex Morgan, Senior Software Engineer in Paris. 8 years of experience. Since 2022, leads a team building a React and TypeScript SaaS platform at Northstar Labs. Previously built Node.js APIs. Professional English." }, { "id": "candidate_2", "candidate_text": "Frontend developer in Lyon with 3 years of React experience on e-commerce sites. JavaScript, some TypeScript. Conversational English." }, { "id": "candidate_3", "candidate_text": "Full-stack JavaScript developer with 6 years of experience, mostly Vue.js and PHP. Fluent English." } ] }' ``` **Python** ```python import os import requests response = requests.post( "https://hirelayer.co/api/v1/matching/job-candidates/rank", headers={"X-API-Key": os.environ["HIRELAYER_API_KEY"]}, json={ "job_text": "Senior Frontend Engineer, Paris (hybrid). You will build our recruiting platform with React and TypeScript. Requirements: 5+ years of frontend development, strong React and TypeScript skills, fluent English. Nice to have: experience with Next.js.", "candidates": [ { "id": "candidate_1", "candidate_text": "Alex Morgan, Senior Software Engineer in Paris. 8 years of experience. Since 2022, leads a team building a React and TypeScript SaaS platform at Northstar Labs. Previously built Node.js APIs. Professional English.", }, { "id": "candidate_2", "candidate_text": "Frontend developer in Lyon with 3 years of React experience on e-commerce sites. JavaScript, some TypeScript. Conversational English.", }, { "id": "candidate_3", "candidate_text": "Full-stack JavaScript developer with 6 years of experience, mostly Vue.js and PHP. Fluent English.", }, ], }, timeout=65, ) response.raise_for_status() data = response.json() print(data["rankings"]) ``` **TypeScript** ```typescript const response = await fetch('https://hirelayer.co/api/v1/matching/job-candidates/rank', { method: 'POST', headers: { 'X-API-Key': process.env.HIRELAYER_API_KEY!, 'Content-Type': 'application/json', }, body: JSON.stringify({ job_text: 'Senior Frontend Engineer, Paris (hybrid). You will build our recruiting platform with React and TypeScript. Requirements: 5+ years of frontend development, strong React and TypeScript skills, fluent English. Nice to have: experience with Next.js.', candidates: [ { id: 'candidate_1', candidate_text: 'Alex Morgan, Senior Software Engineer in Paris. 8 years of experience. Since 2022, leads a team building a React and TypeScript SaaS platform at Northstar Labs. Previously built Node.js APIs. Professional English.', }, { id: 'candidate_2', candidate_text: 'Frontend developer in Lyon with 3 years of React experience on e-commerce sites. JavaScript, some TypeScript. Conversational English.', }, { id: 'candidate_3', candidate_text: 'Full-stack JavaScript developer with 6 years of experience, mostly Vue.js and PHP. Fluent English.', }, ], }), signal: AbortSignal.timeout(65_000), }) if (!response.ok) { throw new Error(`HireLayer ${response.status}: ${await response.text()}`) } const data = await response.json() console.log(data.rankings) ``` ### Response `200` Ranked candidates. | Field | Type | Description | | --- | --- | --- | | `rankings` | `Ranking[]` | One entry per candidate, sorted by `rank`. | | `rankings[].rank` | `integer` | Position from 1 (best) to the number of candidates. ≥ 1. | | `rankings[].candidate_id` | `string` | The `id` you sent, trimmed. | | `rankings[].score` | `number` | Relevance for the job. Use `rank` for ordering. 0–1. | | `rankings[].rationale` | `string` | Why the candidate holds this position, in French. | ```json { "rankings": [ { "rank": 1, "candidate_id": "candidate_1", "score": 0.91, "rationale": "React et TypeScript sont démontrés en production, avec l’expérience demandée." }, { "rank": 2, "candidate_id": "candidate_2", "score": 0.64, "rationale": "Bonne pratique de React, mais expérience plus courte et anglais seulement conversationnel." }, { "rank": 3, "candidate_id": "candidate_3", "score": 0.38, "rationale": "Profil JavaScript solide, mais centré sur Vue.js sans expérience React mentionnée." } ] } ``` ### Errors | Status | `error` | `code` | When | Retry | | --- | --- | --- | --- | --- | | `400` | `Request body must be a JSON object` | — | The body is missing, is a JSON array, or `Content-Type` is not `application/json`. | Do not retry | | `400` | `The request contains unsupported fields` | — | The body contains a field that is not documented for this endpoint. | Do not retry | | `400` | `The 'job_text' field is required` | — | `job_text` is missing or not a string. | Do not retry | | `400` | `job_text cannot be empty` | — | `job_text` is empty after trimming. | Do not retry | | `400` | `job_text must be 50000 characters or less` | — | `job_text` is longer than 50,000 characters after trimming. | Do not retry | | `400` | `The 'candidates' field must be an array` | — | `candidates` is missing or not an array. | Do not retry | | `400` | `candidates cannot be empty` | — | `candidates` is `[]`. | Do not retry | | `400` | `candidates must contain 10 candidates or fewer` | — | More than 10 candidates. | Do not retry | | `400` | `candidates contains an invalid candidate` | — | A candidate has a missing, empty or extra field. | Do not retry | | `400` | `candidate_text must be 50000 characters or less` | — | A `candidate_text` is longer than 50,000 characters after trimming. | Do not retry | | `400` | `candidates contains duplicate ids` | — | Two candidates share the same `id` after trimming. | 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. `availableCredits` is the current balance. | Fix, then retry | | `500` | `Internal server error` | — | Unexpected gateway failure. | Retry with backoff | | `500` | `Internal server error. Please try again later.` | — | The body is not valid JSON, is a JSON primitive or exceeds 10 MB, or the service failed unexpectedly. Check the payload: if it is valid, retry. | Retry with backoff | | `502` | `Upstream API unavailable` | — | The service did not answer within 60 seconds, or could not be reached. | Retry with backoff | | `502` | `The AI processing step failed. Please try again later.` | — | Ranking failed or returned an invalid result after the service's internal retries. | 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. | Retry with backoff | Example error (`400`): ```json { "success": false, "error": "Request body must be a JSON object" } ``` ### Behavior - **Latency:** Synchronous. The gateway waits up to 60 seconds, then returns `502 Upstream API unavailable`. Use a client timeout of at least 65 seconds. - **Retries:** Transient model errors are retried by the service before it answers. Retry `502` and `503` with exponential backoff; never retry `4xx` unchanged. Failed requests are not charged. - **Validation:** Validation stops at the first error, so one call reports one problem. Text fields are trimmed before their length is checked. - **Ordering:** Every candidate appears exactly once and ranks are `1`…`n`. `rank` is authoritative: `score` is not guaranteed to decrease strictly with rank. - **More than 10 candidates:** Ranks are relative to one request. To screen a larger pool, pre-filter it, or score each candidate with [Match](https://hirelayer.co/api-docs/match.md) against the same criteria, which gives comparable scores. ## Next - [HireLayer Match](https://hirelayer.co/api-docs/match.md): Evaluate a resume against job criteria, criterion by criterion, with an explained status and a weighted 0–1 score. - [Build a screening pipeline](https://hirelayer.co/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. --- # Page: HireLayer Skills > Resolve free-text skills, from one skill to a whole resume section, to a taxonomy of 11,061 skills with families and domains, and download the taxonomy. Source: https://hirelayer.co/api-docs/skills · Markdown: https://hirelayer.co/api-docs/skills.md - **Endpoints:** `POST /api/v1/skills/resolve` · `GET /api/v1/skills` - **Input:** JSON · free text up to 5,000 characters, French or English - **Output:** Taxonomy skills with stable ids, families and domains - **Billing:** 1 credit per successful call ## Resolve skills `POST https://hirelayer.co/api/v1/skills/resolve` Turn free text, from one skill to a whole skills section, into taxonomy skills with their families and domains. - **Authentication:** `X-API-Key` header - **Content type:** `application/json` - **Billing:** 1 credit per successful call - **Client timeout:** at least 65 seconds ### Request body | Field | Type | Required | Description | | --- | --- | --- | --- | | `text` | `string` | Yes | Free text in French or English: one skill (`MS Excel`), a compound string (`Pack Office (Word, Excel)`) or a whole skills section (lines, bullets, `;`, comma lists). 1–5,000 characters. | | `language` | `string` | No | `fr` (default) or `en`. One of: `"fr"`, `"en"`. | ### Example request **cURL** ```bash curl -X POST https://hirelayer.co/api/v1/skills/resolve \ -H "X-API-Key: $HIRELAYER_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "text": "Google Sheet/Excel", "language": "fr" }' ``` **Python** ```python import os import requests response = requests.post( "https://hirelayer.co/api/v1/skills/resolve", headers={"X-API-Key": os.environ["HIRELAYER_API_KEY"]}, json={ "text": "Google Sheet/Excel", "language": "fr", }, timeout=65, ) response.raise_for_status() data = response.json() print(data["skills"]) ``` **TypeScript** ```typescript const response = await fetch('https://hirelayer.co/api/v1/skills/resolve', { method: 'POST', headers: { 'X-API-Key': process.env.HIRELAYER_API_KEY!, 'Content-Type': 'application/json', }, body: JSON.stringify({ text: 'Google Sheet/Excel', language: 'fr', }), signal: AbortSignal.timeout(65_000), }) if (!response.ok) { throw new Error(`HireLayer ${response.status}: ${await response.text()}`) } const data = await response.json() console.log(data.skills) ``` ### Response `200` Skills found in the text. | Field | Type | Description | | --- | --- | --- | | `language` | `string` | Language of the names returned. One of: `"fr"`, `"en"`. | | `skills` | `ResolvedSkill[]` | Taxonomy skills named in the text, in the order of the text. | | `skills[].id` | `string` | Stable skill id, e.g. `excel`. Store and filter on it. | | `skills[].name` | `string` | Canonical name in the requested language. | | `skills[].type` | `string` | One of: `"hard"`, `"software"`, `"certification"`, `"soft"`, `"language"`. | | `skills[].families` | `SkillFamilyRef[]` | Families of the skill, the primary one first. | | `skills[].families[].id` | `string` | Stable family id, e.g. `office_software`. | | `skills[].families[].name` | `string` | Family name in the requested language. | | `skills[].families[].domain_id` | `string` | Id of the domain the family belongs to. | | `skills[].domains` | `SkillDomainRef[]` | Domains of those families, the primary one first. | | `skills[].domains[].id` | `string` | Stable domain id, e.g. `data_ai`. | | `skills[].domains[].name` | `string` | Domain name in the requested language. | | `skills[].broader` | `BroaderSkill \| null` | A more general skill to widen a search to (Excel → Microsoft Office), or `null`. | | `skills[].broader.id` | `string` | | | `skills[].broader.name` | `string` | | | `skills[].mention` | `string` | The part of the text the skill was found in. | | `unresolved` | `UnresolvedSkill[]` | Parts of the text that match no taxonomy skill. | | `unresolved[].text` | `string` | Part of the text matching no skill. | | `unresolved[].family_hints` | `object[]` | Families it probably belongs to, possibly none. No skill is guessed. | | `unresolved[].family_hints[].id` | `string` | | | `unresolved[].family_hints[].name` | `string` | | | `taxonomy_version` | `string` | Taxonomy version (`v1`, `v2`…). Results stored with an older version should be resolved again. | ```json { "language": "fr", "skills": [ { "id": "google_sheets", "name": "Google Sheets", "type": "software", "mention": "Google Sheet", "families": [ { "id": "office_software", "name": "Bureautique", "domain_id": "administration_office" }, { "id": "data_analysis", "name": "Analyse de données", "domain_id": "data_ai" }, { "id": "business_intelligence", "name": "Business Intelligence & dataviz", "domain_id": "data_ai" } ], "domains": [ { "id": "administration_office", "name": "Administration & Bureautique" }, { "id": "data_ai", "name": "Data & Intelligence artificielle" } ], "broader": null }, { "id": "excel", "name": "Excel", "type": "software", "mention": "Excel", "families": [ { "id": "office_software", "name": "Bureautique", "domain_id": "administration_office" }, { "id": "data_analysis", "name": "Analyse de données", "domain_id": "data_ai" } ], "domains": [ { "id": "administration_office", "name": "Administration & Bureautique" }, { "id": "data_ai", "name": "Data & Intelligence artificielle" } ], "broader": { "id": "microsoft_office", "name": "Microsoft Office" } } ], "unresolved": [], "taxonomy_version": "v1" } ``` ### Skills section, names in English request Send a whole list in one call: each skill comes back once, in the order of the text. With `language: "en"`, names are returned in English whatever the language of the text. **cURL** ```bash curl -X POST https://hirelayer.co/api/v1/skills/resolve \ -H "X-API-Key: $HIRELAYER_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "text": "React, Node, Cobol, gestion de projet", "language": "en" }' ``` **Python** ```python import os import requests response = requests.post( "https://hirelayer.co/api/v1/skills/resolve", headers={"X-API-Key": os.environ["HIRELAYER_API_KEY"]}, json={ "text": "React, Node, Cobol, gestion de projet", "language": "en", }, timeout=65, ) response.raise_for_status() data = response.json() print(data["skills"]) ``` **TypeScript** ```typescript const response = await fetch('https://hirelayer.co/api/v1/skills/resolve', { method: 'POST', headers: { 'X-API-Key': process.env.HIRELAYER_API_KEY!, 'Content-Type': 'application/json', }, body: JSON.stringify({ text: 'React, Node, Cobol, gestion de projet', language: 'en', }), signal: AbortSignal.timeout(65_000), }) if (!response.ok) { throw new Error(`HireLayer ${response.status}: ${await response.text()}`) } const data = await response.json() console.log(data.skills) ``` ```json { "language": "en", "skills": [ { "id": "react", "name": "React", "type": "software", "mention": "React", "families": [ { "id": "frontend_development", "name": "Frontend development", "domain_id": "software_development" } ], "domains": [ { "id": "software_development", "name": "Software Development" } ], "broader": null }, { "id": "node_js", "name": "Node.js", "type": "software", "mention": "Node", "families": [ { "id": "backend_development", "name": "Backend development", "domain_id": "software_development" }, { "id": "programming_languages", "name": "Programming languages", "domain_id": "software_development" } ], "domains": [ { "id": "software_development", "name": "Software Development" } ], "broader": null }, { "id": "cobol", "name": "COBOL", "type": "software", "mention": "Cobol", "families": [ { "id": "programming_languages", "name": "Programming languages", "domain_id": "software_development" }, { "id": "backend_development", "name": "Backend development", "domain_id": "software_development" } ], "domains": [ { "id": "software_development", "name": "Software Development" } ], "broader": null }, { "id": "project_management", "name": "Project management", "type": "hard", "mention": "gestion de projet", "families": [ { "id": "project_management", "name": "Project management", "domain_id": "management_strategy" } ], "domains": [ { "id": "management_strategy", "name": "Management, Projects & Strategy" } ], "broader": null } ], "unresolved": [], "taxonomy_version": "v1" } ``` ### Errors | Status | `error` | `code` | When | Retry | | --- | --- | --- | --- | --- | | `400` | `Request body must be a JSON object` | — | The body is missing, is a JSON array, or `Content-Type` is not `application/json`. | Do not retry | | `400` | `Unknown field(s): unexpected_field. Accepted: 'text' and optional 'language'` | — | The body contains another field than `text` and `language` (for example `skill`, `skills` or `top_k`); the message names them. | Do not retry | | `400` | `The 'text' field (string) is required` | — | `text` is missing or not a string. | Do not retry | | `400` | `Text cannot be empty` | — | `text` is empty after trimming. | Do not retry | | `400` | `'text' must be at most 5000 characters` | — | `text` is longer than 5,000 characters. | Do not retry | | `400` | `'language' must be 'fr' or 'en'` | — | `language` is present but is not `fr` or `en` (case-sensitive; `null` is rejected). | 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. `availableCredits` is the current balance. | Fix, then retry | | `500` | `Internal server error` | — | Unexpected gateway failure. | Retry with backoff | | `500` | `Internal server error. Please try again later.` | — | The body is not valid JSON, is a JSON primitive or exceeds 10 MB, or the service failed unexpectedly. Check the payload: if it is valid, retry. | Retry with backoff | | `502` | `Upstream API unavailable` | — | The service did not answer within 60 seconds, or could not be reached. | 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. | Retry with backoff | Example error (`400`): ```json { "success": false, "error": "Request body must be a JSON object" } ``` ### Behavior - **Latency:** Synchronous. Most calls answer in well under a second; a long skills section can take 10 seconds or more. The gateway waits up to 60 seconds; use a client timeout of at least 65 seconds. - **Resolution:** There is no score and no threshold: every part of the text is either resolved to taxonomy skills or listed in `unresolved`. A compound mention can yield several skills (`Pack Office (Word, Excel)` gives Microsoft Office, Word and Excel), and a skill named twice is returned once. - **Storage:** Store `id` rather than `name`: names depend on `language`, ids do not. Keep `taxonomy_version` with your results and resolve them again when it changes. - **Strict body:** Only `text` and `language` are accepted. The former `/api/v1/skills/match` fields (`skill`, `skills`, `top_k`) are rejected, and `POST /api/v1/skills/match` now answers `410`. ## List the skills taxonomy `GET https://hirelayer.co/api/v1/skills` Download every skill of the taxonomy with its families, domains and broader skill. - **Authentication:** `X-API-Key` header - **Billing:** 1 credit per successful call - **Client timeout:** at least 65 seconds ### Query parameters | Field | Type | Required | Description | | --- | --- | --- | --- | | `language` | `string` | No | `fr` (default) or `en`. One of: `"fr"`, `"en"`. | ### Example request **cURL** ```bash curl https://hirelayer.co/api/v1/skills \ -H "X-API-Key: $HIRELAYER_API_KEY" ``` **Python** ```python import os import requests response = requests.get( "https://hirelayer.co/api/v1/skills", headers={"X-API-Key": os.environ["HIRELAYER_API_KEY"]}, timeout=65, ) response.raise_for_status() data = response.json() print(data["total"]) ``` **TypeScript** ```typescript const response = await fetch('https://hirelayer.co/api/v1/skills', { headers: { 'X-API-Key': process.env.HIRELAYER_API_KEY!, }, signal: AbortSignal.timeout(65_000), }) if (!response.ok) { throw new Error(`HireLayer ${response.status}: ${await response.text()}`) } const data = await response.json() console.log(data.total) ``` ### Response `200` The taxonomy (first two of 11,061 skills shown). | Field | Type | Description | | --- | --- | --- | | `language` | `string` | Language of the names returned. One of: `"fr"`, `"en"`. | | `total` | `integer` | Number of skills in the taxonomy. | | `skills` | `TaxonomySkill[]` | The whole taxonomy, most frequent in resumes first. There is no pagination. | | `skills[].id` | `string` | Stable skill id, e.g. `excel`. Store and filter on it. | | `skills[].name` | `string` | Canonical name in the requested language. | | `skills[].type` | `string` | One of: `"hard"`, `"software"`, `"certification"`, `"soft"`, `"language"`. | | `skills[].families` | `SkillFamilyRef[]` | Families of the skill, the primary one first. | | `skills[].families[].id` | `string` | Stable family id, e.g. `office_software`. | | `skills[].families[].name` | `string` | Family name in the requested language. | | `skills[].families[].domain_id` | `string` | Id of the domain the family belongs to. | | `skills[].domains` | `SkillDomainRef[]` | Domains of those families, the primary one first. | | `skills[].domains[].id` | `string` | Stable domain id, e.g. `data_ai`. | | `skills[].domains[].name` | `string` | Domain name in the requested language. | | `skills[].broader` | `BroaderSkill \| null` | A more general skill to widen a search to (Excel → Microsoft Office), or `null`. | | `skills[].broader.id` | `string` | | | `skills[].broader.name` | `string` | | | `taxonomy_version` | `string` | Taxonomy version (`v1`, `v2`…). Results stored with an older version should be resolved again. | ```json { "language": "fr", "total": 11061, "skills": [ { "id": "teamwork", "name": "Travail en équipe", "type": "soft", "families": [ { "id": "teamwork", "name": "Travail en équipe & coopération", "domain_id": "soft_skills" } ], "domains": [ { "id": "soft_skills", "name": "Compétences comportementales" } ], "broader": null }, { "id": "microsoft_office", "name": "Microsoft Office", "type": "software", "families": [ { "id": "office_software", "name": "Bureautique", "domain_id": "administration_office" } ], "domains": [ { "id": "administration_office", "name": "Administration & Bureautique" } ], "broader": null } ], "taxonomy_version": "v1" } ``` ### Errors | Status | `error` | `code` | When | Retry | | --- | --- | --- | --- | --- | | `400` | `'language' must be 'fr' or 'en'` | — | The `language` query parameter is present but is not `fr` or `en` (case-sensitive). | 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. `availableCredits` is the current balance. | Fix, then retry | | `500` | `Internal server error` | — | Unexpected gateway failure. | Retry with backoff | | `502` | `Upstream API unavailable` | — | The service did not answer within 60 seconds, or could not be reached. | 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. | Retry with backoff | Example error (`400`): ```json { "success": false, "error": "'language' must be 'fr' or 'en'" } ``` ### Behavior - **Language:** Add `?language=en` for English names; French is the default. - **Size and caching:** The response holds the whole taxonomy (about 4 MB) and costs a credit: cache it on your side, for example once a day, and refresh it when `taxonomy_version` changes. ## Migrating from /skills/match > **Warning: POST /api/v1/skills/match is retired.** Since 5 October 2026 it answers `410` without being billed, and its error names `POST /api/v1/skills/resolve`. The catalog behind `GET /api/v1/skills` became the 11,061-skill taxonomy at the same time. | /skills/match | /skills/resolve | | --- | --- | | `skill`, or `skills` (up to 100) | `text`: one skill or a whole list (lines, bullets, commas), up to 5,000 characters | | `top_k`, `similarity_score` | None: each part of the text is resolved or listed in `unresolved` | | `results[].skill` | `skills[].name`, and `skills[].id` to store | | `domain`, `subcategory` | `families` and `domains`, the primary one first | | One result list per input skill | `mention`: the part of the text each skill comes from | ## Next - [HireLayer CV Extract](https://hirelayer.co/api-docs/extract.md): Parse a resume file into structured candidate JSON with one multipart request: contact details, experience, education, languages and typed skills. - [Build a screening pipeline](https://hirelayer.co/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. --- # Page: Build a screening pipeline > Chain HireLayer CV Extract, Job Extract, Rank and Match to parse resumes, score them against a job and explain the result. Source: https://hirelayer.co/api-docs/screening-pipeline · Markdown: https://hirelayer.co/api-docs/screening-pipeline.md Each API works on its own. Together they cover a screening workflow: structure the job once, parse every resume, shortlist, then explain. ### 1. Extract the job criteria once Call [Job Extract](https://hirelayer.co/api-docs/job-extract.md) when a job is created or its description changes. Let a recruiter review the criteria, then store them with the job: Match takes them as they are. ### 2. Parse each resume Call [CV Extract](https://hirelayer.co/api-docs/extract.md) when a resume arrives and store the JSON. Keep `info_resume.text` for matching, truncated to 50,000 characters. ### 3. Rank the shortlist [Rank](https://hirelayer.co/api-docs/rank.md) orders up to 10 candidates for one job in a single call. For larger pools, pre-filter first, or score each candidate with Match. ### 4. Explain with Match [Match](https://hirelayer.co/api-docs/match.md) evaluates a candidate against every criterion. The score ignores `is_mandatory`: check mandatory criteria with a `not_valid` status in your own code. > **Tip: Normalize skills for search.** Skills from CV Extract are already resolved to the Skills taxonomy when `status` is `normalized`, with their French name but no id. To store ids, send a candidate’s skills, or skills typed by users, to [Skills](https://hirelayer.co/api-docs/skills.md) as one text of up to 5,000 characters. ## Complete program Save the client as `hirelayer.py` or `hirelayer.mts`, put the job description in `job.txt`, then run `python screening.py` or `npx tsx screening.mts`. Credits used: 1 for the criteria, 1 per resume, 1 for the ranking and 1 for the explanation. **Client (hirelayer)** **Python** ```python # hirelayer.py — minimal client with retries. Requires: pip install requests import mimetypes import os import random import time import requests API_BASE = "https://hirelayer.co/api" class HireLayerError(Exception): def __init__(self, status, message, code=None, request_id=None): super().__init__(f"HireLayer {status}: {message}") self.status = status self.code = code self.request_id = request_id def _delay(attempt, retry_after): if retry_after and retry_after.isdigit(): return int(retry_after) return min(30, 2**attempt) + random.random() def call(method, path, *, json=None, files=None, data=None, timeout=65, max_retries=3): """Call HireLayer. Retries 5xx and connection failures, raises HireLayerError otherwise.""" headers = {"X-API-Key": os.environ["HIRELAYER_API_KEY"]} for attempt in range(max_retries + 1): try: response = requests.request( method, API_BASE + path, headers=headers, json=json, files=files, data=data, timeout=timeout, ) except requests.ConnectionError: if attempt == max_retries: raise time.sleep(_delay(attempt, None)) continue if response.ok: return response.json() if response.status_code >= 500 and attempt < max_retries: time.sleep(_delay(attempt, response.headers.get("Retry-After"))) continue try: body = response.json() except ValueError: body = {} raise HireLayerError( response.status_code, body.get("error", response.reason), body.get("code"), response.headers.get("x-parser-request-id"), ) def parse_resume(path, application_id=None): content_type = mimetypes.guess_type(path)[0] or "application/octet-stream" with open(path, "rb") as file: content = file.read() # bytes can be re-sent on retry return call( "POST", "/v3/parser", files={"file": (os.path.basename(path), content, content_type)}, data={"application_id": application_id} if application_id else None, timeout=150, ) ``` **TypeScript** ```typescript // hirelayer.mts — minimal client with retries. Node.js 18+, run with tsx. const API_BASE = 'https://hirelayer.co/api' export class HireLayerError extends Error { status: number code?: string requestId?: string constructor(status: number, message: string, code?: string, requestId?: string) { super(`HireLayer ${status}: ${message}`) this.status = status this.code = code this.requestId = requestId } } const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)) function retryDelay(attempt: number, retryAfter: string | null) { const seconds = Number(retryAfter) if (retryAfter && Number.isInteger(seconds)) return seconds * 1000 return Math.min(30_000, 1000 * 2 ** attempt) + Math.random() * 1000 } /** Calls HireLayer. Retries 5xx responses, throws HireLayerError otherwise. */ export async function callHireLayer( path: string, options: { json?: unknown; form?: FormData; timeoutMs?: number } = {}, maxRetries = 3 ): Promise { const headers: Record = { 'X-API-Key': process.env.HIRELAYER_API_KEY!, } let body: string | FormData | undefined = options.form if (options.json !== undefined) { headers['Content-Type'] = 'application/json' body = JSON.stringify(options.json) } for (let attempt = 0; ; attempt++) { const response = await fetch(API_BASE + path, { method: body === undefined ? 'GET' : 'POST', headers, body, signal: AbortSignal.timeout(options.timeoutMs ?? 65_000), }) if (response.ok) return (await response.json()) as T if (response.status >= 500 && attempt < maxRetries) { await sleep(retryDelay(attempt, response.headers.get('Retry-After'))) continue } const payload = await response.json().catch(() => ({})) throw new HireLayerError( response.status, payload.error ?? response.statusText, payload.code, response.headers.get('x-parser-request-id') ?? undefined ) } } ``` **Pipeline (screening)** **Python** ```python # screening.py — parse resumes, extract criteria, rank, then explain the top match. from hirelayer import call, parse_resume MAX_TEXT = 50_000 # Match and Rank limit; resume text can reach 100,000 characters with open("job.txt", encoding="utf-8") as file: job_text = file.read() # 1. Extract criteria once per job, then store them with the job. criteria = call("POST", "/v1/jobs/extract-criteria", json={"job_text": job_text})[ "matching_criteria" ] # 2. Parse each resume (synchronous, about 35 seconds each). resumes = {} for path in ["alex.pdf", "sam.pdf", "charlie.pdf"]: resume = parse_resume(path, application_id=path) resumes[path] = resume["info_resume"]["text"][:MAX_TEXT] # 3. Rank up to 10 candidates in one call. rankings = call( "POST", "/v1/matching/job-candidates/rank", json={ "job_text": job_text, "candidates": [ {"id": key, "candidate_text": text} for key, text in resumes.items() ], }, )["rankings"] # 4. Explain the best candidate, criterion by criterion. best = rankings[0]["candidate_id"] match = call( "POST", "/v1/matching/job-candidate", json={ "job_text": job_text, "candidate_text": resumes[best], "matching_criteria": criteria, }, ) print(best, round(match["score"], 2), match["summary"]) for criterion in match["evaluated_criteria"]: if criterion["is_mandatory"] and criterion["match_status"] == "not_valid": print("Missing mandatory criterion:", criterion["label"]) ``` **TypeScript** ```typescript // screening.mts — parse resumes, extract criteria, rank, then explain the top match. // Run: npx tsx screening.mts import { readFile } from 'node:fs/promises' import { basename } from 'node:path' import { callHireLayer } from './hirelayer.mts' type Criterion = { id: string label: string weight: number is_mandatory: boolean rationale: string } type ParsedResume = { info_resume: { text: string } } type Ranking = { rank: number; candidate_id: string; score: number } type MatchResult = { score: number summary: string evaluated_criteria: Array } const MAX_TEXT = 50_000 // Match and Rank limit; resume text can reach 100,000 characters const jobText = await readFile('job.txt', 'utf8') // 1. Extract criteria once per job, then store them with the job. const { matching_criteria } = await callHireLayer<{ matching_criteria: Criterion[] }>('/v1/jobs/extract-criteria', { json: { job_text: jobText } }) // 2. Parse each resume (synchronous, about 35 seconds each). const resumes = new Map() for (const path of ['alex.pdf', 'sam.pdf', 'charlie.pdf']) { const form = new FormData() form.append('file', new Blob([await readFile(path)]), basename(path)) form.append('application_id', path) const resume = await callHireLayer('/v3/parser', { form, timeoutMs: 150_000, }) resumes.set(path, resume.info_resume.text.slice(0, MAX_TEXT)) } // 3. Rank up to 10 candidates in one call. const { rankings } = await callHireLayer<{ rankings: Ranking[] }>( '/v1/matching/job-candidates/rank', { json: { job_text: jobText, candidates: [...resumes].map(([id, candidate_text]) => ({ id, candidate_text, })), }, } ) // 4. Explain the best candidate, criterion by criterion. const best = rankings[0].candidate_id const match = await callHireLayer('/v1/matching/job-candidate', { json: { job_text: jobText, candidate_text: resumes.get(best), matching_criteria, }, }) console.log(best, match.score.toFixed(2), match.summary) for (const criterion of match.evaluated_criteria) { if (criterion.is_mandatory && criterion.match_status === 'not_valid') { console.log('Missing mandatory criterion:', criterion.label) } } ``` ## Production checklist - Call HireLayer from a background job, not from a user-facing request: parsing takes about 35 seconds. - Store results: criteria with the job, parsed JSON with the candidate. Re-running a call costs a credit and can return slightly different text. - Use your own IDs as `application_id` and Rank `id`s, so results map back to your records. - Handle `422` from CV Extract as a permanent outcome for that file (not a resume, unreadable, empty). - Send `do_not_store_data=true` if you keep your own copy of the files: HireLayer then does not store the resume file. - Keep a human in the loop: scores and rankings support a recruiter, they do not replace one. ## Next - [Errors and retries](https://hirelayer.co/api-docs/errors.md): Error format, every status code and message, and when to retry a HireLayer API call. - [Build with AI agents](https://hirelayer.co/api-docs/ai-agents.md): Give Claude Code, Codex, Cursor or any coding agent the context it needs to integrate HireLayer correctly: llms.txt, Markdown pages, OpenAPI and a ready-made rules file. --- # Page: Build with AI agents > Give Claude Code, Codex, Cursor or any coding agent the context it needs to integrate HireLayer correctly: llms.txt, Markdown pages, OpenAPI and a ready-made rules file. Source: https://hirelayer.co/api-docs/ai-agents · Markdown: https://hirelayer.co/api-docs/ai-agents.md The documentation is published in machine-readable formats generated from the same source as these pages, so agents read exactly what you read. ## Machine-readable documentation | Resource | Contents | | --- | --- | | [`/llms.txt`](https://hirelayer.co/llms.txt) | Index of every page with a one-line summary, following the llms.txt convention. | | [`/llms-full.txt`](https://hirelayer.co/llms-full.txt) | The whole documentation in one Markdown file. | | `/api-docs/.md` | Any page as clean Markdown, e.g. [`/api-docs/extract.md`](https://hirelayer.co/api-docs/extract.md). | | [`/openapi.json`](https://hirelayer.co/openapi.json) · [`/openapi.yaml`](https://hirelayer.co/openapi.yaml) | OpenAPI 3.1 description of every endpoint, schema and error, for code generators, Postman or Insomnia. | | [`/api-docs/agent-rules.md`](https://hirelayer.co/api-docs/agent-rules.md) | The rules file below, ready to download. | ## Prompt your coding agent Paste a prompt like this one in Claude Code, Codex or Cursor. The agent fetches the docs it needs. ```text Read https://hirelayer.co/api-docs/extract.md and https://hirelayer.co/llms.txt, then help me integrate HireLayer CV Extract (resume parsing) into my project. Follow the documented authentication, timeouts, retries and error handling, and do not invent fields or endpoints. ``` Every page also has **Copy page** (its Markdown), **Copy for AI** (the page plus integration rules) and **Open in ChatGPT** or **Claude** actions. Each endpoint has its own **Copy for AI** button. ## Add project rules Commit these rules to the repository that calls HireLayer, so every agent session follows them: authentication, timeouts, retries, limits and the exact endpoints. | Agent | File | | --- | --- | | Codex, Cursor, and most agents | `AGENTS.md` at the repository root | | Claude Code | `CLAUDE.md`, or `@AGENTS.md` imported from it | | Cursor (project rule) | `.cursor/rules/hirelayer.mdc` | | GitHub Copilot | `.github/copilot-instructions.md` | ```bash curl -o AGENTS.md https://hirelayer.co/api-docs/agent-rules.md ``` **AGENTS.md** ```markdown # HireLayer API This project calls the HireLayer recruiting APIs (resume parsing, job criteria extraction, candidate matching and ranking, skills normalization). Follow these rules when writing or changing that code. ## HireLayer integration rules - Base URL: `https://hirelayer.co/api`. HTTPS only. There is no official SDK: in code, call the REST API directly (Python `requests`, Node.js 18+ `fetch`); working examples for every API are in https://github.com/HireLayer/resume-parser-examples. The official MCP server (hosted at https://hirelayer.co/mcp with OAuth sign-in, or locally with npm `hirelayer-mcp`; source: https://github.com/HireLayer/hirelayer-mcp) is for AI assistants such as Claude, ChatGPT, Cursor or VS Code, not for application code. - Authentication: header `X-API-Key` on every request except `GET /api/v1/health`. Read the key from the `HIRELAYER_API_KEY` environment variable. Never hard-code it, log it, or send it from browser or mobile code. `Authorization: Bearer` is not supported. - Every call is synchronous; there are no jobs to poll. Only for platforms that cannot wait (Zapier), `POST /api/v3/parser` accepts `Prefer: respond-async` with a `webhook_url` and answers 202, then posts the outcome to the webhook. - Timeouts: use a client timeout of at least 150 s for `POST /api/v3/parser` and 65 s for `/api/v1/*`. - Retries: retry 5xx responses and connection failures at most 3 times with exponential backoff and jitter; honour `Retry-After`. Never retry 4xx responses unchanged. There is no idempotency key. - Errors: every error body has an `error` string. CV Extract errors add a `code` (`INVALID_MULTIPART_BODY`, `INVALID_FILE`, `DOCUMENT_NOT_A_RESUME`, `DOCUMENT_TEXT_EMPTY`, `DOCUMENT_UNREADABLE`, `DOCUMENT_TOO_LARGE`, `PARSER_UNAVAILABLE`). Job Extract, Match, Rank and Skills validation errors are `{"success": false, "error": "…"}` with status 400. - Credits: each successful call costs 1 credit, whatever the API. `403` means no credit left. - JSON bodies: Job Extract, Match, Rank and Skills reject unknown fields with 400. Job Extract, Match and Rank text fields are trimmed and limited to 50,000 characters. Skills (`POST /api/v1/skills/resolve`) takes one `text` (≤ 5,000 characters, a single skill or a whole skills section) and an optional `language` (`fr` by default, or `en`); store the returned skill `id`s, not the names. - CV Extract: send the file as the multipart field `file` (about 4.5 MB max). On 200, `status` is always `"success"`; `upstream_status: "partial"` means optional steps were skipped. `info_resume.text` can reach 100,000 characters: truncate it to 50,000 before sending it to Match or Rank. Send `do_not_store_data=true` when the resume file must not be stored: HireLayer then does not store it and `info_resume.url` is `null`. - Generated text (Job Extract labels and rationales, Match summaries and explanations, Rank rationales) is in French. - Do not invent fields, endpoints or parameters. When unsure, read the OpenAPI document or the Markdown reference. ## Endpoints - `POST /api/v3/parser` — HireLayer CV Extract: Upload one resume file and receive the structured candidate profile in the same response. (multipart/form-data, client timeout ≥ 150 s). Docs: https://hirelayer.co/api-docs/extract.md - `POST /api/v1/jobs/extract-criteria` — HireLayer Job Extract: Turn a job description into weighted criteria that can be checked against a resume. (JSON body, client timeout ≥ 65 s). Docs: https://hirelayer.co/api-docs/job-extract.md - `POST /api/v1/matching/job-candidate` — HireLayer Match: Evaluate one resume against each job criterion and get an explained 0–1 score. (JSON body, client timeout ≥ 65 s). Docs: https://hirelayer.co/api-docs/match.md - `POST /api/v1/matching/job-candidates/rank` — HireLayer Rank: Order up to 10 candidates for one job description, with a score and a rationale for each. (JSON body, client timeout ≥ 65 s). Docs: https://hirelayer.co/api-docs/rank.md - `POST /api/v1/skills/resolve` — HireLayer Skills: Turn free text, from one skill to a whole skills section, into taxonomy skills with their families and domains. (JSON body, client timeout ≥ 65 s). Docs: https://hirelayer.co/api-docs/skills.md - `GET /api/v1/skills` — HireLayer Skills: Download every skill of the taxonomy with its families, domains and broader skill. (no body, client timeout ≥ 65 s). Docs: https://hirelayer.co/api-docs/skills.md - `GET /api/v1/health` — Platform: Check that the Job Extract, Match, Skills and Rank service is up. No API key needed. (no body, client timeout ≥ 10 s, no API key). Docs: https://hirelayer.co/api-docs/health.md - `GET /api/v1/me` — Platform: Check that an API key is valid and see the account and credit balance it belongs to. Integrations use it to test a connection. (no body, client timeout ≥ 10 s). Docs: https://hirelayer.co/api-docs/health.md - `POST /api/v2/parser` — HireLayer CV Extract: Legacy asynchronous contract: the request is accepted with `202` and the result is posted to `webhook_url`. (multipart/form-data, client timeout ≥ 50 s, LEGACY, do not use for new code). Docs: https://hirelayer.co/api-docs/extract-v2.md ## References - Index for agents: https://hirelayer.co/llms.txt - Full documentation in one file: https://hirelayer.co/llms-full.txt - OpenAPI 3.1: https://hirelayer.co/openapi.json ``` ## SDKs and MCP HireLayer has no official SDK: the API is plain HTTPS with one header, which agents handle reliably with the docs above. For typed clients, generate one from [`/openapi.json`](https://hirelayer.co/openapi.json). To use HireLayer from an AI assistant (Claude, ChatGPT, Cursor, VS Code, Codex or any MCP client) rather than from your code, connect the official [MCP server](https://hirelayer.co/mcp) at `https://hirelayer.co/mcp`: the assistant opens the HireLayer sign-in page and you allow access, with no key to copy. It exposes resume parsing, job criteria extraction, matching, ranking and skills resolution as tools, and each successful tool call costs 1 credit, like the API. The same server runs locally with npm `hirelayer-mcp` (open source on [GitHub](https://github.com/HireLayer/hirelayer-mcp)) and an API key. In Claude Code: ```bash claude mcp add --transport http hirelayer https://hirelayer.co/mcp ``` See the [MCP server page](https://hirelayer.co/mcp) for Claude, ChatGPT, Cursor, VS Code and Codex. For code integrations, keep calling the REST API directly as described above. ## Next - [Quickstart](https://hirelayer.co/api-docs/quickstart.md): Create an API key, check connectivity and parse a resume with cURL, Python or TypeScript in about five minutes. - [Build a screening pipeline](https://hirelayer.co/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. --- # Page: Health and key check > Two free endpoints: a public health check for uptime monitors, and a key check that confirms an API key and shows its account and credit balance. Source: https://hirelayer.co/api-docs/health · Markdown: https://hirelayer.co/api-docs/health.md ## Check API health `GET https://hirelayer.co/api/v1/health` Check that the Job Extract, Match, Skills and Rank service is up. No API key needed. - **Authentication:** none - **Billing:** Free - **Client timeout:** at least 10 seconds ### Example request **cURL** ```bash curl https://hirelayer.co/api/v1/health ``` **Python** ```python import requests response = requests.get( "https://hirelayer.co/api/v1/health", timeout=10, ) response.raise_for_status() data = response.json() print(data["status"]) ``` **TypeScript** ```typescript const response = await fetch('https://hirelayer.co/api/v1/health', { signal: AbortSignal.timeout(10_000), }) if (!response.ok) { throw new Error(`HireLayer ${response.status}: ${await response.text()}`) } const data = await response.json() console.log(data.status) ``` ### Response `200` The service is up. | Field | Type | Description | | --- | --- | --- | | `status` | `string` | `healthy` whenever the service answers. One of: `"healthy"`, `"unhealthy"`. | | `service` | `string` | Value: `"HireLayer API"`. | | `timestamp` | `string (date-time)` | Server time, ISO 8601 UTC. | ```json { "status": "healthy", "service": "HireLayer API", "timestamp": "2026-10-01T09:30:12.417Z" } ``` ### Errors | Status | `error` | `code` | When | Retry | | --- | --- | --- | --- | --- | | `502` | `Upstream API unavailable` | — | The service is down or did not answer within 60 seconds. | Retry with backoff | Example error (`502`): ```json { "error": "Upstream API unavailable" } ``` ### Behavior - **Scope:** Covers the service behind `/api/v1`. It does not check HireLayer CV Extract (`/api/v3/parser`), and it does not validate API keys. ## Check an API key `GET https://hirelayer.co/api/v1/me` Check that an API key is valid and see the account and credit balance it belongs to. Integrations use it to test a connection. - **Authentication:** `X-API-Key` header - **Billing:** Free - **Client timeout:** at least 10 seconds ### Example request **cURL** ```bash curl https://hirelayer.co/api/v1/me \ -H "X-API-Key: $HIRELAYER_API_KEY" ``` **Python** ```python import os import requests response = requests.get( "https://hirelayer.co/api/v1/me", headers={"X-API-Key": os.environ["HIRELAYER_API_KEY"]}, timeout=10, ) response.raise_for_status() data = response.json() print(data["account"]["email"]) ``` **TypeScript** ```typescript const response = await fetch('https://hirelayer.co/api/v1/me', { headers: { 'X-API-Key': process.env.HIRELAYER_API_KEY!, }, signal: AbortSignal.timeout(10_000), }) if (!response.ok) { throw new Error(`HireLayer ${response.status}: ${await response.text()}`) } const data = await response.json() console.log(data.account.email) ``` ### Response `200` The key is valid. | Field | Type | Description | | --- | --- | --- | | `authenticated` | `boolean` | Value: `true`. | | `account` | `object` | | | `account.email` | `string \| null` | Email of the account that owns the key. | | `account.plan` | `string` | Current plan. One of: `"free"`, `"starter"`, `"scale"`, `"pay_as_you_go"`, `"enterprise"`. | | `credits` | `object` | | | `credits.available` | `integer \| null` | Credits left: monthly credits plus purchased credits. `null` on an unlimited plan or when the balance could not be read. | | `credits.unlimited` | `boolean` | `true` on plans without a credit limit. | ```json { "authenticated": true, "account": { "email": "alex@example.com", "plan": "starter" }, "credits": { "available": 742, "unlimited": false } } ``` ### Errors | Status | `error` | `code` | When | 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 | | `500` | `Internal server error` | — | Unexpected gateway failure. | Retry with backoff | Example error (`401`): ```json { "error": "Missing API Key" } ``` ### Behavior - **Billing:** Free and not counted as API usage, so a connection test in n8n, Zapier, Make or Power Automate never spends a credit. ## Next - [Quickstart](https://hirelayer.co/api-docs/quickstart.md): Create an API key, check connectivity and parse a resume with cURL, Python or TypeScript in about five minutes. - [Errors and retries](https://hirelayer.co/api-docs/errors.md): Error format, every status code and message, and when to retry a HireLayer API call. --- # Page: CV Extract V2 (legacy) > Reference for the legacy asynchronous V2 resume parser. Kept for existing integrations; use V3 for new ones. > **Legacy.** Maintained for existing integrations only. Source: https://hirelayer.co/api-docs/extract-v2 · Markdown: https://hirelayer.co/api-docs/extract-v2.md > **Warning: Legacy contract.** V2 is maintained for existing integrations only. [HireLayer CV Extract V3](https://hirelayer.co/api-docs/extract.md) is synchronous, supports more formats and returns typed errors. ## Submit a resume (legacy V2) `POST https://hirelayer.co/api/v2/parser` Legacy asynchronous contract: the request is accepted with `202` and the result is posted to `webhook_url`. - **Authentication:** `X-API-Key` header - **Content type:** `multipart/form-data` - **Billing:** 1 credit when the request is accepted (HTTP 202) - **Client timeout:** at least 50 seconds - **Status:** Legacy — maintained for existing integrations only ### Request body | Field | Type | Required | Description | | --- | --- | --- | --- | | `file` | `file` | Yes | The resume: PDF, DOCX, ODT, PPTX, ODP, TXT, JPG or PNG. | | `webhook_url` | `string (URL)` | Yes | URL that receives the result when processing ends. | | `application_id` | `string` | No | Your own reference, echoed in the webhook payload. | ### Example request **cURL** ```bash curl -X POST https://hirelayer.co/api/v2/parser \ -H "X-API-Key: $HIRELAYER_API_KEY" \ -F "file=@resume.pdf;type=application/pdf" \ -F "webhook_url=https://example.com/hirelayer/webhook" \ -F "application_id=app_123" ``` **Python** ```python import os import requests with open("resume.pdf", "rb") as file: response = requests.post( "https://hirelayer.co/api/v2/parser", headers={"X-API-Key": os.environ["HIRELAYER_API_KEY"]}, files={"file": ("resume.pdf", file, "application/pdf")}, data={ "webhook_url": "https://example.com/hirelayer/webhook", "application_id": "app_123", }, timeout=50, ) response.raise_for_status() data = response.json() print(data["message"]) ``` **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/hirelayer/webhook') form.append('application_id', 'app_123') const response = await fetch('https://hirelayer.co/api/v2/parser', { method: 'POST', headers: { 'X-API-Key': process.env.HIRELAYER_API_KEY!, }, body: form, signal: AbortSignal.timeout(50_000), }) if (!response.ok) { throw new Error(`HireLayer ${response.status}: ${await response.text()}`) } const data = await response.json() console.log(data.message) ``` ### Response `202` Accepted. The result is sent to `webhook_url` later. | Field | Type | Description | | --- | --- | --- | | `message` | `string` | Value: `"CV processing initiated."`. | | `body` | `null` | | | `error` | `boolean` | Value: `false`. | ```json { "message": "CV processing initiated.", "body": null, "error": false } ``` ### Errors | Status | `error` | `code` | When | Retry | | --- | --- | --- | --- | --- | | `400` | `An error occurred while processing the document or extracting its text. Please try again later.` | — | Any error from the legacy parser is returned with its status code (`400` for a missing file or `webhook_url`, unsupported type…) and this generic message. | 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 | | `415` | `Content-Type must be multipart/form-data` | — | The request is not `multipart/form-data`. | Do not retry | | `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 | Example error (`401`): ```json { "error": "Missing API Key" } ``` ### Behavior - **Asynchronous:** The gateway waits up to 45 seconds for the request to be accepted. The webhook then receives `{"status": "success", …}` with the parsed resume, or `{"status": "failure", "error_code": …, "error_message": …}`. - **Billing:** The credit is consumed when the request is accepted, even if processing fails later. ## Next - [HireLayer CV Extract](https://hirelayer.co/api-docs/extract.md): Parse a resume file into structured candidate JSON with one multipart request: contact details, experience, education, languages and typed skills.