Bullhorn resume parser: what the API returns, and its limits
Published
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.
| Operation | What you send | What comes back | Documented input formats |
|---|---|---|---|
POST /resume/parseToCandidate | One file, multipart form data | Unsaved candidate data as JSON | html or text |
POST /resume/parseToCandidateViaJson | Resume text inside a JSON body | Unsaved candidate data as JSON | html or text |
POST /resume/parseToHrXml | One file, multipart form data | An hrXml string | text, 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:
- Nothing is saved. A parse call creates no record. You decide what to keep and write it with separate entity calls.
- One file per request, and not base64. The reference says the attached file "must be a non-base64-encoded file" and that each operation "Takes one file per request". A backlog is your own loop.
- The
formatparameter is required, and the lists differ. ForparseToCandidatethe reference lists onlyhtmlandtext. The long list, PDF and Word included, is documented onparseToHrXml. Check which formats your account accepts on each call before you build an upload form around one of them.
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:
| Part | Fields in Bullhorn's example |
|---|---|
candidate | address (with address1, address2, city, state, zip, countryID) and description |
candidateEducation | school, city, state, degree, major, gpa, startDate, endDate, graduationDate |
candidateWorkHistory | startDate, 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.
- 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.
- 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. - Run the OAuth 2.0 authorization code flow. The guide notes the access token "is valid for 10 minutes", so exchange it promptly.
- Log in to get a session. The REST login call returns a
BhRestTokenand 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
| Question | Bullhorn parse operations | Standalone parsing API |
|---|---|---|
| Needs a Bullhorn account and OAuth keys | Yes | No |
| Creates a record | No, you create it afterwards | No, it returns JSON to your code |
| Output shape | Bullhorn entities, or HR-XML | The API's own JSON schema |
| Dates | Millisecond timestamps | Depends on the API |
| Skills | A separate association you fill yourself | Depends on the API; often a list in the response |
| Files per request | One | Depends on the API |
| Works for CVs that never enter Bullhorn | Only with a Bullhorn session | Yes |
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:
- Every CV you parse becomes a candidate in Bullhorn.
- You already hold OAuth keys and a working login flow for the account.
- Recruiters read the result inside Bullhorn, and fix a field there when the parse misses it.
- Contact details, education and work history are what you need, and skills are handled by recruiters or a separate step.
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.
- Parse the file and keep the full JSON on your side.
- Create the Candidate through
PUT /entity/Candidatewith the contact fields. - Create the history. One
CandidateWorkHistoryper role and oneCandidateEducationper school, with dates converted to timestamps and titles trimmed to 50 characters. - Attach skills by matching each one to a
Skillid in the account and adding it toprimarySkills. - 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
- The Bullhorn resume parser is three REST operations:
parseToCandidatefor a file,parseToCandidateViaJsonfor text, andparseToHrXmlfor an HR-XML result. - A parse saves nothing. You create the Candidate, its work history and its education with separate calls.
- Mind the details: one file per request, a required
formatwhose documented values differ by operation, timestamp dates, and a 50-character job title. - Skills are an association, filled by matching against the account's own skill list.
- It is enough when every CV becomes a Bullhorn candidate and recruiters are the readers.
- Add a standalone parser when you parse before a candidate exists, serve several ATSs, let code decide, or parse CVs that are not candidates.
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.