# 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 → reference catalog). Every call is synchronous; each successful call costs 1 credit. Documentation version: 2026-10-01. Generated from the same source as https://onlineresumeparser.com/api-docs. ## HireLayer integration rules - Base URL: `https://onlineresumeparser.com/api`. HTTPS only. There is no official SDK and no MCP server: call the REST API directly (Python `requests`, Node.js 18+ `fetch`). - 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. - 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_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 and Rank reject unknown fields with 400. Text fields are trimmed and limited to 50,000 characters. Skills match takes `skill` or `skills` (≤ 100 items), never both. - 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://onlineresumeparser.com/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://onlineresumeparser.com/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://onlineresumeparser.com/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://onlineresumeparser.com/api-docs/rank.md - `POST /api/v1/skills/match` — HireLayer Skills: Map free-text skills, one or up to 100 at a time, to the closest catalog skills. (JSON body, client timeout ≥ 65 s). Docs: https://onlineresumeparser.com/api-docs/skills.md - `GET /api/v1/skills` — HireLayer Skills: Download the whole reference catalog used for matching. (no body, client timeout ≥ 65 s). Docs: https://onlineresumeparser.com/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://onlineresumeparser.com/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://onlineresumeparser.com/api-docs/extract-v2.md ## References - Index for agents: https://onlineresumeparser.com/llms.txt - Full documentation in one file: https://onlineresumeparser.com/llms-full.txt - OpenAPI 3.1: https://onlineresumeparser.com/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://onlineresumeparser.com/api-docs · Markdown: https://onlineresumeparser.com/api-docs.md - [HireLayer CV Extract](https://onlineresumeparser.com/api-docs/extract.md): Resume file → structured candidate JSON - [HireLayer Job Extract](https://onlineresumeparser.com/api-docs/job-extract.md): Job description → weighted criteria - [HireLayer Match](https://onlineresumeparser.com/api-docs/match.md): One candidate vs. one job, criterion by criterion - [HireLayer Rank](https://onlineresumeparser.com/api-docs/rank.md): Up to 10 candidates ranked for one job - [HireLayer Skills](https://onlineresumeparser.com/api-docs/skills.md): Free-text skills → reference catalog ## Start here - [Quickstart](https://onlineresumeparser.com/api-docs/quickstart.md): Get a key and parse your first resume in five minutes. - [Screening pipeline](https://onlineresumeparser.com/api-docs/screening-pipeline.md): Parse, extract criteria, rank and explain, end to end. - [Build with AI agents](https://onlineresumeparser.com/api-docs/ai-agents.md): llms.txt, Markdown pages, OpenAPI and ready-made agent rules. - [Errors and retries](https://onlineresumeparser.com/api-docs/errors.md): Every status code, error message and retry rule. ## Basics - **Base URL:** `https://onlineresumeparser.com/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://onlineresumeparser.com/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://onlineresumeparser.com/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://onlineresumeparser.com/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://onlineresumeparser.com/api-docs/rank.md#rank-job-candidates) | | `POST` | `/api/v1/skills/match` | Map free-text skills, one or up to 100 at a time, to the closest catalog skills. | [Skills](https://onlineresumeparser.com/api-docs/skills.md#skills-match) | | `GET` | `/api/v1/skills` | Download the whole reference catalog used for matching. | [Skills](https://onlineresumeparser.com/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://onlineresumeparser.com/api-docs/health.md#health) | | `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://onlineresumeparser.com/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://onlineresumeparser.com/llms.txt) indexes them, [`/llms-full.txt`](https://onlineresumeparser.com/llms-full.txt) contains all of them, and [`/openapi.json`](https://onlineresumeparser.com/openapi.json) describes every endpoint. All are generated from the same source as these pages. ## Next - [Quickstart](https://onlineresumeparser.com/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://onlineresumeparser.com/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://onlineresumeparser.com/api-docs/quickstart · Markdown: https://onlineresumeparser.com/api-docs/quickstart.md ### 1. Create an API key [Sign up](https://onlineresumeparser.com/auth/signup) (the Free plan includes 50 credits a month), then create a key in [Dashboard → API keys](https://onlineresumeparser.com/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://onlineresumeparser.com/api/v1/health ``` **Python** ```python import requests response = requests.get( "https://onlineresumeparser.com/api/v1/health", timeout=10, ) response.raise_for_status() data = response.json() print(data["status"]) ``` **TypeScript** ```typescript const response = await fetch('https://onlineresumeparser.com/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", "skills_loaded": 664, "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://onlineresumeparser.com/api/v3/parser \ -H "X-API-Key: $HIRELAYER_API_KEY" \ -F "file=@resume.pdf;type=application/pdf" \ -F "application_id=app_123" ``` **Python** ```python import os import requests with open("resume.pdf", "rb") as file: response = requests.post( "https://onlineresumeparser.com/api/v3/parser", headers={"X-API-Key": os.environ["HIRELAYER_API_KEY"]}, files={"file": ("resume.pdf", file, "application/pdf")}, data={ "application_id": "app_123", }, timeout=150, ) response.raise_for_status() data = response.json() print(data["info_candidate"]["full_name"]) ``` **TypeScript** ```typescript import { readFile } from 'node:fs/promises' const form = new FormData() form.append( 'file', new Blob([await readFile('resume.pdf')], { type: 'application/pdf' }), 'resume.pdf' ) form.append('application_id', 'app_123') const response = await fetch('https://onlineresumeparser.com/api/v3/parser', { method: 'POST', headers: { 'X-API-Key': process.env.HIRELAYER_API_KEY!, }, body: form, signal: AbortSignal.timeout(150_000), }) if (!response.ok) { throw new Error(`HireLayer ${response.status}: ${await response.text()}`) } const data = await response.json() console.log(data.info_candidate.full_name) ``` 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://onlineresumeparser.com/api-docs/extract.md#parse-resume-response). Errors return a non-2xx status with an `error` message: see [Errors and retries](https://onlineresumeparser.com/api-docs/errors.md). ## Next steps - [Screening pipeline](https://onlineresumeparser.com/api-docs/screening-pipeline.md): Chain Extract, Job Extract, Rank and Match. - [Build with AI agents](https://onlineresumeparser.com/api-docs/ai-agents.md): Let Claude Code, Codex or Cursor write the integration. ## Next - [Authentication](https://onlineresumeparser.com/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://onlineresumeparser.com/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://onlineresumeparser.com/api-docs/authentication · Markdown: https://onlineresumeparser.com/api-docs/authentication.md Send your key in the `X-API-Key` header of every request. Only the [health check](https://onlineresumeparser.com/api-docs/health.md) is public. ```http POST /api/v1/jobs/extract-criteria HTTP/1.1 Host: onlineresumeparser.com 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://onlineresumeparser.com/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://onlineresumeparser.com/api-docs/limits.md). | ## Next - [Errors and retries](https://onlineresumeparser.com/api-docs/errors.md): Error format, every status code and message, and when to retry a HireLayer API call. - [Limits and credits](https://onlineresumeparser.com/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://onlineresumeparser.com/api-docs/errors · Markdown: https://onlineresumeparser.com/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_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, 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://onlineresumeparser.com/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://onlineresumeparser.com/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` | `Missing required file field` | — | 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/match` | Status | `error` | `code` | Retry | | --- | --- | --- | --- | | `400` | `Provide either 'skill' or 'skills', not both` | — | Do not retry | | `400` | `The 'skill' or 'skills' field is required` | — | Do not retry | | `400` | `The 'skill' field is required` | — | Do not retry | | `400` | `Skill cannot be empty` | — | Do not retry | | `400` | `The 'skills' (list) field is required` | — | Do not retry | | `400` | `The 'skills' field must be a non-empty list` | — | Do not retry | | `400` | `Maximum 100 skills per batch request` | — | Do not retry | | `400` | `top_k must be an integer between 1 and 50` | — | Do not retry | | `400` | `Request body must be a JSON object` | — | 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 | | --- | --- | --- | --- | | `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 | ### `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://onlineresumeparser.com/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://onlineresumeparser.com/api-docs/screening-pipeline.md): Chain HireLayer CV Extract, Job Extract, Rank and Match to parse resumes, score them against a job and explain the result. --- # Page: Limits and credits > How credits are consumed, request size and batch limits, timeouts and concurrency guidance for the HireLayer APIs. Source: https://onlineresumeparser.com/api-docs/limits · Markdown: https://onlineresumeparser.com/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 batch costs 1 credit for up to 100 skills. - 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 | `skills` ≤ 100 items · `top_k` 1–50 | | 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-resume-parser) first. ## Next - [Errors and retries](https://onlineresumeparser.com/api-docs/errors.md): Error format, every status code and message, and when to retry a HireLayer API call. - [Build a screening pipeline](https://onlineresumeparser.com/api-docs/screening-pipeline.md): Chain HireLayer CV Extract, Job Extract, Rank and Match to parse resumes, score them against a job and explain the result. --- # 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://onlineresumeparser.com/api-docs/extract · Markdown: https://onlineresumeparser.com/api-docs/extract.md - **Endpoint:** `POST /api/v3/parser` - **Input:** `multipart/form-data` · 13 file formats · about 4.5 MB max - **Output:** Synchronous JSON, usually in about 35 s (145 s max) - **Billing:** 1 credit per successful parse ## Parse a resume `POST https://onlineresumeparser.com/api/v3/parser` Upload one resume file and receive the structured candidate profile in the same response. - **Authentication:** `X-API-Key` header - **Content type:** `multipart/form-data` - **Billing:** 1 credit per successful parse (HTTP 200) - **Client timeout:** at least 150 seconds ### Request body | Field | Type | Required | Description | | --- | --- | --- | --- | | `file` | `file` | Yes | The resume: PDF, DOC, DOCX, ODT, PPT, PPTX, ODP, XLS, RTF, TXT, JPG/JPEG, PNG or BMP. The type is detected from the content. Keep it under 4.5 MB. | | `application_id` | `string` | No | Your own reference, echoed in `info_resume.application_id`. | | `webhook_url` | `string (URL)` | No | HTTP(S) URL that also receives the result. The response stays synchronous. See [Webhook](#webhook). | | `do_not_store_data` | `string` | No | `true`: the resume file is **not stored**. `info_resume.url` is then `null`, and the cropped photo is only reachable through a temporary link valid 10 minutes. Case-insensitive. Defaults to `false` (file stored). One of: `"true"`, `"false"`. | ### Example request **cURL** ```bash curl -X POST https://onlineresumeparser.com/api/v3/parser \ -H "X-API-Key: $HIRELAYER_API_KEY" \ -F "file=@resume.pdf;type=application/pdf" \ -F "application_id=app_123" ``` **Python** ```python import os import requests with open("resume.pdf", "rb") as file: response = requests.post( "https://onlineresumeparser.com/api/v3/parser", headers={"X-API-Key": os.environ["HIRELAYER_API_KEY"]}, files={"file": ("resume.pdf", file, "application/pdf")}, data={ "application_id": "app_123", }, timeout=150, ) response.raise_for_status() data = response.json() print(data["info_candidate"]["full_name"]) ``` **TypeScript** ```typescript import { readFile } from 'node:fs/promises' const form = new FormData() form.append( 'file', new Blob([await readFile('resume.pdf')], { type: 'application/pdf' }), 'resume.pdf' ) form.append('application_id', 'app_123') const response = await fetch('https://onlineresumeparser.com/api/v3/parser', { method: 'POST', headers: { 'X-API-Key': process.env.HIRELAYER_API_KEY!, }, body: form, signal: AbortSignal.timeout(150_000), }) if (!response.ok) { throw new Error(`HireLayer ${response.status}: ${await response.text()}`) } const data = await response.json() console.log(data.info_candidate.full_name) ``` ### Response `200` Parsed resume. | Field | Type | Description | | --- | --- | --- | | `status` | `string` | Always `success` on HTTP 200. Value: `"success"`. | | `upstream_status` | `string` | May be absent. Present when an optional step was skipped (see `warnings`). The data is still usable. Value: `"partial"`. | | `request_id` | `string` | Unique request ID. Quote it when contacting support. | | `warnings` | `string[]` | Human-readable notes about skipped steps (OCR, photo, geocoding, occupation codes). Informational: do not parse. | | `errors` | `string[]` | Always empty on HTTP 200. Failures use non-2xx statuses. | | `info_resume` | `ResumeInfo` | The document. | | `info_resume.application_id` | `string \| null` | Your `application_id`, or `null` when not sent. | | `info_resume.date_parsing` | `string` | Parsing time in UTC, `YYYY-MM-DDTHH:mm:ss`, without a zone suffix. | | `info_resume.language` | `string \| null` | Main language of the resume, uppercase ISO 639-1 code such as `EN` or `FR`. | | `info_resume.url` | `string \| null` | URL of the stored original file. Anyone with the link can open it. `null` with `do_not_store_data=true`, because the file is not stored. | | `info_resume.face_url` | `string \| null` | Candidate photo cropped from the resume. `null` when no face is found and for `.txt` files. | | `info_resume.face_url_expires_at` | `string \| null` | With `do_not_store_data=true`: expiry of the temporary `face_url`, 10 minutes after parsing, UTC `YYYY-MM-DDTHH:mm:ss`. Otherwise `null` and `face_url` does not expire. | | `info_resume.text` | `string` | Full extracted text, including OCR. May end with `[EXTRACTED_HYPERLINKS]` and `[PDF_ANNOTATION_TEXTS]` sections. | | `info_candidate` | `Candidate` | The candidate profile. | | `info_candidate.full_name` | `string \| null` | First and last name. | | `info_candidate.last_name` | `string \| null` | Last name. | | `info_candidate.first_name` | `string \| null` | First name. | | `info_candidate.email` | `string \| null` | Lower-cased email address. | | `info_candidate.phone_number` | `string \| null` | E.164 phone number, e.g. `+33612345678`. The country is inferred from the resume, France by default. | | `info_candidate.birth_date` | `string \| null` | Date of birth, `YYYY-MM-DD`. A year alone becomes `YYYY-01-01`. | | `info_candidate.age` | `number \| null` | Age in years, as stated or inferred. | | `info_candidate.availability_now` | `boolean \| null` | Whether the candidate is available now. | | `info_candidate.availability_date` | `string \| null` | Date from which the candidate is available, `YYYY-MM-DD`. | | `info_candidate.driver_license` | `string[]` | Driving licences as written, e.g. `Permis B`. | | `info_candidate.job_title` | `string \| null` | Target or current job title. Falls back to the most recent position. | | `info_candidate.education_name` | `string \| null` | Highest qualification relevant to the job title. | | `info_candidate.education_level` | `string \| null` | Highest European Qualifications Framework (EQF) level. One of: `"Level 1"`, `"Level 2"`, `"Level 3"`, `"Level 4"`, `"Level 5"`, `"Level 6"`, `"Level 7"`, `"Level 8"`, `"Other"`. | | `info_candidate.experience_level` | `string \| null` | Total professional experience bracket. One of: `"0 to 1 year"`, `"1 to 3 years"`, `"3 to 5 years"`, `"5 to 10 years"`, `"More than 10 years"`. | | `info_candidate.linkedin_url` | `string \| null` | LinkedIn profile URL. | | `info_candidate.github_url` | `string \| null` | GitHub profile URL. | | `info_candidate.other_urls` | `string[]` | Other URLs in the resume (portfolio, website…). | | `info_candidate.location` | `Location` | Where the candidate lives. | | `info_candidate.location.country` | `string \| null` | Country name. | | `info_candidate.location.country_code` | `string \| null` | ISO 3166-1 alpha-2 country code, e.g. `FR`. | | `info_candidate.location.region` | `string \| null` | Region. | | `info_candidate.location.department` | `string \| null` | Department or county. | | `info_candidate.location.city` | `string \| null` | City. | | `info_candidate.location.postal_code` | `string \| null` | Postal code. | | `info_candidate.location.full_address` | `string \| null` | Address as a single line. | | `info_candidate.location.latitude` | `number \| null` | Latitude from geocoding. | | `info_candidate.location.longitude` | `number \| null` | Longitude from geocoding. | | `info_candidate.mobility` | `Mobility` | Where else the candidate can work. | | `info_candidate.mobility.can_work_in_other_cities` | `boolean \| null` | Whether the resume says the candidate can work elsewhere. `null` when not mentioned. | | `info_candidate.mobility.other_cities` | `MobilityCity[]` | Cities explicitly named as possible work locations. | | `info_candidate.mobility.other_cities[].city` | `string \| null` | City named in the resume. | | `info_candidate.mobility.other_cities[].country` | `string \| null` | Country, when stated or reliably inferred. | | `info_candidate.mobility.other_cities[].postal_code` | `string \| null` | Postal code, when present in the resume. | | `info_candidate.mobility.other_cities[].latitude` | `number \| null` | Latitude from geocoding. | | `info_candidate.mobility.other_cities[].longitude` | `number \| null` | Longitude from geocoding. | | `work_experiences` | `WorkExperience[]` | | | `work_experiences[].company_name` | `string \| null` | Employer name. | | `work_experiences[].job_title` | `string \| null` | Job title for this position. | | `work_experiences[].description` | `string \| null` | Tasks, responsibilities and achievements. | | `work_experiences[].contract_type` | `string \| null` | Canonical contract type. Unrecognized values become `Other`. One of: `"Permanent contract"`, `"Fixed-term contract"`, `"Temporary assignment"`, `"Internship"`, `"Apprenticeship"`, `"Freelance"`, `"Volunteering"`, `"Other"`. | | `work_experiences[].start_date` | `string \| null` | Start date, `YYYY-MM-DD`. A year alone becomes `YYYY-01-01`; a month alone, its first day. | | `work_experiences[].end_date` | `string \| null` | End date, `YYYY-MM-DD`. `null` for an ongoing role. A year alone becomes `YYYY-12-31`; a month alone, its last day. | | `work_experiences[].currently_active` | `boolean \| null` | Whether this is a current position. | | `work_experiences[].work_experience_country` | `string \| null` | Country of the position. | | `work_experiences[].work_experience_country_code` | `string \| null` | ISO 3166-1 alpha-2 country code. | | `work_experiences[].work_experience_city` | `string \| null` | City of the position. | | `work_experiences[].work_experience_postal_code` | `string \| null` | Postal code of the position. | | `work_experiences[].experience_duration` | `number \| null` | Duration of this position in months, as extracted. | | `educations` | `Education[]` | | | `educations[].degree_title` | `string \| null` | Name of the degree or programme. | | `educations[].school_name` | `string \| null` | School or institution. | | `educations[].description` | `string \| null` | Details of the programme. | | `educations[].degree_type` | `string \| null` | EQF level of the degree, expected to be one of `Level 1`…`Level 8` or `Other`. Not normalized: treat other strings as `Other`. | | `educations[].start_date` | `string \| null` | Start date, `YYYY-MM-DD`. | | `educations[].end_date` | `string \| null` | End date, `YYYY-MM-DD`. | | `educations[].currently_active` | `boolean \| null` | Whether the programme is in progress. | | `educations[].location` | `EducationLocation` | Location of the school. | | `educations[].location.country` | `string \| null` | Country name. | | `educations[].location.country_code` | `string \| null` | ISO 3166-1 alpha-2 country code, e.g. `FR`. | | `educations[].location.region` | `string \| null` | Region. | | `educations[].location.department` | `string \| null` | Department or county. | | `educations[].location.city` | `string \| null` | City. | | `educations[].location.postal_code` | `string \| null` | Postal code. | | `educations[].location.full_address` | `string \| null` | Address as a single line. | | `rome_jobs` | `RomeJob[]` | Occupations predicted from `info_candidate.job_title` with the French ROME taxonomy. | | `rome_jobs[].job_title` | `string` | Occupation label, in French. | | `rome_jobs[].job_code` | `string` | Occupation (appellation) code. | | `rome_jobs[].rome_title` | `string` | ROME job family label, in French. | | `rome_jobs[].rome_code` | `string` | ROME code, e.g. `M1805`. | | `rome_jobs[].prediction_score` | `number` | Confidence between 0.7 and 1. | | `languages` | `Language[]` | | | `languages[].language` | `string` | Language name as written in the resume. | | `languages[].level` | `string \| null` | CEFR-based proficiency, or `null` when not stated. One of: `"Native or Bilingual (C2)"`, `"Full Professional Proficiency (C1)"`, `"Professional Working Proficiency (B2)"`, `"Limited Working Proficiency (B1)"`, `"Advanced Basic Proficiency (A2)"`, `"Introductory Proficiency (A1)"`. | | `skills` | `Skill[]` | | | `skills[].skill_title` | `string` | Catalog label when `status` is `normalized`, otherwise the text from the resume. | | `skills[].skill_type` | `string` | One of: `"Hard skill"`, `"Soft skill"`, `"Software skill"`. | | `skills[].status` | `string` | `normalized`: matched to the [HireLayer Skills catalog](https://onlineresumeparser.com/api-docs/skills.md). `raw`: no close catalog match. One of: `"normalized"`, `"raw"`. | | `skills[].domain` | `string \| null` | Catalog domain. `null` when `status` is `raw`. | | `skills[].subcategory` | `string \| null` | Catalog subcategory. `null` when `status` is `raw`. | | `certifications` | `string[]` | | | `interests` | `string[]` | | ```json { "status": "success", "request_id": "6f1c2a9e-4b7d-4c3e-9a51-2f8d7e6b1c04", "warnings": [], "errors": [], "info_resume": { "application_id": "app_123", "date_parsing": "2026-10-01T09:30:12", "language": "EN", "url": "https://files.example.com/original/1790847012000-alex-morgan.pdf", "face_url": "https://files.example.com/faces/1790847012000-alex-morgan.jpg", "face_url_expires_at": null, "text": "Alex Morgan\nSenior Software Engineer\nParis, France\nalex.morgan@example.com\n\nSenior software engineer with 8 years of experience building web platforms…" }, "info_candidate": { "full_name": "Alex Morgan", "last_name": "Morgan", "first_name": "Alex", "email": "alex.morgan@example.com", "phone_number": "+33612345678", "birth_date": "1992-04-18", "age": 34, "availability_now": false, "availability_date": "2026-11-01", "driver_license": [ "Permis B" ], "job_title": "Senior Software Engineer", "education_name": "Master of Science in Computer Science", "education_level": "Level 7", "experience_level": "5 to 10 years", "linkedin_url": "https://www.linkedin.com/in/alex-morgan", "github_url": "https://github.com/alexmorgan", "other_urls": [ "https://alexmorgan.dev" ], "location": { "country": "France", "country_code": "FR", "region": "Île-de-France", "department": "Paris", "city": "Paris", "postal_code": "75011", "full_address": "75011 Paris, France", "latitude": 48.8589, "longitude": 2.3801 }, "mobility": { "can_work_in_other_cities": true, "other_cities": [ { "city": "Lyon", "country": "France", "postal_code": null, "latitude": 45.764, "longitude": 4.8357 } ] } }, "work_experiences": [ { "company_name": "Northstar Labs", "job_title": "Senior Software Engineer", "description": "Lead a team of five engineers building a TypeScript and React SaaS platform.", "contract_type": "Permanent contract", "start_date": "2022-03-01", "end_date": null, "currently_active": true, "work_experience_country": "France", "work_experience_country_code": "FR", "work_experience_city": "Paris", "work_experience_postal_code": null, "experience_duration": 55 }, { "company_name": "Atelier Digital", "job_title": "Software Engineer", "description": "Built Node.js APIs and data pipelines for recruitment clients.", "contract_type": "Permanent contract", "start_date": "2018-09-01", "end_date": "2022-02-28", "currently_active": false, "work_experience_country": "France", "work_experience_country_code": "FR", "work_experience_city": "Paris", "work_experience_postal_code": null, "experience_duration": 42 } ], "educations": [ { "degree_title": "Master of Science in Computer Science", "school_name": "École Polytechnique", "description": "Distributed systems and software architecture.", "degree_type": "Level 7", "start_date": "2014-09-01", "end_date": "2016-06-30", "currently_active": false, "location": { "country": "France", "country_code": "FR", "region": "Île-de-France", "department": "Essonne", "city": "Palaiseau", "postal_code": "91120", "full_address": "Palaiseau, France" } } ], "rome_jobs": [ { "job_title": "Ingénieur / Ingénieure logiciel", "job_code": "38971", "rome_title": "Études et développement informatique", "rome_code": "M1805", "prediction_score": 0.93 } ], "languages": [ { "language": "English", "level": "Native or Bilingual (C2)" }, { "language": "French", "level": "Professional Working Proficiency (B2)" } ], "skills": [ { "skill_title": "React", "skill_type": "Hard skill", "status": "normalized", "domain": "Technologie", "subcategory": "Languages & Frameworks" }, { "skill_title": "Typescript", "skill_type": "Hard skill", "status": "normalized", "domain": "Technologie", "subcategory": "Languages & Frameworks" }, { "skill_title": "Figma", "skill_type": "Software skill", "status": "normalized", "domain": "Design & Contenu", "subcategory": "Logiciel" }, { "skill_title": "Technical leadership", "skill_type": "Soft skill", "status": "raw", "domain": null, "subcategory": null } ], "certifications": [ "AWS Certified Developer – Associate" ], "interests": [ "Open-source software", "Climbing" ] } ``` ### Errors | Status | `error` | `code` | When | Retry | | --- | --- | --- | --- | --- | | `400` | `Missing required file field` | — | No multipart part named `file`. | Do not retry | | `400` | `application_id must be a string when provided` | — | `application_id` was sent as a file part. | Do not retry | | `400` | `The uploaded file is empty or invalid. Please check the file and try again.` | `INVALID_FILE` | Empty file, unsupported format, content that does not match its type, or a `do_not_store_data` value other than `true`/`false`. | Do not retry | | `401` | `Missing API Key` | — | The `X-API-Key` header is absent. `Authorization: Bearer` is not accepted. | Do not retry | | `401` | `Invalid API Key` | — | The key is malformed, unknown or revoked. Rotating a key revokes the previous one immediately. | Do not retry | | `403` | `Insufficient credits available` | — | The account has no credit left. | Fix, then retry | | `413` | `The uploaded file is too large to process. Please upload a smaller file.` | — | The encoded upload exceeds 6 MiB (a file of about 4.5 MB). | Do not retry | | `415` | `Content-Type must be multipart/form-data` | — | The request is not `multipart/form-data`. | Do not retry | | `422` | `This document does not appear to be a CV or resume. Please upload a CV or resume and try again.` | `DOCUMENT_NOT_A_RESUME` | The document is clearly not a resume (cover letter, ID, invoice…). | Do not retry | | `422` | `The text could not be extracted from this document. Please verify that the file is readable and contains selectable text.` | `DOCUMENT_TEXT_EMPTY` | No text was found, even with OCR. | Do not retry | | `422` | `The document could not be read. Please upload a valid, readable file.` | `DOCUMENT_UNREADABLE` | The file is corrupted or cannot be opened. | Do not retry | | `422` | `This document contains too much text to process. Please try a shorter or simpler version.` | `DOCUMENT_TOO_LARGE` | More than 100,000 characters of text were extracted. | Do not retry | | `500` | `Internal Server Error` | — | Unexpected failure. | Retry with backoff | | `502` | `An error occurred while processing the document or extracting its text. Please try again later.` | `PARSER_UNAVAILABLE` | The parser failed or returned an invalid result. | Retry with backoff | | `503` | `An error occurred while processing the document or extracting its text. Please try again later.` | `PARSER_UNAVAILABLE` | A processing step (text extraction, OCR, model) is temporarily unavailable. Headers: `Retry-After: 1`. | Retry with backoff | | `503` | `Credit service temporarily unavailable` | — | Credits could not be checked or deducted. No credit is consumed, including when the operation itself had completed. Headers: `Retry-After: 1`. | Retry with backoff | | `504` | `An error occurred while processing the document or extracting its text. Please try again later.` | `PARSER_UNAVAILABLE` | Parsing did not finish within 145 seconds. | Retry with backoff | Example error (`400`): ```json { "error": "The uploaded file is empty or invalid. Please check the file and try again.", "code": "INVALID_FILE" } ``` ### Behavior - **Latency:** Synchronous. Parsing usually takes about 35 seconds; scans that need OCR take longer. The gateway waits up to 145 seconds, then returns `504`. Use a client timeout of at least 150 seconds. - **Retries:** Retry `502`, `503` and `504` with exponential backoff and honour `Retry-After`. Never retry `4xx` unchanged. Failed requests are not charged. - **Idempotency:** There is no idempotency key. A request your client abandons can still complete and be charged: do not use a client timeout shorter than the gateway timeout. - **Partial results:** When an optional step is skipped (OCR of some pages, photo, geocoding, occupation codes), the response is still `200` with `upstream_status: "partial"` and a note in `warnings`. - **Documents:** 13 formats (PDF, DOC, DOCX, ODT, PPT, PPTX, ODP, XLS, RTF, TXT, JPG/JPEG, PNG and BMP). All pages are read; scanned documents are OCR'd on their first 4 pages. Up to 100,000 extracted characters. - **Languages:** Free-text values stay in the language of the resume (no translation). Enumerated fields use the fixed English values of the response schema. `rome_jobs` labels are in French. - **Request ID:** Successful responses carry `request_id`; error responses carry the `x-parser-request-id` header. Quote them when contacting support. > **Beta: Fast mode (beta).** A faster parsing mode is in beta and is enabled per account on request: [contact us](https://cal.com/resumeparser/demo-resume-parser). It has no request parameter, and its timing is not published yet. ## Webhook When `webhook_url` is set, the result is also posted there as JSON, before the HTTP response is returned. Use it only as a convenience: the HTTP response remains the source of truth. - Sent only for successful parses, with `Content-Type: application/json` and no signature header. Add a secret token to the URL and check it on receipt. - Payload: the parsed resume. Unlike the HTTP response, `status` is `"partial"` when some steps were skipped, and `upstream_status` is absent. - 2-second timeout per attempt, up to 3 attempts on timeouts, network errors, `429` and `5xx`. A webhook failure does not change the HTTP response. - The webhook can arrive even when the HTTP request ends with an error (for example `403` when credits run out during the call). Reconcile with your own `application_id`. ## Data storage With `do_not_store_data=true`, HireLayer does **not store the resume file**. The parsed data is returned in the response as usual. | | `do_not_store_data=false` (default) | `do_not_store_data=true` | | --- | --- | --- | | Original file | Stored. `info_resume.url` links to it. | **Not stored.** `info_resume.url` is `null`. | | Candidate photo | Stored. `face_url` does not expire. | Temporary link valid 10 minutes (`face_url_expires_at`). | | Parsed data | Returned in the response. | Returned in the response. | > **Warning: Stored files are reachable by URL.** Anyone with `info_resume.url` or a non-expiring `face_url` can open the file. Treat these URLs as personal data, or send `do_not_store_data=true` and keep your own copy. ## Legacy V2 Existing integrations can keep using the asynchronous [V2 contract](https://onlineresumeparser.com/api-docs/extract-v2.md). New integrations must use V3. ## Next - [HireLayer Job Extract](https://onlineresumeparser.com/api-docs/job-extract.md): Extract explicit, weighted and mandatory criteria from a job description, ready to send to HireLayer Match. - [Build a screening pipeline](https://onlineresumeparser.com/api-docs/screening-pipeline.md): Chain HireLayer CV Extract, Job Extract, Rank and Match to parse resumes, score them against a job and explain the result. --- # Page: HireLayer Job Extract > Extract explicit, weighted and mandatory criteria from a job description, ready to send to HireLayer Match. Source: https://onlineresumeparser.com/api-docs/job-extract · Markdown: https://onlineresumeparser.com/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://onlineresumeparser.com/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://onlineresumeparser.com/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://onlineresumeparser.com/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://onlineresumeparser.com/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://onlineresumeparser.com/api-docs/match.md). You can also edit, remove or write criteria yourself. ## Next - [HireLayer Match](https://onlineresumeparser.com/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://onlineresumeparser.com/api-docs/screening-pipeline.md): Chain HireLayer CV Extract, Job Extract, Rank and Match to parse resumes, score them against a job and explain the result. --- # Page: HireLayer Match > Evaluate a resume against job criteria, criterion by criterion, with an explained status and a weighted 0–1 score. Source: https://onlineresumeparser.com/api-docs/match · Markdown: https://onlineresumeparser.com/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://onlineresumeparser.com/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://onlineresumeparser.com/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://onlineresumeparser.com/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://onlineresumeparser.com/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://onlineresumeparser.com/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://onlineresumeparser.com/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://onlineresumeparser.com/api-docs/screening-pipeline.md): Chain HireLayer CV Extract, Job Extract, Rank and Match to parse resumes, score them against a job and explain the result. --- # 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://onlineresumeparser.com/api-docs/rank · Markdown: https://onlineresumeparser.com/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://onlineresumeparser.com/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://onlineresumeparser.com/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://onlineresumeparser.com/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://onlineresumeparser.com/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://onlineresumeparser.com/api-docs/match.md) against the same criteria, which gives comparable scores. ## Next - [HireLayer Match](https://onlineresumeparser.com/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://onlineresumeparser.com/api-docs/screening-pipeline.md): Chain HireLayer CV Extract, Job Extract, Rank and Match to parse resumes, score them against a job and explain the result. --- # Page: HireLayer Skills > Normalize free-text skills against the HireLayer catalog, one at a time or in batches of 100, and download the catalog. Source: https://onlineresumeparser.com/api-docs/skills · Markdown: https://onlineresumeparser.com/api-docs/skills.md - **Endpoints:** `POST /api/v1/skills/match` · `GET /api/v1/skills` - **Input:** JSON · one skill, or up to 100 - **Output:** Closest catalog skills with a similarity score - **Billing:** 1 credit per successful call, single or batch ## Match skills to the catalog `POST https://onlineresumeparser.com/api/v1/skills/match` Map free-text skills, one or up to 100 at a time, to the closest catalog skills. - **Authentication:** `X-API-Key` header - **Content type:** `application/json` - **Billing:** 1 credit per successful call, single or batch - **Client timeout:** at least 65 seconds ### Request body | Field | Type | Required | Description | | --- | --- | --- | --- | | `skill` | `string` | No | One free-text skill. Send `skill` or `skills`, not both. Non-empty. | | `skills` | `string[]` | No | Batch of free-text skills. Non-string items are converted with `String()`. 1–100 items. | | `top_k` | `integer` | No | Matches per skill. Defaults to `5` with `skill`, `3` with `skills`. 1–50. | Send exactly one of `skill` or `skills`. ### Example request **cURL** ```bash curl -X POST https://onlineresumeparser.com/api/v1/skills/match \ -H "X-API-Key: $HIRELAYER_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "skill": "react js", "top_k": 3 }' ``` **Python** ```python import os import requests response = requests.post( "https://onlineresumeparser.com/api/v1/skills/match", headers={"X-API-Key": os.environ["HIRELAYER_API_KEY"]}, json={ "skill": "react js", "top_k": 3, }, timeout=65, ) response.raise_for_status() data = response.json() print(data["results"]) ``` **TypeScript** ```typescript const response = await fetch('https://onlineresumeparser.com/api/v1/skills/match', { method: 'POST', headers: { 'X-API-Key': process.env.HIRELAYER_API_KEY!, 'Content-Type': 'application/json', }, body: JSON.stringify({ skill: 'react js', top_k: 3, }), 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.results) ``` ### Response `200` Single mode (`skill`). | Field | Type | Description | | --- | --- | --- | | `query_skill` | `string` | Your skill, trimmed. | | `total_results` | `integer` | Number of `results`, at most `top_k`. | | `results` | `SkillMatch[]` | Closest catalog skills, best first. | | `results[].rank` | `integer` | Position, 1 is the closest. ≥ 1. | | `results[].skill` | `string` | Catalog label. | | `results[].similarity_score` | `number` | Cosine similarity rounded to 4 decimals. `1` for an exact label or synonym match. ≤ 1. | | `results[].domain` | `string` | Catalog domain, e.g. `Technologie`. | | `results[].subcategory` | `string` | Catalog subcategory. Empty string when the skill has none. | ```json { "query_skill": "react js", "total_results": 3, "results": [ { "rank": 1, "skill": "React", "similarity_score": 0.8712, "domain": "Technologie", "subcategory": "Languages & Frameworks" }, { "rank": 2, "skill": "React Native", "similarity_score": 0.7934, "domain": "Technologie", "subcategory": "Languages & Frameworks" }, { "rank": 3, "skill": "Javascript", "similarity_score": 0.7121, "domain": "Technologie", "subcategory": "Languages & Frameworks" } ] } ``` ### Response `200` Batch mode (`skills`). | Field | Type | Description | | --- | --- | --- | | `total_queries` | `integer` | Number of skills sent. | | `successful_matches` | `integer` | Number of items with `success: true`. | | `results` | `SkillBatchItem[]` | One item per input skill, in input order. Duplicates are kept. | | `results[].query_skill` | `string` | The input skill. | | `results[].success` | `boolean` | | | `results[].results` | `SkillMatch[]` | May be absent. Present when `success` is `true`. | | `results[].results[].rank` | `integer` | Position, 1 is the closest. ≥ 1. | | `results[].results[].skill` | `string` | Catalog label. | | `results[].results[].similarity_score` | `number` | Cosine similarity rounded to 4 decimals. `1` for an exact label or synonym match. ≤ 1. | | `results[].results[].domain` | `string` | Catalog domain, e.g. `Technologie`. | | `results[].results[].subcategory` | `string` | Catalog subcategory. Empty string when the skill has none. | | `results[].error` | `string` | May be absent. Present when `success` is `false`: `Empty skill`, or `The AI processing step failed. Please try again later.` when matching failed (retry this skill). | ### Batch request Send `skills` to match up to 100 values in one call. Each item succeeds or fails on its own; the HTTP status stays `200`. **cURL** ```bash curl -X POST https://onlineresumeparser.com/api/v1/skills/match \ -H "X-API-Key: $HIRELAYER_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "skills": [ "python", "gestion de projets", " " ], "top_k": 1 }' ``` **Python** ```python import os import requests response = requests.post( "https://onlineresumeparser.com/api/v1/skills/match", headers={"X-API-Key": os.environ["HIRELAYER_API_KEY"]}, json={ "skills": [ "python", "gestion de projets", " ", ], "top_k": 1, }, timeout=65, ) response.raise_for_status() data = response.json() print(data["results"]) ``` **TypeScript** ```typescript const response = await fetch('https://onlineresumeparser.com/api/v1/skills/match', { method: 'POST', headers: { 'X-API-Key': process.env.HIRELAYER_API_KEY!, 'Content-Type': 'application/json', }, body: JSON.stringify({ skills: [ 'python', 'gestion de projets', ' ', ], top_k: 1, }), 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.results) ``` ```json { "total_queries": 3, "successful_matches": 2, "results": [ { "query_skill": "python", "success": true, "results": [ { "rank": 1, "skill": "Python", "similarity_score": 1, "domain": "Technologie", "subcategory": "Languages & Frameworks" } ] }, { "query_skill": "gestion de projets", "success": true, "results": [ { "rank": 1, "skill": "Gestion de projet - PMO", "similarity_score": 1, "domain": "Business", "subcategory": "" } ] }, { "query_skill": " ", "success": false, "error": "Empty skill" } ] } ``` ### Errors | Status | `error` | `code` | When | Retry | | --- | --- | --- | --- | --- | | `400` | `Provide either 'skill' or 'skills', not both` | — | Both keys are present, even if one is `null`. | Do not retry | | `400` | `The 'skill' or 'skills' field is required` | — | Neither key is present. | Do not retry | | `400` | `The 'skill' field is required` | — | `skill` is not a string. | Do not retry | | `400` | `Skill cannot be empty` | — | `skill` is empty after trimming. | Do not retry | | `400` | `The 'skills' (list) field is required` | — | `skills` is not an array. | Do not retry | | `400` | `The 'skills' field must be a non-empty list` | — | `skills` is `[]`. | Do not retry | | `400` | `Maximum 100 skills per batch request` | — | `skills` has more than 100 items. | Do not retry | | `400` | `top_k must be an integer between 1 and 50` | — | `top_k` is not a JSON integer from 1 to 50 (`"5"` and `null` are rejected). | Do not 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 | | `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": "Provide either 'skill' or 'skills', not both" } ``` ### Behavior - **Latency:** Synchronous. The gateway waits up to 60 seconds; use a client timeout of at least 65 seconds. - **Exact matches:** When the normalized text (case, accents and punctuation ignored) equals a catalog label or synonym, that skill is ranked first with `similarity_score: 1`. Other results come from semantic similarity. - **Thresholds:** Results are always returned, even weak ones. Choose your own `similarity_score` threshold; HireLayer CV Extract uses 0.75 to mark a skill as `normalized`. - **Unknown fields:** Unlike the other endpoints, unknown body fields are ignored. ## List the skill catalog `GET https://onlineresumeparser.com/api/v1/skills` Download the whole reference catalog used for matching. - **Authentication:** `X-API-Key` header - **Billing:** 1 credit per successful call - **Client timeout:** at least 65 seconds ### Example request **cURL** ```bash curl https://onlineresumeparser.com/api/v1/skills \ -H "X-API-Key: $HIRELAYER_API_KEY" ``` **Python** ```python import os import requests response = requests.get( "https://onlineresumeparser.com/api/v1/skills", headers={"X-API-Key": os.environ["HIRELAYER_API_KEY"]}, timeout=65, ) response.raise_for_status() data = response.json() print(data["total_skills"]) ``` **TypeScript** ```typescript const response = await fetch('https://onlineresumeparser.com/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_skills) ``` ### Response `200` The catalog. | Field | Type | Description | | --- | --- | --- | | `total_skills` | `integer` | Number of skills in the catalog. | | `skills` | `CatalogSkill[]` | The whole catalog. There is no pagination. | | `skills[].skill` | `string` | Catalog label. | | `skills[].domain` | `string` | Catalog domain, e.g. `Technologie`. | | `skills[].subcategory` | `string` | Catalog subcategory. Empty string when the skill has none. | | `skills[].rank` | `integer` | 1-based position in the catalog. Not a relevance score. ≥ 1. | ```json { "total_skills": 664, "skills": [ { "skill": "GMAO", "domain": "Business", "subcategory": "", "rank": 1 }, { "skill": "Gantt", "domain": "Business", "subcategory": "", "rank": 2 }, { "skill": "Figma", "domain": "Design & Contenu", "subcategory": "Logiciel", "rank": 180 }, { "skill": "React", "domain": "Technologie", "subcategory": "Languages & Frameworks", "rank": 499 } ] } ``` ### 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 | | `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 (`401`): ```json { "error": "Missing API Key" } ``` ### Behavior - **Caching:** The catalog changes rarely and each call costs a credit: cache it on your side, for example once a day. - **Labels:** Labels and domains are mostly French (`Technologie`, `Ressources Humaines`, `Design & Contenu`…). Query parameters are ignored. ## Next - [HireLayer CV Extract](https://onlineresumeparser.com/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://onlineresumeparser.com/api-docs/screening-pipeline.md): Chain HireLayer CV Extract, Job Extract, Rank and Match to parse resumes, score them against a job and explain the result. --- # 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://onlineresumeparser.com/api-docs/screening-pipeline · Markdown: https://onlineresumeparser.com/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://onlineresumeparser.com/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://onlineresumeparser.com/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://onlineresumeparser.com/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://onlineresumeparser.com/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 matched to the catalog when `status` is `normalized`. Send the `raw` ones, or skills typed by users, to [Skills](https://onlineresumeparser.com/api-docs/skills.md) in batches of up to 100. ## 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://onlineresumeparser.com/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://onlineresumeparser.com/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://onlineresumeparser.com/api-docs/errors.md): Error format, every status code and message, and when to retry a HireLayer API call. - [Build with AI agents](https://onlineresumeparser.com/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://onlineresumeparser.com/api-docs/ai-agents · Markdown: https://onlineresumeparser.com/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://onlineresumeparser.com/llms.txt) | Index of every page with a one-line summary, following the llms.txt convention. | | [`/llms-full.txt`](https://onlineresumeparser.com/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://onlineresumeparser.com/api-docs/extract.md). | | [`/openapi.json`](https://onlineresumeparser.com/openapi.json) · [`/openapi.yaml`](https://onlineresumeparser.com/openapi.yaml) | OpenAPI 3.1 description of every endpoint, schema and error, for code generators, Postman or Insomnia. | | [`/api-docs/agent-rules.md`](https://onlineresumeparser.com/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://onlineresumeparser.com/api-docs/extract.md and https://onlineresumeparser.com/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://onlineresumeparser.com/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://onlineresumeparser.com/api`. HTTPS only. There is no official SDK and no MCP server: call the REST API directly (Python `requests`, Node.js 18+ `fetch`). - 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. - 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_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 and Rank reject unknown fields with 400. Text fields are trimmed and limited to 50,000 characters. Skills match takes `skill` or `skills` (≤ 100 items), never both. - 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://onlineresumeparser.com/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://onlineresumeparser.com/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://onlineresumeparser.com/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://onlineresumeparser.com/api-docs/rank.md - `POST /api/v1/skills/match` — HireLayer Skills: Map free-text skills, one or up to 100 at a time, to the closest catalog skills. (JSON body, client timeout ≥ 65 s). Docs: https://onlineresumeparser.com/api-docs/skills.md - `GET /api/v1/skills` — HireLayer Skills: Download the whole reference catalog used for matching. (no body, client timeout ≥ 65 s). Docs: https://onlineresumeparser.com/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://onlineresumeparser.com/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://onlineresumeparser.com/api-docs/extract-v2.md ## References - Index for agents: https://onlineresumeparser.com/llms.txt - Full documentation in one file: https://onlineresumeparser.com/llms-full.txt - OpenAPI 3.1: https://onlineresumeparser.com/openapi.json ``` ## SDKs and MCP HireLayer has no SDK and no MCP server: 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://onlineresumeparser.com/openapi.json). ## Next - [Quickstart](https://onlineresumeparser.com/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://onlineresumeparser.com/api-docs/screening-pipeline.md): Chain HireLayer CV Extract, Job Extract, Rank and Match to parse resumes, score them against a job and explain the result. --- # Page: Health check > A public endpoint for uptime monitors and deployment checks. No API key and no credit needed. Source: https://onlineresumeparser.com/api-docs/health · Markdown: https://onlineresumeparser.com/api-docs/health.md ## Check API health `GET https://onlineresumeparser.com/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://onlineresumeparser.com/api/v1/health ``` **Python** ```python import requests response = requests.get( "https://onlineresumeparser.com/api/v1/health", timeout=10, ) response.raise_for_status() data = response.json() print(data["status"]) ``` **TypeScript** ```typescript const response = await fetch('https://onlineresumeparser.com/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"`. | | `skills_loaded` | `integer` | Number of skills in the loaded catalog. | | `timestamp` | `string (date-time)` | Server time, ISO 8601 UTC. | ```json { "status": "healthy", "service": "HireLayer API", "skills_loaded": 664, "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. ## Next - [Quickstart](https://onlineresumeparser.com/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://onlineresumeparser.com/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://onlineresumeparser.com/api-docs/extract-v2 · Markdown: https://onlineresumeparser.com/api-docs/extract-v2.md > **Warning: Legacy contract.** V2 is maintained for existing integrations only. [HireLayer CV Extract V3](https://onlineresumeparser.com/api-docs/extract.md) is synchronous, supports more formats and returns typed errors. ## Submit a resume (legacy V2) `POST https://onlineresumeparser.com/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://onlineresumeparser.com/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://onlineresumeparser.com/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://onlineresumeparser.com/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://onlineresumeparser.com/api-docs/extract.md): Parse a resume file into structured candidate JSON with one multipart request: contact details, experience, education, languages and typed skills.