Home

/

Build It in a Weekend. Run It for Years.

/

Bullhorn & JobAdder Endpoint Cheat-Sheets

Bullhorn & JobAdder Endpoint Cheat-Sheets

Appendix C
Appendix
7
min read

Two one-page references for the Bullhorn and JobAdder REST calls an AI recruiting agent actually leans on: get logged in, read a job and a candidate, pull a CV, search, write a decision back. Everything below is condensed from verified research, the same groundwork that sits behind connecting an agent to your ATS and the use-case builds.

A standing health warning, because it's the whole thesis in miniature: these are correct as of writing, and APIs drift. Both the Bullhorn and JobAdder rows are from fully public official docs and are high-confidence — Bullhorn's at bullhorn.github.io, JobAdder's from its official OpenAPI v2 spec. That's the floor, not the ceiling: tokens expire, hosts move, and "stable" is a verb.

A cheat-sheet has a shelf life. That's not a flaw in the cheat-sheet — it's the reason someone has to own it.

— Bullhorn REST API

Verified against the official docs at bullhorn.github.io.

Auth — three-legged, plus a mandatory step zero

Every host is swimlane-specific (data centre: west, east, emea, …). Resolve the swimlane first; never hardcode hosts.

StepCallReturns / note
0. Data centre (no auth)GET https://rest.bullhornstaffing.com/rest-services/loginInfo?username={apiUsername}oauthUrl + restUrl bases for the user's swimlane. Do this first, always.
1. AuthorizeGET https://auth-{dc}.bullhornstaffing.com/oauth/authorize?client_id={id}&response_type=code&action=Login&username={u}&password={p}&redirect_uri={uri}&state={csrf}Redirects to redirect_uri?code={authCode}. action=Login&username&password is the documented headless variant; the interactive flow omits credentials.
2. TokenPOST https://auth-{dc}.bullhornstaffing.com/oauth/token?grant_type=authorization_code&code={authCode}&client_id={id}&client_secret={secret}&redirect_uri={uri}{ access_token, refresh_token, expires_in }. Refresh token issued only if Bullhorn enabled it for the client.
3. REST loginPOST https://rest-{dc}.bullhornstaffing.com/rest-services/login?version=2.0&access_token={access_token}{ BhRestToken, restUrl }. restUrl is the base for all entity calls and embeds corpToken — read it, don't hardcode it.
RefreshPOST .../oauth/token?grant_type=refresh_token&refresh_token={rt}&client_id={id}&client_secret={secret}New access_token and new refresh_token (rotates — persist the newest). Then redo step 3 for a fresh BhRestToken.

Session & token model

ThingLifetime / behaviour
access_token10 minutes.
refresh_tokenNo time expiry, but single-use — rotates on every refresh. Persist the newest or you lock yourself out.
BhRestToken (session)Reuse it — docs explicitly warn against logging in fresh per request (load). Pass as ?BhRestToken={token} query param, BhRestToken header, or cookie. Treat HTTP 401 as "expired", then refresh → re-login.

Key operations

All relative to restUrl; append &BhRestToken={token} to every call.

OperationCallNote
Get jobGET {restUrl}entity/JobOrder/{id}?fields=id,title,status,clientCorporation,clientContact,employmentTypefields= (or layout=) is required — 404 without it.
Get candidateGET {restUrl}entity/Candidate/{id}?fields=id,firstName,lastName,email,status,occupationSame fields= rule.
List CV / filesGET {restUrl}entity/Candidate/{id}/fileAttachments?fields=id,name,contentType,fileSize,type,dateAdded,isResumefileAttachments replaces the deprecated /entityFiles.
Download a fileGET {restUrl}file/Candidate/{id}/{fileId}Returns { File: { name, contentType, fileContent (base64) } }. Append /raw for raw bytes.
Upload a filePUT {restUrl}file/Candidate/{id}Base64 JSON or multipart body (externalID, fileContent/file, fileType, name).
Parse CV → text`POST {restUrl}resume/parseToCandidate?format={DOC\HTML\PDF\DOCX}`Multipart resume body. Add &populateDescription=text (or html) to get the body as plain text. No separate convert-to-text endpoint — text comes via populateDescription.
Search (full-text, eventually-consistent)GET {restUrl}search/{Entity}?query={lucene}&fields=...&start=0&count=20&sort=...Lucene index. e.g. search/Candidate?query=isDeleted:0 AND occupation:"Engineer". Can search nested file data, e.g. fileAttachments.description:(+Manager +IT).
Query (DB, strongly consistent)GET {restUrl}query/{Entity}?where={jpql}&fields=...&start=0&count=...JPQL. e.g. where=lastName='Smith' AND status='Active'. Use POST if where > ~7,500 chars.
Write a notePUT {restUrl}entity/NoteBody e.g. { "action":"Outbound Call", "comments":"...", "personReference":{"id":{candidateId}}, "jobOrder":{"id":{jobId}} }. Returns { changedEntityId, changeType:"INSERT" }.
Update a fieldPOST {restUrl}entity/Candidate/{id}Body e.g. { "status":"Placed" }changeType:"UPDATE". Read-only fields rejected. (Create a non-note entity = PUT entity/{Entity}.)

Limits, paging, sandbox

ConcernBehaviour
Rate limitOn HTTP 429, wait 1s and retry until success — implement exponential backoff. No published per-second quota. [VERIFY] numeric limits with Bullhorn support.
Paginationstart (offset, default 0) + count (page size, default 20; max varies, commonly up to 500). Response includes total. Loop start += count.
Consistencysearch is index-backed (lag — new records may not appear immediately). Use query when you need read-after-write consistency.
SandboxNo single public sandbox — request a test corp from Bullhorn. Always resolve the swimlane via loginInfo.

Docs: bullhorn.github.io/Getting-Started-with-REST/ · bullhorn.github.io/docs/oauth/ · bullhorn.github.io/rest-api-docs/ · bullhorn.github.io/Resume-Parsing/

Bullhorn auth relay (reference): loginInfo to authorize to token to REST login to BhRestToken, with the 10-minute token clock and rotating refresh token.

— JobAdder REST API v2

Verified against JobAdder's official OpenAPI v2 spec. It's public and high-confidence — no [VERIFY] rows here.

Environments & prerequisite

ThingValue
Identity (auth) hosthttps://id.jobadder.com (authorize + token) — same host for prod and test
API hosthttps://api.jobadder.com/v2 — but don't hardcode it. The token response returns a per-account api base URL; store it with the tokens and prefix every call with that.
PrerequisiteRegister an application at developers.jobadder.com/register to get a client_id and client_secret. No partner agreement needed. For a sandbox, register a separate Test application (its own client id/secret) and connect a test account — there is no separate sandbox host.

Auth — OAuth2 authorization code

StepCallReturns / note
1. AuthorizeGET https://id.jobadder.com/connect/authorize?response_type=code&client_id={id}&scope={scopes}&redirect_uri={uri}&state={csrf}Redirects to redirect_uri?code={authCode}&state=.... Auth code valid 5 minutes.
2. Token (first exchange)POST https://id.jobadder.com/connect/token (form-encoded) body grant_type=authorization_code&client_id={id}&client_secret={secret}&code={authCode}&redirect_uri={uri}{ access_token, expires_in:3600, token_type:"Bearer", refresh_token, api:"https://api.jobadder.com/v2" }. Store the api base URL with the tokens — it prefixes every call.
3. Refresh (rotation)POST https://id.jobadder.com/connect/token body grant_type=refresh_token&client_id={id}&client_secret={secret}&refresh_token={rt}New access_token and a new refresh_token (rotating) + a fresh api URL. Persist the new refresh token every time or you lock yourself out. Refresh requires the offline_access scope.

Token / scope model — one bearer header

Every v2 call authenticates with the OAuth access_token as a standard bearer token. There's no second static key.

ThingValueLifetime
Authorization: Bearer {access_token}The access_token from token/refreshexpires_in:3600 (~60 minutes); refresh proactively.
refresh_tokenReturned on token and on every refresh — rotatesNo fixed expiry, but single-use. Persist the newest. Needs the offline_access scope.
Scopesread, write, offline_access plus fine-grained (read_job, read_candidate, write_note, read_candidate_note, …)Requested at authorize time; grant only what each agent needs.

Key operations

Base = the api URL from the token response (e.g. https://api.jobadder.com/v2). Header Authorization: Bearer {access_token} on each.

OperationCallNote
Get candidateGET /candidates/{candidateId}firstName, lastName, email, skillTags[], education[], ….
Get jobGET /jobs/{jobId}Skills/requirements live in skillTags.tags (JobOrderSkillTags = { matchAll, tags[] }); also category / custom fields. No /jobs/{id}/skills.
List CV / filesGET /candidates/{id}/attachments?type=Resume&latest=trueEach item { attachmentId, type, category, fileName, fileType }.
Download a fileGET /candidates/{id}/attachments/{attachmentId}Returns the raw binary file.
CV → textNo parsed-resume-text endpoint. Closest is full-text search over the latest resume: GET /candidates?keywords={terms}"Search for key words within the latest candidate resume." Extract text yourself if you need the body.
Search / listGET /candidates?offset={n}&limit={n} · GET /jobs?offset={n}&limit={n}limit max 1000; limit=0 returns only totalCount. Response envelope { items:[…], totalCount, links:{ first, prev, next, last } }.
Write a note / activityPOST /candidates/{candidateId}/notes (also POST /jobs/{jobId}/notes)Body AddCandidateNoteCommand { text (REQUIRED), type?, applicationId?[], reference? }201 NoteModel. JobAdder has no separate "activity" resource — activities are notes. (Status moves via PUT /candidates/{id}/status.)

Limits, paging, sandbox

ConcernBehaviour
Token lifetimeaccess_token expires_in:3600 (~60 min) — refresh before then.
Refresh tokenRotates on every refresh (new refresh_token in the body each time) — persist the newest. Requires the offline_access scope.
Rate limitsJobAdder applies API throttling, but the exact numbers live behind its Zendesk help centre. Don't invent one — consult JobAdder's API Throttling guide (or api@jobadder.com) and implement HTTP 429 backoff defensively.
Paginationoffset + limit (max 1000) → envelope { items, totalCount, links{ first, prev, next, last } }. Prefer following links.next over computing offsets.
Sandbox vs prodNo separate sandbox host. Register a separate Test application at developers.jobadder.com/register (own client id/secret) and connect a test account.

Docs: api.jobadder.com/v2/docs (interactive) · developers.jobadder.com · spec mirror github.com/vitaliymashkov/jobadder-api/blob/master/jobadder-openapi-v2.json

— Side-by-side, at a glance

BullhornJobAdder
Auth modelOAuth2 → REST login (3-legged + swimlane lookup)OAuth2 authorization code
Per-call credentialBhRestToken (session)Authorization: Bearer {access_token} (one header)
Short-token lifeaccess_token 10 min; session reused until 401access_token expires_in:3600 (~60 min)
Refresh tokenRotates (single-use)Rotates (new refresh token on every refresh); needs offline_access
Job entityJobOrder/jobs/{id} (skills in skillTags.tags)
Search vs querysearch (index, lazy) and query (DB, consistent)List with offset/limit; resume full-text via ?keywords=
Write-backPUT entity/NotePOST /candidates/{id}/notes (activities = notes)
SandboxRequest a test corp; resolve swimlaneRegister a separate Test application
Doc confidencePublic official docs (high)Public OpenAPI v2 spec (high)

Two systems, one job: read the spec, do the work, write the decision back where a human will see it. The auth dance is just the cover charge.

For where every figure and claim traces back to, see Sources & Further Reading.

the-math-no-recruiter-can-win-by-hand
what-an-ai-agent-actually-is
the-leash
the-toolkit
the-model-small-capable-swappable
talking-to-your-ats
use-case-1-resume-screening-against-a-job
the-shape-of-the-loop
running-it-thought-action-observation
use-case-2-cv-formatting-redacting-for-clients
reformatting-into-your-branded-template
resume-shortlisting
that-was-easy
security-compliance
keeping-pii-out-of-the-llm
exceptions-reliability
silent-api-drift-the-ats-changes-under-you
when-it-fails-anyway-dead-letter-and-the-leash
monitoring-observability
maintenance-the-lifecycle
the-scorecard-success-metrics-kpis
build-vs-buy-vs-managed
what-an-engineer-actually-costs
what-the-wider-data-says-happens-next
conclusion-how-this-gets-run-for-you
the-promises-behind-the-service
fuller-code-listings
one-full-screening-react-loop-semantic-kernel
env-deployment-reference
secrets-in-dev-vs-production
bullhorn-jobadder-endpoint-cheat-sheets
sources-further-reading
compliance-primary-law-sources

Download the full PDF for free?

Free download — no account required

Get the PDF
Get the PDF
Related Chapters
Free Download
Get the full PDF
All pages, including all code examples, diagrams, and the appendix reference card.
No spam. Unsubscribe at any time.
Your email won't be shared.
Oops! There's a problem with your request. We're working on fixing it. Please try again later.