# 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.

Source: https://hirelayer.co/api-docs/migrate-from-hrflow · Markdown: https://hirelayer.co/api-docs/migrate-from-hrflow.md

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](https://hirelayer.co/api-docs/parse-resume.md) 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](https://hirelayer.co/api-docs/webhooks.md) |

## Keep HrFlow.ai’s SDK

Set the API URL to `https://hirelayer.co/api/hrflow/v1/` and the secret to your [HireLayer key](https://hirelayer.co/api-docs/authentication.md). The parsing call and its answer keep HrFlow.ai’s format.

**Parse a resume through the HrFlow.ai-compatible API**

**Python**

```python
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**

```bash
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](https://hirelayer.co/api-docs/authentication.md#workspaces): 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.

## Call the Resume Parser in HrFlow.ai’s format

Add `?output_format=hrflow` to [`POST /api/v4/resumes/parse`](https://hirelayer.co/api-docs/parse-resume.md): 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`](https://hirelayer.co/api-docs/legacy-parsers.md) takes the same parameter and gives the same body.

**Parse a resume in HrFlow.ai’s format**

**cURL**

```bash
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"
```

**Python**

```python
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"])
```

**TypeScript**

```typescript
import { readFile } from 'node:fs/promises'

const form = new FormData()
form.append(
  'file',
  new Blob([await readFile('resume.pdf')], { type: 'application/pdf' }),
  'resume.pdf'
)

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

**201, cut**

```json
{
  "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](https://hirelayer.co/api-docs/parse-resume.md). 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.

## The profile, field by field

| 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.

> **Note: Nothing is estimated.** A value HireLayer does not extract is `null` or an empty list: `id`, `consent_algorithmic`, `archived_at`, `info.gender`, `info.summary`, logos, `courses`, `tasks`, the lists of each experience, and the page counts and file size of `data.parsing`. Contract types, the duration of each role, degree levels, skill categories and ROME labels have no place in HrFlow.ai’s format: the [Resume Parser](https://hirelayer.co/api-docs/parse-resume.md)’s own JSON has them.

## Errors

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](https://hirelayer.co/api-docs/errors.md) to branch on.

**401**

```json
{
  "code": 401,
  "message": "Invalid API key: it is unknown, malformed or revoked.",
  "error_code": "invalid_api_key"
}
```

## Webhooks

With `webhook_url` and `output_format=hrflow`, the call answers the usual `202`, and the [`resume.parsed` event](https://hirelayer.co/api-docs/webhooks.md) 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.

## Before you switch

- 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](https://hirelayer.co/api-docs/search-resumes.md) and the [Resume Scorer](https://hirelayer.co/api-docs/score.md), on the resumes you parsed.
- To keep nothing, send `do_not_store_data=true` (Enterprise plan): `key` is then the `request_id` and `attachments` is empty.

> **Tip: Another vendor’s format.** Moving from another resume parser and want to keep your code? [Tell us which format you need](https://hirelayer.co/book-a-demo).

## Next

- [HireLayer Resume Parser](https://hirelayer.co/api-docs/parse-resume.md): HireLayer resume parser API reference: send a PDF, Word or image resume in one multipart request and get the candidate back as JSON, photo included, in 70 languages.
- [Errors and retries](https://hirelayer.co/api-docs/errors.md): The error format of the HireLayer API, every error code, and when to retry a call.
