ResumeJSON

Bullhorn resume parser: what the API returns, and its limits

Bullhorn resume parser: what the REST API gives you

The Bullhorn resume parser is reached through three operations under /resume in the Bullhorn REST API: parseToCandidate, parseToCandidateViaJson and parseToHrXml. The first two turn a resume into unsaved candidate data, with education and work history as separate lists, which your code then uses to create the real Candidate records. The third returns the parse as an HR-XML document. If you are integrating with a Bullhorn account your staffing firm already runs, those calls are usually all you need. If you are parsing CVs that may never become Bullhorn candidates, or you want one schema across several systems, a parser of your own is the better fit.

This article is for the developer writing that integration. Everything about Bullhorn below was read from Bullhorn's public REST API reference and its getting-started guide at bullhorn.github.io, as of October 2026. We build ResumeJSON, a resume parsing API, so read the last sections as written by an interested party.


The three Bullhorn parse operations

All three take a resume and return parsed data. They differ in what you send and what comes back.

OperationWhat you sendWhat comes backDocumented input formats
POST /resume/parseToCandidateOne file, multipart form dataUnsaved candidate data as JSONhtml or text
POST /resume/parseToCandidateViaJsonResume text inside a JSON bodyUnsaved candidate data as JSONhtml or text
POST /resume/parseToHrXmlOne file, multipart form dataAn hrXml stringtext, html, pdf, doc, docx, rtf or odt; no image format listed

Bullhorn describes the first one plainly:

The parseToCandidate operation parses a resume to unsaved Candidate data. A typical use case for this operation is to use parts of the response in the bodies of calls to create new Candidate, CandidateWorkHistory, and CandidateEducation entities.

Three details in the reference are worth reading twice:

The populateDescription option

Both candidate operations accept populateDescription=text or populateDescription=html. With it set, the response's description field carries the resume itself, so a recruiter opening the candidate sees the CV inside Bullhorn. The reference warns that this value is JSON-encoded in the response and must be JSON-encoded again if you parse the response into an object and then send the description in a later request body. Forgetting that is a common source of broken candidate descriptions.

What the parse response contains

The documented example response for parseToCandidate has three parts:

PartFields in Bullhorn's example
candidateaddress (with address1, address2, city, state, zip, countryID) and description
candidateEducationschool, city, state, degree, major, gpa, startDate, endDate, graduationDate
candidateWorkHistorystartDate, endDate, comments

The example is abbreviated, so read it as a sample of the shape rather than a full list. Two things about that shape matter when you write the mapping code.

Dates are timestamps. The example's startDate values are millisecond epoch numbers such as 978368400000. A CV that says "2019 to 2021" arrives as two exact instants, so decide early whether your code treats a year-only date as January or as unknown.

The target entities have length limits. Bullhorn's entity reference gives CandidateWorkHistory.title a length of 50 characters and companyName a length of 100. A long job title copied straight from a parse needs trimming before the create call.

Where skills live

The documented parse example carries no skills list. In Bullhorn, skills are a separate association: primarySkills on the Candidate, pointing at Skill entities from your own skill list. Getting skills from a CV into Bullhorn therefore means matching each extracted skill to an existing Skill id and then calling the association endpoint. If your workflow depends on skills, plan that matching step, whichever parser you use. Our skills taxonomy article covers how to map free-text CV skills onto a fixed list.

Getting access to the Bullhorn API

The parse calls sit behind the same authentication as the rest of the REST API, and getting it working often takes longer than the parse code.

  1. Get OAuth keys. Bullhorn's getting-started guide says customers "can obtain OAuth keys for developing applications with the Bullhorn REST API by creating a support ticket via the Bullhorn Resource Center." There is no self-serve signup.
  2. Find the data center. Call GET rest-services/loginInfo?username={API_Username} to get the right URLs for that user. The wrong URL answers with a 307 redirect, and your code must follow it.
  3. Run the OAuth 2.0 authorization code flow. The guide notes the access token "is valid for 10 minutes", so exchange it promptly.
  4. Log in to get a session. The REST login call returns a BhRestToken and a base URL. Every later call, the parse calls included, sends that token and uses that base URL.

If you are a software vendor rather than a Bullhorn customer, every customer you serve brings their own keys, data center and session. That is normal for an ATS integration, and it is also why some teams parse before Bullhorn is involved at all.

Bullhorn's parser and a standalone parsing API, side by side

QuestionBullhorn parse operationsStandalone parsing API
Needs a Bullhorn account and OAuth keysYesNo
Creates a recordNo, you create it afterwardsNo, it returns JSON to your code
Output shapeBullhorn entities, or HR-XMLThe API's own JSON schema
DatesMillisecond timestampsDepends on the API
SkillsA separate association you fill yourselfDepends on the API; often a list in the response
Files per requestOneDepends on the API
Works for CVs that never enter BullhornOnly with a Bullhorn sessionYes

Neither column wins in general. They suit different jobs.

When Bullhorn's own parser is enough

Stay with the Bullhorn parse operations when all of these hold:

That describes most internal integrations at a staffing firm running Bullhorn. The parser is part of a system you already pay for, its output maps onto the entities you will write anyway, and a second vendor would add a key, a bill and a second schema for no gain.

When you need a parser outside Bullhorn

There are four common cases where the operations above do not reach.

The CV arrives before a candidate exists

A careers page or job board that wants to autofill its application form has to parse the file while the applicant is still on the page. Calling Bullhorn at that moment means holding a Bullhorn session for an anonymous visitor. A standalone parse runs on the file alone and lets the applicant correct the fields before anything reaches the ATS.

You serve more than one ATS

A product selling to recruiters on Bullhorn, Lever, Zoho Recruit and others meets a different parse shape in each. Parsing once, into one schema, gives your code one record to read and one mapping per ATS on the way out.

Your code makes the decision

Automated resume screening and candidate matching need typed fields your code can filter on: a skills list, dated roles, total experience. With Bullhorn you rebuild those from timestamps, comments and an association you filled yourself.

The CVs are not candidates

An old folder of CVs, a sourcing export, a set of files you only want to search: making each one a Bullhorn record just to read it is the wrong order. Bulk resume parsing covers running a backlog through a parser once and keeping the results.

Using a parsing API together with Bullhorn

The two combine well. Parse first, then write to Bullhorn with what you already have.

  1. Parse the file and keep the full JSON on your side.
  2. Create the Candidate through PUT /entity/Candidate with the contact fields.
  3. Create the history. One CandidateWorkHistory per role and one CandidateEducation per school, with dates converted to timestamps and titles trimmed to 50 characters.
  4. Attach skills by matching each one to a Skill id in the account and adding it to primarySkills.
  5. Store the Bullhorn candidate id beside your record so the two can be joined later.

Step 1 with ResumeJSON is one request, with the file sent as multipart form data:

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

const form = new FormData()
form.set('file', new Blob([await readFile('cv.pdf')]), 'cv.pdf')

const res = await fetch('https://resumejson-resume-cv-parser-api.p.rapidapi.com/v1/parse', {
  method: 'POST',
  headers: {
    'x-rapidapi-key': process.env.RAPIDAPI_KEY,
    'x-rapidapi-host': 'resumejson-resume-cv-parser-api.p.rapidapi.com'
  },
  body: form
})

const { resume } = await res.json()
// resume.basics.full_name, resume.basics.email, resume.basics.phone
// resume.work[]: company, title, start_date, end_date
// resume.education[], resume.skills[], resume.languages[]

Dates come back as YYYY-MM, or YYYY when the CV states no month, so converting a role to a Bullhorn timestamp is a choice you make once in code rather than one the parser makes for you. ResumeJSON reads PDF, DOCX, plain text and images of a page (JPEG, PNG or WebP), returns null for a value the CV does not state, and is sold through RapidAPI. The docs carry the full field reference, and you can try a real CV in the free parser before writing any code.

Summary

If you are designing what to store, how to build an ATS covers the data model, and HR-XML resume explains the format parseToHrXml returns.

All articles