Skip to content

Bullhorn integration · Event subscriptions + REST API

Score Bullhorn candidates with HireLayer

Bullhorn parses resumes itself and is rolling out a parser built with Textkernel technology, a company Bullhorn owns. HireLayer is an independent complement that processes data in the EU: it scores each submission against the job order with an explanation per criterion, and returns normalized skills your recruiters can search.

Trigger
Event subscription: JobSubmission inserted
Bullhorn API
REST API, OAuth 2.0 and a BhRestToken session
Write-back
Note, custom fields, work history, skills
Cost
2 credits per submission, plus 1 per job order
Bullhorn + HireLayer
  1. 1Bullhorn logs the submissionBullhornA candidate is submitted to a job order, or applies from a job board as a web response. Bullhorn adds an INSERTED event for the JobSubmission to your subscription.
  2. 2Parse the resumeHireLayerYour service reads the events on a schedule, downloads the Resume attachment with /file/Candidate/{id}/{fileId} and sends it to HireLayer CV Extract.
  3. 3Score against the job orderHireLayerHireLayer Match compares the resume with criteria taken from the job order description and returns a score and an explanation per criterion.
  4. 4Write back to BullhornBullhornA note on the candidate linked to the job order, the score in a custom field of the submission, and skills or work history when the record lacks them.

Use cases

What staffing teams do with HireLayer and Bullhorn

  • Sort web responses

    Job board applicants arrive as submissions with the status New Lead. A score on each one lets recruiters call the strongest maches first instead of working in arrival order.

  • Prepare client shortlists

    Send the resume text of up to 10 candidates for a job order to HireLayer Rank and get them back in order, each with a score and a rationale to support the submittal.

  • Searchable skills

    Match HireLayer’s normalized skills to your Bullhorn Skill list and add them to primarySkills, so candidates parsed years appart share the same skill names.

  • Backfill imported records

    Candidates imported from an older system may have a resume attachment but no work history. Parse the attachment and create CandidateWorkHistory and CandidateEducation entries.

Setup

Connect Bullhorn to HireLayer

This is a service you run on a schedule, for example a serverless function or an n8n workflow. It needs Bullhorn REST API access, which Bullhorn does not include in ATS Growth (formerly Team Edition).

  1. 1

    Request API credentials

    Open a support ticket in the Bullhorn Resource Center to get an OAuth client ID, client secret and registered redirect URI. Use a dedicated API user whose entitlements cover candidates, submissions, files and notes.

  2. 2

    Log in and keep the session

    Call GET https://rest.bullhornstaffing.com/rest-services/loginInfo?username=… to find your data center, get an OAuth access token (valid 10 minutes), then POST /rest-services/login for a BhRestToken and your restUrl. Refresh the token instead of logging in before every call.

  3. 3

    Subscribe to submissions

    Create the subscription once with PUT /event/subscription/hirelayer?type=entity&names=JobSubmission&eventTypes=INSERTED, then read it on a schedule, for example every 30 minutes, with GET /event/subscription/hirelayer?maxEvents=100.

  4. 4

    Extract criteria, parse, score and write back

    Send each job order’s description to HireLayer Job Extract once and store the criteria. For each event, load the submission, download the resume, parse it, score it and create a note with PUT /entity/Note.

Field mapping

Where HireLayer results go in Bullhorn

Bullhorn creates entities with PUT and updates them with POST. Work history and education are separate entities linked to the candidate, and custom fields are set up per account in Field Mappings.

HireLayer fieldBullhorn REST APINote
score + summary + evaluated_criteriaPUT /entity/Note (personReference, commentingPerson, action, comments)action must be in your commentActionList
scoreJobSubmission customInt1–5 (POST /entity/JobSubmission/{id})For example score × 100
work_experiences[]PUT /entity/CandidateWorkHistory (companyName, title, startDate, endDate, comments)title holds 50 characters
educations[]PUT /entity/CandidateEducation (school, degree, startDate, endDate)
skills[].skill_titleprimarySkills (PUT /entity/Candidate/{id}/primarySkills/{skillIds})IDs from your Skill list, or skillSet as text
info_candidate.job_titleoccupation (POST /entity/Candidate/{id})

Example

A polling job in TypeScript

A minimal Node.js job: it reads new JobSubmission events, downloads each candidate’s Resume attachment, parses and scores it, and adds a note linked to the job order. Add your own session refresh, storage for criteria and retries.

bullhorn-poll.tstypescript
const HIRELAYER = 'https://hirelayer.co'
const headers = { 'X-API-Key': process.env.HIRELAYER_API_KEY! }

// restUrl and BhRestToken come from POST /rest-services/login.
// Reuse the session: do not log in again before every call.
export async function poll(restUrl: string, bhRestToken: string) {
  const bh = (path: string, init?: RequestInit) =>
    fetch(`${restUrl}${path}${path.includes('?') ? '&' : '?'}BhRestToken=${bhRestToken}`, init)
      .then((r) => r.text())
      .then((text) => (text ? JSON.parse(text) : null))

  // Created once: PUT event/subscription/hirelayer?type=entity&names=JobSubmission&eventTypes=INSERTED
  const batch = await bh('event/subscription/hirelayer?maxEvents=100')
  for (const event of batch?.events ?? []) {
    const { data: submission } = await bh(
      `entity/JobSubmission/${event.entityId}?fields=id,candidate,jobOrder`
    )
    const candidateId = submission.candidate.id
    const files = await bh(`entity/Candidate/${candidateId}/fileAttachments?fields=id,name,type`)
    const resume = files.data.find((f: { type: string }) => f.type === 'Resume')
    if (!resume) continue

    // 1. Download (JSON with base64 content) and parse (about 35 s)
    const { File } = await bh(`file/Candidate/${candidateId}/${resume.id}`)
    const form = new FormData()
    form.append('file', new Blob([Buffer.from(File.fileContent, 'base64')]), File.name)
    form.append('application_id', String(submission.id))
    const parsed = await fetch(`${HIRELAYER}/api/v3/parser`, {
      method: 'POST', headers, body: form, signal: AbortSignal.timeout(150_000),
    }).then((r) => r.json())

    // 2. Score with the criteria stored for this job order
    const criteria = await loadCriteria(submission.jobOrder.id)
    const match = await fetch(`${HIRELAYER}/api/v1/matching/job-candidate`, {
      method: 'POST',
      headers: { ...headers, 'Content-Type': 'application/json' },
      body: JSON.stringify({
        job_text: criteria.jobText,
        candidate_text: parsed.info_resume.text,
        matching_criteria: criteria.items,
      }),
    }).then((r) => r.json())

    // 3. Note on the candidate, linked to the job order (PUT creates)
    await bh('entity/Note', {
      method: 'PUT',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        personReference: { id: candidateId },
        commentingPerson: { id: Number(process.env.BULLHORN_USER_ID) },
        jobOrder: { id: submission.jobOrder.id },
        action: 'HireLayer score', // a value of your commentActionList
        comments: `HireLayer score: ${Math.round(match.score * 100)}/100\n${match.summary}`,
      }),
    })
  }
}

Limits

Good to know

  • API access depends on your edition

    ATS Growth (formerly Team Edition) has no REST API access, only Marketplace integrations. On other editions, Bullhorn allows up to 1,500 requests per minute and 50 concurrent sessions; on HTTP 429, wait one second and retry.

  • Events are pulled, not pushed

    Bullhorn has no push webhooks. Events contain IDs only, and Bullhorn purges the ones nobody reads in time, so keep the schedule runing. The number of subscriptions per database is limited too.

  • Bullhorn already parses resumes

    Its parser fills the candidate, work history and education, also through POST /resume/parseToCandidate, and its upgrade with Textkernel technology rolls out through 2026. Use HireLayer for job-specific scores and normalized skills, and write history only to records that lack it.

  • Not a Marketplace app

    HireLayer is not listed in the Bullhorn Marketplace. Your service uses your own API credentials, so its calls count toward your API limits; usage tied to validated Bullhorn partners does not.

FAQ

HireLayer and Bullhorn: questions

Does HireLayer integrate with Bullhorn?

Yes, through the Bullhorn REST API: your service reads new submissions from an event subscription, sends the resume to HireLayer, and writes the score back as a note and fields. HireLayer has no app in the Bullhorn Marketplace.

Bullhorn already parses resumes. Why add HireLayer?

Bullhorn’s parser fills the candidate record. HireLayer answers a different question: how well this candidate fits this job order, with a score from 0 to 1 and an explanation per criterion. It also returns normalized skills, and Bullhorn’s own parse stays as it is.

Can I screen Bullhorn candidates with AI?

Yes. HireLayer turns the job order into weighted criteria and scores each submission against them, with explanations written in French. The score helps recruiters decide whom to call first; it does not change submission statuses.

Does Bullhorn send webhooks?

No. Bullhorn uses event subscriptions that your service reads on a schedule, and each event holds IDs only. For no-code, Make lists community-built Bullhorn apps, not maintained by Make, which also need OAuth keys from Bullhorn support.

Where is candidate data processed?

HireLayer processes data in the EU. Resumes go from Bullhorn to HireLayer only through the service you run, and the parse request accepts a do_not_store_data option for strict retention rules.

Connect Bullhorn to HireLayer today

The Free plan includes 50 credits a month, no card required. One successful call is one credit, on every API.