Skip to content

Resume Parser Benchmark: HireLayer, Textkernel, RChilli and Affinda on 5 Test Resumes

Five test resumes through HireLayer, Textkernel, RChilli and Affinda: language levels, scans, infographics, contract types and skills, with the raw JSON.

Published · 9 minutes read

Cover reading “Resume parser benchmark” with a chart icon and a document icon

We ran the same five resumes through HireLayer, Textkernel, RChilli and Affinda and checked every field against what the resumes really say. HireLayer returned a language level for all 13 languages, the right employer, title and dates for 17 of the 18 positions, French contract types and ROME codes. The other three parsers returned no language level at all (Textkernel, RChilli) or got some wrong (Affinda), and two of them lost the work history of the scanned or the infographic resume. The trade-off is speed: HireLayer took 22 to 31 seconds per resume, the others under 6.

HireLayer is our product, so read this with that in mind. To keep the test checkable, we wrote the five resumes ourselves (fictional people, so the right answer is known field by field) and you can download them below and run them through any parser. We read the raw JSON each API returned, not the vendor's on-screen preview, and we quote that JSON throughout the article.

How we ran the test

Each parser was used on 7 October 2026 through its own web demo or sandbox, with the account we created for the test and the default settings. Textkernel offers an optional LLM Parser in its demo, so we ran it a second time with that option on and report both runs.

ParserWhere we ran itSettings
HireLayerDashboard, Resumes workspace (same endpoint as POST /api/v3/parser)Default
AffindaResume Parser sandbox, EU regionDefault
TextkernelTx Platform demo (engine 9.22, API 10.13)Default, then with “Enable LLM Parser”
RChilliMyAccount, Integration (Demo), Resume Parser 8.0.0, Frankfurt serverDefault, resume indexing turned off

The five test resumes

Each one targets a layout or a detail that trips parsers in real hiring pipelines. Download them and try them on your own shortlist.

ResumeFormatWhat it tests
CV1 · Camille RousseauFrench PDF, two columns with a sidebarContact and languages in the sidebar, an apprenticeship (alternance), an internship, a TOEIC score
CV2 · Daniel OkaforEnglish PDF, two pagesTwo roles at the same employer, GitHub, Yoruba, certifications
CV3 · Inès BenaliFrench Word file laid out in tablesA nurse: temp work, a fixed-term contract, French healthcare diplomas and the AFGSU certificate
CV4 · Thomas LefèvreFrench scan (image only, slightly rotated, with noise)OCR, accents, language levels written as words
CV5 · Sofia MarchettiEnglish infographic PDFA timeline with dates like 03.2021 — now, the name on two lines, contact details behind icons, skill dots

Results at a glance

A position counts as correct when the employer, the job title and the dates all match the resume. Language levels count as correct when they match the level written on the resume, or its plain reading for words such as “native” or “notions”.

HireLayerAffindaTextkernelTextkernel LLM ParserRChilli
Positions correct (out of 18)171491510
Language levels correct (out of 13)136, and 2 wrong000
Resumes with no usable work history01 (CV5)1 (CV5)02 (CV4, CV5)
French contract types typed (out of 4)4200No field
ROME occupation codesYesNoNoNoNo
Gender returned without being writtenNoNo4 resumes of 55 resumes of 5No
Time per resume22 to 31 sA few secondsAbout 2.7 s3.5 to 5.7 sAbout 0.5 s

Language levels

Recruiters filter on language levels all the time, so a parser that returns “English” without “C1” leaves the hardest part to a human. On CV1 the sidebar reads “Français : langue maternelle”, “Anglais : C1 (TOEIC 910/990)” and “Espagnol : B1”. Here is what each API returned for that block.

HireLayer

"languages": [
  { "language": "Français", "level": "Native or Bilingual (C2)" },
  { "language": "Anglais", "level": "Full Professional Proficiency (C1)" },
  { "language": "Espagnol", "level": "Limited Working Proficiency (B1)" }
]

Affinda

"languages": [
  { "name": "French" },
  { "name": "English", "proficiency": "advanced_c1" },
  { "name": "Spanish", "proficiency": "intermediate_b1" }
]

Textkernel (same output with the LLM Parser)

"LanguageCompetencies": [
  { "Language": "English", "LanguageCode": "en" },
  { "Language": "Spanish", "LanguageCode": "es" },
  { "Language": "French", "LanguageCode": "fr" }
]

RChilli

"LanguageKnown": [
  { "Language": "Anglais", "LanguageCode": "en" },
  { "Language": "Français", "LanguageCode": "fr" }
]

Textkernel and RChilli never returned a level, on any of the five resumes, and RChilli dropped Spanish here. Affinda returned levels on some resumes but never for a mother tongue, and on the scanned CV4 it read “Anglais : courant (B2)” as C1 and “Allemand : notions” as B1:

HireLayer, CV4

"languages": [
  { "language": "Anglais", "level": "Professional Working Proficiency (B2)" },
  { "language": "Allemand", "level": "Advanced Basic Proficiency (A2)" }
]

Affinda, CV4

"languages": [
  { "name": "English", "proficiency": "advanced_c1" },
  { "name": "German", "proficiency": "intermediate_b1" }
]

If you want the scales behind those labels, our guide to language proficiency levels on a resume maps CEFR, LinkedIn and ILR side by side.

Scanned and infographic resumes

Two-column and designer resumes are common, and they are where the gaps between parsers get wide. CV5 puts dates in a timeline column, written as 03.2021 — now, and the name on two lines. HireLayer returned the current role, the dates and both schools:

HireLayer, CV5

"info_candidate": {
  "full_name": "SOFIA MARCHETTI",
  "job_title": "Lead Product Designer",
  "experience_level": "More than 10 years"
},
"work_experiences": [
  {
    "company_name": "BlaBlaCar",
    "job_title": "Lead Product Designer",
    "start_date": "2021-03-01",
    "end_date": null,
    "currently_active": true,
    "work_experience_city": "Paris"
  },
  {
    "company_name": "Self-employed",
    "job_title": "Freelance UX/UI Designer",
    "contract_type": "Freelance",
    "start_date": "2018-01-01",
    "end_date": "2021-02-28",
    "currently_active": false
  }
  // trimmed
],
"educations": [
  { "degree_title": "MSc Digital and Interaction Design", "school_name": "Politecnico di Milano", "start_date": "2013-01-01", "end_date": "2015-12-31" },
  { "degree_title": "BA Graphic Design and Visual Communication", "school_name": "Università Iuav di Venezia", "start_date": "2010-01-01", "end_date": "2013-12-31" }
]

Affinda found the three positions but gave each one an end date equal to its start date, so no current role and 14 months of total experience for a designer with more than ten years of work. School names were cut in half:

Affinda, CV5

"workExperience": [
  {
    "jobTitle": "Lead Product Designer",
    "organization": "BlaBlaCar",
    "dateRange": { "start": { "date": "2021-03-01" }, "end": { "date": "2021-03-01" } }
  }
  // the two other positions also end the month they start
],
"education": [
  { "qualification": "MSc", "institution": "Politecnico di", "dateRange": { "end": { "date": "2013-01-01" } } },
  { "qualification": "BA", "institution": "Università Iuav", "dateRange": { "end": { "date": "2010-01-01" } } }
],
"employmentMetrics": { "totalExperienceMonths": 14, "longestTenureMonths": 12 }

Textkernel with default settings returned two positions with no dates and described the candidate as entry-level. With the LLM Parser turned on, it returned the three positions with their dates.

Textkernel, CV5, default settings

"EmploymentHistory": {
  "ExperienceSummary": {
    "Description": "SOFIA MARCHETTI is experienced in: User Experience (Product Design; Web Design); and Information Technology (User Interface). SOFIA MARCHETTI appears to be an entry-level candidate, with 0 months of experience.",
    "MonthsOfWorkExperience": 0
  },
  "Positions": [
    { "JobTitle": { "Raw": "Lead Product Designer" }, "Employer": { "Name": { "Raw": "BlaBlaCar" } } },
    { "JobTitle": { "Raw": "Freelance UX/UI Designer" }, "Employer": { "Name": { "Raw": "Self-employed" } } }
    // no StartDate or EndDate on either position; the Fabrica position is missing
  ]
}

RChilli returned no position and no school, and kept only the first name:

RChilli, CV5

"Name": { "FullName": "SOFIA", "FirstName": "SOFIA", "LastName": "" },
"WorkedPeriod": { "TotalExperienceInMonths": "", "TotalExperienceInYear": "", "TotalExperienceRange": "" },
"ExecutiveSummary": "SOFIA doesn't have any experience in the resume.",
"SubCategory": "Human Resources Specialists"

The scanned CV4 tests OCR. HireLayer read the email, the accents and the three positions with their cities:

HireLayer, CV4

"info_candidate": {
  "full_name": "Thomas LEFÈVRE",
  "email": "[email protected]",
  "phone_number": "+33639987703",
  "job_title": "Contrôleur de gestion senior"
},
"work_experiences": [
  { "company_name": "Airbus", "job_title": "Contrôleur de gestion senior", "start_date": "2020-03-01", "currently_active": true, "work_experience_city": "Blagnac" },
  { "company_name": "Capgemini Invent", "job_title": "Consultant en performance financière", "start_date": "2015-09-01", "end_date": "2020-02-01", "work_experience_city": "Toulouse" },
  { "company_name": "KPMG", "job_title": "Auditeur financier", "start_date": "2012-09-01", "end_date": "2015-08-01", "work_experience_city": "Paris" }
]

Textkernel misread the email address in both runs, wich means a recruiter cannot reach the candidate from the parsed profile:

Textkernel, CV4

"ContactInformation": {
  "CandidateName": { "FormattedName": "Thomas Lefèvre" },
  "EmailAddresses": ["[email protected]"]
}

RChilli read the contact details but returned no work history:

RChilli, CV4

"Name": { "FullName": "Thomas LEFEVRE" },
"Email": [{ "EmailAddress": "[email protected]", "ConfidenceScore": 5 }],
"CurrentEmployer": "",
"JobProfile": "",
"WorkedPeriod": { "TotalExperienceInMonths": "", "TotalExperienceInYear": "", "TotalExperienceRange": "" }

French contract types

An alternance (work-study apprenticeship), an internship, temp work and a fixed-term contract (CDD) do not weigh the same when you screen a French candidate. HireLayer typed all four:

HireLayer, CV1 and CV3

// CV1
{ "company_name": "L'Oréal", "job_title": "Assistante chef de produit (alternance)", "contract_type": "Apprenticeship", "start_date": "2017-09-01", "end_date": "2019-02-28" },
{ "company_name": "Mairie de Lille", "job_title": "Stage – Chargée de communication", "contract_type": "Internship", "start_date": "2017-04-01", "end_date": "2017-07-31" },
// CV3
{ "company_name": "Adecco Medical", "job_title": "Aide-soignante en intérim", "contract_type": "Temporary assignment", "start_date": "2019-01-01", "end_date": "2022-12-31" },
{ "company_name": "Hôpital Delafontaine", "job_title": "Agent de service hospitalier", "contract_type": "Fixed-term contract", "start_date": "2015-07-01", "end_date": "2015-08-31" }

Affinda filed both the apprenticeship and the temp assignment as internships. Textkernel returned UNSPECIFIED for every French position, and with default settings it also cut “L'Oréal” down to “Oréal” and lost the end date. RChilli has no contract type field.

Affinda, CV1 and CV3

// CV1
{ "jobTitle": "Assistante chef de produit (alternance)", "organization": "L 'Oréal", "employmentType": "internship" },
// CV3
{ "jobTitle": "Aide-soignante en intérim", "organization": "Adecco Medical", "employmentType": "internship" }

Textkernel, CV1 and CV3

// CV1, default settings
{ "JobTitle": { "Raw": "Assistante chef de produit" }, "Employer": { "Name": { "Raw": "Oréal" } }, "StartDate": { "Date": "2017-09-01" }, "JobType": "UNSPECIFIED" },
// CV3
{ "JobTitle": { "Raw": "Aide-soignante en intérim" }, "Employer": { "Name": { "Raw": "Adecco Medical" } }, "JobType": "UNSPECIFIED" }

HireLayer also suggests ROME occupation codes, the French public employment service's job classification. For CV1 the first suggestion is M1718 with a score of 0.991 (see the full response below). None of the other three parsers return ROME codes.

Skills extraction

Skills only help matching when they come back as clean, normalized values. On the nurse's resume (CV3), HireLayer returned 17 skills in French, 16 of them normalized with a domain and a subcategory:

HireLayer, CV3

"skills": [
  { "skill_title": "Pose et gestion de perfusions", "skill_type": "Hard skill", "status": "normalized", "domain": "Santé & Pharmacie", "subcategory": "Soins infirmiers" },
  { "skill_title": "Réfection de pansements", "skill_type": "Hard skill", "status": "normalized", "domain": "Santé & Pharmacie", "subcategory": "Soins infirmiers" },
  { "skill_title": "Électrocardiogramme", "skill_type": "Hard skill", "status": "normalized", "domain": "Santé & Pharmacie", "subcategory": "Soins infirmiers" },
  { "skill_title": "Logiciel Orbis", "skill_type": "Software skill", "status": "normalized", "domain": "Santé & Pharmacie", "subcategory": "Secrétariat médical & logiciels de santé" },
  { "skill_title": "Netsoins", "skill_type": "Software skill", "status": "normalized", "domain": "Santé & Pharmacie", "subcategory": "Secrétariat médical & logiciels de santé" },
  { "skill_title": "Prévention des escarres", "skill_type": "Hard skill", "status": "normalized", "domain": "Santé & Pharmacie", "subcategory": "Soins infirmiers" }
  // 11 more skills trimmed
]

Affinda returned 28 skills, but 21 of them have no taxonomy entry and several are pieces of sentences, with a bracket left open:

Affinda, CV3

"skills": [
  { "name": "Electrocardiography", "text": "ECG", "taxonomy": { "type": "specialized_skill" } },
  { "name": "perfusions", "text": "perfusions" },
  { "name": "pansements complexes", "text": "pansements complexes" },
  { "name": "Orbis (dossier patient", "text": "Orbis (dossier patient" },
  { "name": "Rigoureuse", "text": "Rigoureuse" }
  // 21 of the 28 skills have no taxonomy entry
]

Textkernel normalized cleanly but added skills that are not on the resume: the care home is called “Les Jardins d'Automne”, so the nurse recieved “Entretien des Espaces Verts” (grounds maintenance) and “Gardiennage” (caretaking). The same thing happend on other resumes, with “Expediting” and “Stock Control” for a backend engineer.

Textkernel, CV3

"Skills": {
  "Raw": [{ "Name": "Jardins" }, { "Name": "perfusions" }, { "Name": "pansements" }],
  "Normalized": [
    { "Name": "Entretien des Espaces Verts", "Type": "Professional" },
    { "Name": "Gardiennage", "Type": "Professional" },
    { "Name": "Perfusion Tissulaire", "Type": "Professional" },
    { "Name": "Pansement", "Type": "Professional" }
  ]
  // trimmed
}

RChilli returned seven skills, one of them “AP” taken from “AP-HP”, the Paris hospital group, and missed perfusions, dressings and ECG:

RChilli, CV3

"SkillKeywords": "Prévention Des Infections,Gériatrie,Cardiologie,Nursing,Aide Aux Repas,AP,Nettoyage Des Chambres"

Personal data the resume does not contain

None of the five resumes states a gender. Textkernel returned one anyway, on four resumes with default settings and on all five with the LLM Parser. In most hiring contexts that is a field you would have to strip before storing the profile. HireLayer, Affinda and RChilli did not return one.

Textkernel, CV1

"PersonalAttributes": {
  "DateOfBirth": { "Date": "1995-03-14" },
  "DrivingLicense": "B",
  "Gender": "Female"
}

Processing time

This is where HireLayer is behind. It took 22 to 31 seconds per resume in this test, against about half a second for RChilli, 2.7 seconds for Textkernel (3.5 to 5.7 seconds with the LLM Parser) and a few seconds for Affinda. For a candidate waiting on an application form, plan for that delay in the interface. For imports of many resumes at once, send them in parallel or through a queue; our guide to bulk resume parsing shows how.

A full HireLayer response

For reference, this is the HireLayer response for CV1, with long arrays trimmed where marked. The API documentation describes every field.

{
  "status": "success",
  "request_id": "80db4016-0ebe-4f73-826a-e7ad14f1eb47",
  "warnings": [],
  "errors": [],
  "info_resume": {
    "application_id": "app_52d31b13-5",
    "date_parsing": "2026-10-07T17:24:01",
    "language": "FR",
    "text": "CONTACT\n12 rue des Lilas\n69003 Lyon…" // 1,950 characters, trimmed
  },
  "info_candidate": {
    "full_name": "Camille Rousseau",
    "last_name": "Rousseau",
    "first_name": "Camille",
    "email": "[email protected]",
    "phone_number": "+33639982147",
    "birth_date": "1995-03-14",
    "age": 31,
    "availability_now": true,
    "availability_date": null,
    "driver_license": ["Permis B"],
    "job_title": "Cheffe de projet marketing digital",
    "education_name": "Master Marketing digital",
    "education_level": "Level 7",
    "experience_level": "5 to 10 years",
    "linkedin_url": "https://linkedin.com/in/crousseau-mktg",
    "github_url": null,
    "other_urls": [],
    "location": {
      "country": "France",
      "country_code": "FR",
      "region": "Auvergne-Rhône-Alpes",
      "department": "Rhône",
      "city": "Lyon",
      "postal_code": "69003",
      "full_address": "12 rue des Lilas, 69003 Lyon, France",
      "latitude": 45.759689,
      "longitude": 4.848398
    },
    "mobility": { "can_work_in_other_cities": null, "other_cities": [] }
  },
  "educations": [
    {
      "degree_title": "Master Marketing digital",
      "school_name": "IAE Lyon – Université Jean Moulin Lyon 3",
      "description": null,
      "degree_type": "Level 7",
      "start_date": "2017-01-01",
      "end_date": "2019-12-31",
      "currently_active": false,
      "location": {
        "country": "France",
        "country_code": "FR",
        "region": "Auvergne-Rhône-Alpes",
        "department": "Rhône",
        "city": "Lyon",
        "postal_code": null,
        "full_address": "IAE Lyon – Université Jean Moulin Lyon 3, Lyon, France"
      }
    },
    {
      "degree_title": "Licence Économie-Gestion",
      "school_name": "Université de Lille",
      "degree_type": "Level 6",
      "start_date": "2014-01-01",
      "end_date": "2017-12-31"
      // location trimmed
    },
    {
      "degree_title": "Baccalauréat ES, mention Bien",
      "school_name": "Lycée Faidherbe – Lille",
      "degree_type": "Level 4",
      "start_date": "2014-01-01",
      "end_date": "2014-12-31"
      // location trimmed
    }
  ],
  "work_experiences": [
    {
      "company_name": "Groupe SEB",
      "job_title": "Cheffe de projet marketing digital",
      "description": "Pilotage des campagnes SEA et SEO de 4 marques (Tefal, Moulinex, Rowenta, Krups), budget annuel de 1,2 M€. Mise en place de tableaux de bord sous Looker Studio et GA4. Management de 2 chargés de marketing et coordination de l'agence média.",
      "contract_type": null,
      "start_date": "2021-09-01",
      "end_date": null,
      "currently_active": true,
      "work_experience_country": "France",
      "work_experience_country_code": "FR",
      "work_experience_city": "Écully",
      "work_experience_postal_code": null,
      "experience_duration": 61
    },
    {
      "company_name": "Decathlon",
      "job_title": "Chargée de marketing digital",
      "contract_type": null,
      "start_date": "2019-03-01",
      "end_date": "2021-08-31",
      "currently_active": false,
      "work_experience_city": "Villeneuve-d'Ascq",
      "experience_duration": 30
      // description and country trimmed
    },
    {
      "company_name": "L'Oréal",
      "job_title": "Assistante chef de produit (alternance)",
      "contract_type": "Apprenticeship",
      "start_date": "2017-09-01",
      "end_date": "2019-02-28",
      "currently_active": false,
      "work_experience_city": "Clichy",
      "experience_duration": 18
    },
    {
      "company_name": "Mairie de Lille",
      "job_title": "Stage – Chargée de communication",
      "contract_type": "Internship",
      "start_date": "2017-04-01",
      "end_date": "2017-07-31",
      "currently_active": false,
      "work_experience_city": "Lille",
      "experience_duration": 4
    }
  ],
  "rome_jobs": [
    {
      "job_title": "Chef / Cheffe de projet marketing digital",
      "job_code": "510894",
      "rome_title": "Chargé / Chargée de marketing digital",
      "rome_code": "M1718",
      "prediction_score": 0.991
    }
    // 4 more ROME suggestions trimmed
  ],
  "languages": [
    { "language": "Français", "level": "Native or Bilingual (C2)" },
    { "language": "Anglais", "level": "Full Professional Proficiency (C1)" },
    { "language": "Espagnol", "level": "Limited Working Proficiency (B1)" }
  ],
  "skills": [
    { "skill_title": "SEO", "skill_type": "Hard skill", "status": "normalized", "domain": "Marketing & Communication", "subcategory": "Marketing digital & acquisition" },
    { "skill_title": "Google Ads", "skill_type": "Software skill", "status": "normalized", "domain": "Marketing & Communication", "subcategory": "Marketing digital & acquisition" },
    { "skill_title": "Looker Studio", "skill_type": "Software skill", "status": "normalized", "domain": "Data & Intelligence artificielle", "subcategory": "Business Intelligence & dataviz" },
    { "skill_title": "Salesforce Marketing Cloud", "skill_type": "Software skill", "status": "normalized", "domain": "Marketing & Communication", "subcategory": "CRM & marketing automation" },
    { "skill_title": "Management d'équipe", "skill_type": "Hard skill", "status": "normalized", "domain": "Management, Projet & Stratégie", "subcategory": "Management d'équipe" },
    { "skill_title": "A/B Testing", "skill_type": "Hard skill", "status": "normalized", "domain": "Marketing & Communication", "subcategory": "Web analytics & optimisation" }
    // 13 more skills trimmed
  ],
  "certifications": ["Google Ads Search Certification"],
  "interests": ["Trail", "photographie argentique"]
}

Run the test resumes yourself

Download the five files above and parse them in the live demo, or with the free API plan (50 credits a month). You get the same JSON as in this article.

Reproduce or extend the benchmark

Five resumes are enough to show where parsers diverge, not to rank them on every kind of CV. The best test is still your own documents: pick 50 to 100 resumes that look like your real intake, write down the expected values for the fields you filter on, and score each parser seperately. Our benchmark plan for resume parser APIs and the guide to measuring parsing accuracy explain how to set it up. The feature and price comparisons with each vendor are on our comparison pages.

Frequently asked questions

Which resume parser returns language proficiency levels?

In this test, HireLayer returned a CEFR level for all 13 languages across the five resumes, including mother tongues. Affinda returned levels on some resumes, two of them wrong, and none for mother tongues. Textkernel and RChilli returned the language name only.

Which resume parser handles scanned resumes?

On our scanned French resume, HireLayer and Affinda extracted the three positions and the correct email. Textkernel extracted the positions but misread the email address, and RChilli read the contact details but returned no work history.

How long does HireLayer take to parse a resume?

Between 22 and 31 seconds per resume in this test. Textkernel, Affinda and RChilli answered in under 6 seconds. For bulk imports, send resumes in parallel or through a queue.

Can I run this benchmark myself?

Yes. The five resumes are fictional and free to download from this page. Run them through each parser and compare the JSON with the values printed on the resumes.

Sources and further reading

  1. The five test resumes (PDF and DOCX)
  2. Affinda Resume Parser
  3. Textkernel Tx Platform demo
  4. RChilli Resume Parser
  5. HireLayer comparison pages, with every vendor source listed

Louis Desclous

Published on · Reading time: 9 minutes