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?

Download full PDF
build-it-in-a-weekend.pdf
Oops! Something went wrong while submitting the form.
Related Chapters