Guides
Migrate from HrFlow.ai
Keep the code that reads HrFlow.ai profiles: point HrFlow.ai’s SDK at HireLayer, or call the Resume Parser with output_format=hrflow. Field mapping, errors and webhooks.
HireLayer answers in HrFlow.ai’s JSON format, so the code that reads your profiles stays. Choose how you call it: keep HrFlow.ai’s SDK and change two settings, or call the Resume Parser with output_format=hrflow. Both parse, bill and store the resume the same way: 1 credit, the resume stored in the workspace under its application_id.
| HrFlow.ai-compatible API | output_format=hrflow | |
|---|---|---|
| Call | POST /api/hrflow/v1/profile/parsing/file | POST /api/v4/resumes/parse?output_format=hrflow |
| For | HrFlow.ai’s SDK, or HTTP calls written for HrFlow.ai | New calls to HireLayer, read with your HrFlow.ai code |
| What changes | The API URL and the key | The call; the code that reads the result stays |
| Webhooks | None: every parse answers 201 | webhook_url, signed |
Set the API URL to https://hirelayer.co/api/hrflow/v1/ and the secret to your HireLayer key. The parsing call and its answer keep HrFlow.ai’s format.
import os
from hrflow import Hrflow
client = Hrflow(
api_url="https://hirelayer.co/api/hrflow/v1/",
api_secret=os.environ["HIRELAYER_API_KEY"],
api_user="you@company.com", # ignored
)
with open("resume.pdf", "rb") as pdf:
response = client.profile.parsing.add_file(
source_key="acme",
profile_file=pdf,
reference="cand_123",
sync_parsing=1,
)
profile = response["data"]["profile"]curl -X POST https://hirelayer.co/api/hrflow/v1/profile/parsing/file \
-H "X-API-KEY: $HIRELAYER_API_KEY" \
-F "source_key=acme" \
-F "reference=cand_123" \
-F "file=@resume.pdf"| HrFlow.ai request | At HireLayer |
|---|---|
X-API-KEY header | Your HireLayer key |
X-USER-EMAIL header | Ignored |
file | The resume |
source_key | The workspace: each Source keeps its profiles apart. Absent, default. |
reference, else key | The resume’s application_id, when it is a valid id; else one derived from the file. reference comes back as sent. |
tags, metadatas, labels, created_at | Back in the profile, your tags before HireLayer’s |
sync_parsing and the other fields | Ignored: the answer is always the real-time 201 |
Only parsing is served. Every other path under /api/hrflow/v1/ (profile retrieval, search, scoring, Sources) answers 404 in HrFlow.ai’s format. If your Source parses asynchronously (202, then a webhook or a GET), read the 201 body instead.
Add ?output_format=hrflow to POST /api/v4/resumes/parse: the same parse, answered 201 with HrFlow.ai’s body. output_format=hirelayer, the default, answers the Resume Parser’s own JSON. The legacy /api/v3/parser takes the same parameter and gives the same body.
curl -X POST -g "https://hirelayer.co/api/v4/resumes/parse?output_format=hrflow" \
-H "X-API-Key: $HIRELAYER_API_KEY" \
-F "file=@resume.pdf;type=application/pdf"import os
import requests
with open("resume.pdf", "rb") as file:
response = requests.post(
"https://hirelayer.co/api/v4/resumes/parse?output_format=hrflow",
headers={"X-API-Key": os.environ["HIRELAYER_API_KEY"]},
files={"file": ("resume.pdf", file, "application/pdf")},
timeout=150,
)
response.raise_for_status()
data = response.json()
print(data["data"]["profile"]["info"])import { readFile } from 'node:fs/promises'
const form = new FormData()
form.append(
'file',
new Blob([await readFile('resume.pdf')], { type: 'application/pdf' }),
'resume.pdf'
)
const response = await fetch('https://hirelayer.co/api/v4/resumes/parse?output_format=hrflow', {
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.data.profile.info){
"code": 201,
"message": "Profile parsed successfully.",
"data": {
"parsing": {
"key": "file_d7e21d373637a897b17b8fdf63122807",
"…": "…"
},
"profile": {
"key": "file_d7e21d373637a897b17b8fdf63122807",
"reference": "file_d7e21d373637a897b17b8fdf63122807",
"source_key": null,
"info": {
"full_name": "Elias MOREAU",
"email": "elias.moreau@example.com",
"…": "…"
},
"…": "…"
}
}
}The full answer is the 201 example of Parse a resume. Send HireLayer-Workspace to name the profile’s Source (source_key). An unknown output_format is refused with 400 invalid_request before anything is billed.
| HrFlow.ai field | Value |
|---|---|
key | The resume’s application_id; without storage, the parse’s request_id. Not a 40-character HrFlow.ai key. |
reference | Your application_id (or reference through the compatible API). |
source_key, source.key | The workspace, when you send HireLayer-Workspace (or source_key); else null. |
created_at, updated_at | Parsing time, UTC: 2026-10-26T22:38:42+0000. |
text, text_language | The full text; its language as a lowercase ISO 639-1 code. |
info.full_name, first_name, last_name, email | As in the resume. |
info.phone | E.164 (+33612345678), where HrFlow.ai keeps the number as written. |
info.date_birth, info.driving_license | Date of birth; licences joined with , . |
info.location | text, lat, lng, gmaps and the 21 fields keys; fields.country is ISO 3166-1 alpha-3 (FRA). |
info.urls[] | The LinkedIn, GitHub and other links found, typed by host. |
info.picture | The candidate’s photo cut from the resume, a link that expires. |
experiences[], educations[] | Company or school, title, description, location (text only), date_start and date_end as 2019-05-01T00:00:00, a key per entry. |
experiences_duration, educations_duration | Years over the periods with an end date, overlaps counted once. |
skills[] | {name, type, value: null}: hard and software skills are hard, soft skills soft; names in lowercase. |
languages[] | name as written, in lowercase; value the CEFR level (HrFlow.ai: null). |
certifications[], interests[] | {name, value: null}, in lowercase. |
attachments[] | The original file, or empty without storage. |
tags[] | Values HrFlow.ai has no field for: job_title, education_name, education_level, experience_level, age, availability_now, driver_license, can_work_in_other_cities, mobility_city, rome_code, rome_appellation_code. |
ranges[] | availability, when the resume gives a date. |
data.parsing holds the same values in HrFlow.ai’s parsing layout: dates as {iso8601, text, timestamp}, persons, emails, phones and urls lists, and is_active on each experience and education.
Errors keep their HTTP status and headers, the request id included, with HrFlow.ai’s body: code is the status, message the HireLayer message, error_code the HireLayer error code to branch on.
{
"code": 401,
"message": "Invalid API key: it is unknown, malformed or revoked.",
"error_code": "invalid_api_key"
}With webhook_url and output_format=hrflow, the call answers the usual 202, and the resume.parsed event carries HrFlow.ai’s body in data, signed as every event. It is not HrFlow.ai’s event envelope: read data.data.profile. A failed parse sends resume.parse_failed with the HireLayer error.
- Set a client timeout of 150 seconds: a parse takes about 25 to 35 seconds, and HrFlow.ai’s SDK sets none.
- Search and scoring on stored profiles have their own HireLayer endpoints: Talent Pool Search and the Resume Scorer, on the resumes you parsed.
- To keep nothing, send
do_not_store_data=true(Enterprise plan):keyis then therequest_idandattachmentsis empty.