# Bullhorn REST API The Bullhorn REST API is a comprehensive HTTP/JSON API for the Bullhorn staffing platform, providing full programmatic access to ATS (Applicant Tracking System) and CRM functionality. It enables external systems to create, read, update, and delete staffing data across all core entities including Candidates, JobOrders, Placements, ClientContacts, and ClientCorporations. The API follows standard REST conventions, supports both Lucene-based full-text search and SQL-like JPQL queries, handles file attachments, resume parsing, event subscriptions, and mass updates at scale. Authentication uses an OAuth 2.0 flow: an access token is obtained through Bullhorn's OAuth authorization server, then exchanged for a `BhRestToken` session key via the `/login` endpoint. The `BhRestToken` must accompany all subsequent requests as a query parameter, HTTP header, or cookie. All API URLs are namespaced by corporation token (e.g., `https://rest{swimlane#}.bullhornstaffing.com/rest-services/{corpToken}/`), which is returned at login time. Sessions expire and must be refreshed; use the `/ping` endpoint to check session validity before making calls. --- ## GET /rest-services/loginInfo — Discover correct data-center URL Before authenticating, retrieve the correct data-center-specific login URL for a given username. Using the wrong data-center URL results in a `307` redirect. ```shell # Step 1: Discover login URL for the API user curl "https://rest.bullhornstaffing.com/rest-services/loginInfo?username=myApiUser" # Step 2: Login with OAuth access token curl "https://rest.bullhornstaffing.com/login?access_token=YOUR_OAUTH_ACCESS_TOKEN&version=*&ttl=60" # Example Response { "BhRestToken": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", "restUrl": "https://rest42.bullhornstaffing.com/rest-services/e999/" } # Step 3: Check session expiry curl "https://rest42.bullhornstaffing.com/rest-services/e999/ping?BhRestToken=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" # Response: { "sessionExpires": 1323449994922 } # Step 4: Logout when done curl "https://rest42.bullhornstaffing.com/rest-services/e999/logout?BhRestToken=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" # Response: { logout: "OK" } ``` --- ## GET /entity/{entityType}/{entityId} — Read a single entity Retrieve one or more entity records by ID, specifying which fields to return. The `id` field is always included. Supports embedded metadata, private label filtering, and editable-status flags. ```shell # Fetch a single Candidate by ID curl "https://rest42.bullhornstaffing.com/rest-services/e999/entity/Candidate/5059165?fields=firstName,lastName,address,status&BhRestToken=TOKEN" # Example Response { "id": 5059165, "firstName": "Alanzo", "lastName": "Smith", "address": { "address1": "100 Main St", "city": "Sacramento", "state": "CA", "zip": "95814", "countryID": 1 }, "status": "Active" } # Fetch multiple Candidates in one call (comma-separated IDs) curl "https://rest42.bullhornstaffing.com/rest-services/e999/entity/Candidate/123,456,789?fields=id,firstName,lastName,status&BhRestToken=TOKEN" # Response { "data": [ { "id": 123, "firstName": "Alanzo", "lastName": "Smith", "status": "Active" }, { "id": 456, "firstName": "Janis", "lastName": "Williams", "status": "New Lead" }, { "id": 789, "firstName": "Marcus", "lastName": "Jones", "status": "Active" } ] } # Read today's effective version of an effective-dated entity (e.g., Location) curl "https://rest42.bullhornstaffing.com/rest-services/e999/entity/Location/1234?fields=address,title&BhRestToken=TOKEN" # Read the version effective on a specific date curl "https://rest42.bullhornstaffing.com/rest-services/e999/entity/Location/1234?fields=address,title&effectiveOn=2027-12-31&BhRestToken=TOKEN" # Read all versions of an effective-dated entity curl "https://rest42.bullhornstaffing.com/rest-services/e999/entity/Location/1234/versions?fields=address,effectiveDate,effectiveEndDate&BhRestToken=TOKEN" # Response: { "data": [ { "id": 1234, "versionId": 123, "effectiveDate": "2020-01-01", ... }, ... ] } # Read to-many associations (ClientContacts of a ClientCorporation) curl "https://rest42.bullhornstaffing.com/rest-services/e999/entity/ClientCorporation/5059165/clientContacts?fields=firstName,lastName,email1&count=25&BhRestToken=TOKEN" ``` --- ## PUT /entity/{entityType} — Create a new entity Create a new entity record by sending a JSON body. Returns the new entity's ID and change type. To-many associations cannot be set at creation time; use a subsequent PUT association call. ```shell # Create a new Candidate curl -X PUT \ -H "Content-Type: application/json" \ -d '{ "firstName": "Jane", "lastName": "Doe", "email1": "jane.doe@example.com", "phone": "617-555-0100", "status": "New Lead", "address": { "address1": "55 Summer St", "city": "Boston", "state": "MA", "zip": "02110", "countryID": 1 } }' \ "https://rest42.bullhornstaffing.com/rest-services/e999/entity/Candidate?BhRestToken=TOKEN" # Example Response { "changedEntityId": 6001234, "changeType": "INSERT" } # Create a new JobOrder linked to an existing ClientContact (to-one association) curl -X PUT \ -H "Content-Type: application/json" \ -d '{ "title": "Senior Java Developer", "clientContact": { "id": 4500 }, "clientCorporation": { "id": 1200 }, "numOpenings": 2, "salary": 130000, "isOpen": true, "status": "Accepting Candidates" }' \ "https://rest42.bullhornstaffing.com/rest-services/e999/entity/JobOrder?BhRestToken=TOKEN" # Response: { "changedEntityId": 900123, "changeType": "INSERT" } # Add to-many associations (associate Skills to a Candidate) curl -X PUT \ "https://rest42.bullhornstaffing.com/rest-services/e999/entity/Candidate/6001234/primarySkills/964,684,253?BhRestToken=TOKEN" ``` --- ## POST /entity/{entityType}/{entityId} — Update an existing entity Update fields on an existing entity. Only supply the fields you want to change; read-only fields are ignored. ```shell # Update Candidate status and email curl -X POST \ -H "Content-Type: application/json" \ -d '{ "id": 5059165, "status": "Active", "email1": "updated.email@example.com", "owner": { "id": 3301 } }' \ "https://rest42.bullhornstaffing.com/rest-services/e999/entity/Candidate/5059165?BhRestToken=TOKEN" # Example Response { "changedEntityId": 5059165, "changeType": "UPDATE" } # Soft-delete a ClientContact (sets isDeleted=true, does not remove from DB) curl -X POST \ -H "Content-Type: application/json" \ -d '{ "isDeleted": true }' \ "https://rest42.bullhornstaffing.com/rest-services/e999/entity/ClientContact/1804?BhRestToken=TOKEN" # Create an additional version of an effective-dated entity (Location) curl -X POST \ -H "Content-Type: application/json" \ -d '{ "effectiveDate": "2026-01-01", "title": "Updated Office Location", "address": { "address1": "500 Boylston St", "city": "Boston" } }' \ "https://rest42.bullhornstaffing.com/rest-services/e999/entity/Location/1234?BhRestToken=TOKEN" ``` --- ## DELETE /entity/{entityType}/{entityId} — Delete an entity or association Hard-deletes hard-deletable entities; soft-deletes (sets `isDeleted=true`) soft-deletable entities. Cannot delete immutable entities (BusinessSector, Category, Country, ClientCorporation, Skill, Specialty, State, TimeUnit). ```shell # Hard delete a NoteEntity curl -X DELETE \ "https://rest42.bullhornstaffing.com/rest-services/e999/entity/NoteEntity/2552?BhRestToken=TOKEN" # Response: HTTP 200 (no body for hard delete) # Remove a to-many association (disassociate a Skill from a Candidate) curl -X DELETE \ "https://rest42.bullhornstaffing.com/rest-services/e999/entity/Candidate/3084/primarySkills/253?BhRestToken=TOKEN" ``` --- ## GET /search/{entityType} — Full-text Lucene search Search entities against a Lucene index. Supports field-specific queries, wildcards, ranges, boolean operators, and relevance scoring. Supported entities: Candidate, ClientContact, ClientCorporation, JobOrder, Lead, Note, Opportunity, Placement, Task. ```shell # Search for Candidates with last name Smith, return top 3 results curl "https://rest42.bullhornstaffing.com/rest-services/e999/search/Candidate?query=lastName:Smith&fields=id,firstName,lastName,status&count=3&sort=-dateAdded&BhRestToken=TOKEN" # Response { "data": [ { "_score": 1.70002, "id": 5059165, "firstName": "Alanzo", "lastName": "Smith", "status": "Active" } ] } # Search for active, non-deleted Candidates with Java skills (compound query) curl "https://rest42.bullhornstaffing.com/rest-services/e999/search/Candidate?query=isDeleted:0+AND+status:Active+AND+primarySkills.value:Java&fields=id,firstName,lastName,primarySkills&count=25&BhRestToken=TOKEN" # POST form for long queries (IDs list exceeds 7500 chars) curl -X POST \ -H "Content-Type: application/json" \ -d '{"query": "id:10125 10126 10127 10128 10129 10130 10131 10132 10133 10134"}' \ "https://rest42.bullhornstaffing.com/rest-services/e999/search/Candidate?fields=id,firstName,lastName&count=50&BhRestToken=TOKEN" # Discover available Lucene index fields for an entity (no query params) curl "https://rest42.bullhornstaffing.com/rest-services/e999/search/Candidate?BhRestToken=TOKEN" ``` --- ## GET /query/{entityType} — JPQL database query Query entities using Java Persistence Query Language (JPQL/SQL-like) syntax against the database. Use for entities not supported by search, or when precise SQL filtering is needed. ```shell # Query ClientContacts by last name curl "https://rest42.bullhornstaffing.com/rest-services/e999/query/ClientContact?where=lastName='Smith'&fields=id,firstName,lastName,email1&count=10&BhRestToken=TOKEN" # Response { "data": [ { "id": 5059165, "firstName": "Alanzo", "lastName": "Smith", "email1": "a.smith@corp.com" } ] } # Complex JPQL: active placements with salary over 100k, ordered by dateAdded curl "https://rest42.bullhornstaffing.com/rest-services/e999/query/Placement?where=status='Active'+AND+salary>100000+AND+isDeleted=false&fields=id,candidate,jobOrder,salary,status,dateAdded&orderBy=dateAdded&count=50&BhRestToken=TOKEN" # Get count only (no data returned) curl "https://rest42.bullhornstaffing.com/rest-services/e999/query/Candidate?where=isDeleted=false+AND+status='Active'&fields=id&totalOnly=true&BhRestToken=TOKEN" # Response: { "total": 4821 } # POST form for long where clauses curl -X POST \ -H "Content-Type: application/json" \ -d '{"where": "id IN (10125, 10126, 10127, 10128, 10129, 10130)"}' \ "https://rest42.bullhornstaffing.com/rest-services/e999/query/ClientContact?fields=id,firstName,lastName&BhRestToken=TOKEN" ``` --- ## GET /find — FastFind (intent-aware search) FastFind automatically detects the intent of a search query (email address, phone number, entity ID, or person/company name) and searches the appropriate fields across multiple entity types simultaneously. ```shell # Search by last name across all supported entity types curl "https://rest42.bullhornstaffing.com/rest-services/e999/find?query=Smith&countPerEntity=5&BhRestToken=TOKEN" # Search by email address (detected automatically) curl "https://rest42.bullhornstaffing.com/rest-services/e999/find?query=jane.doe%40example.com&countPerEntity=3&BhRestToken=TOKEN" # Search by phone number curl "https://rest42.bullhornstaffing.com/rest-services/e999/find?query=617-555-0100&countPerEntity=3&BhRestToken=TOKEN" # Search by entity ID (numeric-only query) curl "https://rest42.bullhornstaffing.com/rest-services/e999/find?query=5059165&countPerEntity=1&BhRestToken=TOKEN" # Search with wildcard (single-word with * expands to firstName search) curl "https://rest42.bullhornstaffing.com/rest-services/e999/find?query=Jan*&countPerEntity=10&BhRestToken=TOKEN" # Example Response { "data": [ { "id": 5059165, "firstName": "Alanzo", "lastName": "Smith" } ] } ``` --- ## GET /meta and GET /meta/{entityType} — Entity metadata Retrieve metadata describing entity structure, field types, data types, max lengths, validation rules, and association info. Use `fields=*` to retrieve all field metadata. ```shell # List all available entity types curl "https://rest42.bullhornstaffing.com/rest-services/e999/meta?BhRestToken=TOKEN" # Response: [{ "entity": "Candidate", "metaUrl": "..." }, { "entity": "JobOrder", "metaUrl": "..." }, ...] # Full metadata for the Candidate entity curl "https://rest42.bullhornstaffing.com/rest-services/e999/meta/Candidate?fields=*&BhRestToken=TOKEN" # Response (excerpt) { "entity": "Candidate", "label": "Candidate", "fields": [ { "name": "id", "type": "ID", "dataType": "Integer" }, { "name": "firstName", "type": "SCALAR", "dataType": "String", "maxLength": 50, "required": false }, { "name": "lastName", "type": "SCALAR", "dataType": "String", "maxLength": 50, "required": true }, { "name": "status", "type": "SCALAR", "dataType": "String", "inputType": "SELECT", "options": [{ "value": "Active", "label": "Active" }, { "value": "New Lead", "label": "New Lead" }] }, { "name": "owner", "type": "TO_ONE", "dataType": "Integer", "associatedEntity": { "entity": "CorporateUser" } }, { "name": "primarySkills", "type": "TO_MANY", "associatedEntity": { "entity": "Skill" } } ] } # Inline metadata on any entity/search/query call using meta parameter curl "https://rest42.bullhornstaffing.com/rest-services/e999/entity/Candidate/5059165?fields=firstName,lastName&meta=full&BhRestToken=TOKEN" ``` --- ## GET/PUT/POST/DELETE /file — File attachments Attach, retrieve, update, and delete files on Candidate, ClientContact, ClientCorporation, JobOrder, Opportunity, and Placement entities. Files can be uploaded as base64-encoded JSON or multipart/form-data. ```shell # Download a file as base64-encoded text curl "https://rest42.bullhornstaffing.com/rest-services/e999/file/Candidate/3835/231?BhRestToken=TOKEN" # Response: { "File": { "contentType": "text/plain", "fileContent": "VGhpcyBpcyBh...", "name": "Resume.txt" } } # Download raw (browser-friendly, triggers download) curl "https://rest42.bullhornstaffing.com/rest-services/e999/file/Candidate/3835/231/raw?BhRestToken=TOKEN" # List file attachments for a JobOrder curl "https://rest42.bullhornstaffing.com/rest-services/e999/entity/JobOrder/203866/fileAttachments?fields=id,name,contentType,fileSize,dateAdded&BhRestToken=TOKEN" # Upload a resume file (base64-encoded) curl -X PUT \ -H "Content-Type: application/json" \ -d '{ "externalID": "resume-2024", "fileContent": "VGhpcyBpcyBhIHZlcnkgc21hbGwgdGV4dCBmaWxlLg==", "fileType": "SAMPLE", "name": "JaneDoe_Resume.pdf", "contentType": "application/pdf", "description": "Resume for Jane Doe", "type": "cover" }' \ "https://rest42.bullhornstaffing.com/rest-services/e999/file/Candidate/5097909?BhRestToken=TOKEN" # Response: { "fileId": 178 } # Upload via multipart form (raw) curl -X PUT \ -F "file=@/path/to/resume.pdf" \ "https://rest42.bullhornstaffing.com/rest-services/e999/file/Candidate/5097909/raw?filetype=SAMPLE&externalID=resume-2024&BhRestToken=TOKEN" # Soft-delete a file attachment curl -X DELETE \ "https://rest42.bullhornstaffing.com/rest-services/e999/file/Candidate/3835/231?BhRestToken=TOKEN" # Response: { "fileId": 231, "changeType": "DELETED" } ``` --- ## POST /resume/parseToCandidate — Parse resume to Candidate data Parse a resume file or text block into structured Candidate, CandidateWorkHistory, and CandidateEducation data. The result is unsaved — use the response fields as the body of a subsequent `PUT /entity/Candidate` call. ```shell # Parse a resume file (multipart upload) curl -X POST \ -F "file=@/path/to/candidate_resume.pdf" \ "https://rest42.bullhornstaffing.com/rest-services/e999/resume/parseToCandidate?format=pdf&populateDescription=html&BhRestToken=TOKEN" # Parse resume text sent as JSON curl -X POST \ -H "Content-Type: application/json" \ -d '{"resume": "Jane Doe\n123 Main St, Boston MA 02110\njane@example.com\n617-555-0100\n\nExperience:\nSenior Developer, Acme Corp 2019-2024\n\nEducation:\nB.Sc Computer Science, MIT 2015"}' \ "https://rest42.bullhornstaffing.com/rest-services/e999/resume/parseToCandidateViaJson?format=text&populateDescription=text&BhRestToken=TOKEN" # Example Response { "candidate": { "firstName": "Jane", "lastName": "Doe", "email1": "jane@example.com", "phone": "617-555-0100", "address": { "address1": "123 Main St", "city": "Boston", "state": "MA", "zip": "02110", "countryID": 1 }, "description": "Jane Doe\n123 Main St..." }, "candidateWorkHistory": [ { "companyName": "Acme Corp", "title": "Senior Developer", "startDate": 1546300800000, "endDate": 1704067200000 } ], "candidateEducation": [ { "school": "MIT", "degree": "B.Sc", "major": "Computer Science", "graduationDate": 1420070400000 } ] } # Convert resume file to HTML (for storing in candidate description) curl -X POST \ -F "file=@/path/to/resume.docx" \ "https://rest42.bullhornstaffing.com/rest-services/e999/resume/convertToHtml?format=docx&BhRestToken=TOKEN" # Parse to HR-XML format curl -X POST \ -F "file=@/path/to/resume.pdf" \ "https://rest42.bullhornstaffing.com/rest-services/e999/resume/parseToHrXml?format=pdf&BhRestToken=TOKEN" ``` --- ## POST /massUpdate/{entityType} — Bulk update entities Update a field on up to 10,000 entity records in a single call. First check `GET /massUpdate/{entityType}` for supported fields and required entitlements. ```shell # See which entity types support mass update curl "https://rest42.bullhornstaffing.com/rest-services/e999/massUpdate?BhRestToken=TOKEN" # Response: ["Candidate","ClientContact","ClientCorporation","JobOrder","JobSubmission","Lead","Opportunity","Placement","Task","Tearsheet"] # See supported fields for JobOrder mass update curl "https://rest42.bullhornstaffing.com/rest-services/e999/massUpdate/JobOrder?BhRestToken=TOKEN" # Response: [{ "propertyName": "isOpen", "entitlementRequired": "Mass Open/Close Job" }, ...] # Mass-close multiple JobOrders curl -X POST \ -H "Content-Type: application/json" \ -d '{ "ids": [900123, 900124, 900125], "isOpen": false }' \ "https://rest42.bullhornstaffing.com/rest-services/e999/massUpdate/JobOrder?BhRestToken=TOKEN" # Response: { "count": 3 } # Mass-reassign Candidates to a new owner (to-one property) curl -X POST \ -H "Content-Type: application/json" \ -d '{ "ids": [5059165, 5059166, 5059167], "owner": 3301 }' \ "https://rest42.bullhornstaffing.com/rest-services/e999/massUpdate/Candidate?BhRestToken=TOKEN" # Mass-update to-many: add and remove assigned users on multiple JobOrders curl -X POST \ -H "Content-Type: application/json" \ -d '{ "ids": [900123, 900124], "assignedUsers": { "add": [3301, 3302], "remove": [3299] } }' \ "https://rest42.bullhornstaffing.com/rest-services/e999/massUpdate/JobOrder?BhRestToken=TOKEN" ``` --- ## PUT/GET/DELETE /event/subscription — Event subscriptions Subscribe to real-time entity change events (INSERT, UPDATE, DELETE) using a named subscription queue. Poll the subscription to receive batches of events. ```shell # Create a subscription for Candidate and Placement events curl -X PUT \ "https://rest42.bullhornstaffing.com/rest-services/e999/event/subscription/myIntegrationSub?type=entity&names=Candidate,Placement&eventTypes=INSERTED,UPDATED,DELETED&BhRestToken=TOKEN" # Response { "lastRequestId": 0, "subscriptionId": "myIntegrationSub", "createdOn": 1335285871323, "jmsSelector": "JMSType='ENTITY' AND BhCorpId=44 AND BhEntityName IN ('Candidate','Placement') AND BhEntityEventType IN ('INSERTED','UPDATED','DELETED')" } # Poll for up to 100 events curl "https://rest42.bullhornstaffing.com/rest-services/e999/event/subscription/myIntegrationSub?maxEvents=100&BhRestToken=TOKEN" # Response { "requestId": 1, "events": [ { "eventId": "ID:JBM-40000517", "eventType": "ENTITY", "eventTimestamp": 1495559294820, "eventMetadata": { "PERSON_ID": "1314", "TRANSACTION_ID": "c8d8f9ea-5ae6-4346-831c-29b91fcb703d" }, "entityName": "Candidate", "entityId": 5059165, "entityEventType": "UPDATED", "updatedProperties": ["status", "owner"] } ] } # Re-fetch events from a specific requestId (replay) curl "https://rest42.bullhornstaffing.com/rest-services/e999/event/subscription/myIntegrationSub?maxEvents=100&requestId=1&BhRestToken=TOKEN" # Get last processed requestId curl "https://rest42.bullhornstaffing.com/rest-services/e999/event/subscription/myIntegrationSub/lastRequestId?BhRestToken=TOKEN" # Response: { "result": 5 } # Delete subscription curl -X DELETE \ "https://rest42.bullhornstaffing.com/rest-services/e999/event/subscription/myIntegrationSub?BhRestToken=TOKEN" # Response: { "result": true } ``` --- ## POST /association/{entity}/{field} — Bulk fetch association IDs Given a list of entity IDs, retrieve the IDs of their associated entities for a specified to-many or to-one field in a single call. ```shell # Get all primarySkill IDs for multiple Candidates at once curl -X POST \ -H "Content-Type: application/json" \ -d '{ "ids": [7681, 2625, 1464], "showTotalMatched": true, "start": 0, "count": 100 }' \ "https://rest42.bullhornstaffing.com/rest-services/e999/association/Candidate/primarySkills?BhRestToken=TOKEN" # Response: each inner array is [entityId, associatedEntityId] { "total": 24, "data": [ [7681, 10115], [2625, 19739], [1464, 241506] ] } ``` --- ## POST /services/CCPA/notifyOnCapture — CCPA data capture notification Send CCPA data capture notification emails and create notes on person records (Candidate, ClientContact, Lead) to comply with California Consumer Privacy Act requirements. ```shell curl -X POST \ -H "Content-Type: application/json" \ -d '{ "entity": "Candidate", "ids": [5059165, 5059166, 5059167] }' \ "https://rest42.bullhornstaffing.com/rest-services/e999/services/CCPA/notifyOnCapture?BhRestToken=TOKEN" # Response { "results": { "SUCCESS": [5059165, 5059166, 5059167], "FAILURE": [] }, "overallStatus": "SUCCESS", "message": "Notify on capture email has been sent and note added.", "successIds": [5059165, 5059166, 5059167], "failureIds": [] } ``` --- ## PUT/POST /services/CorporateUser — Create or update a system user Create or update CorporateUser accounts including all profile fields, department assignments, SAML/SSO configuration, login restrictions, and custom fields. ```shell # Create a new CorporateUser (PUT) curl -X PUT \ -H "Content-Type: application/json" \ -d '{ "firstName": "Jane", "lastName": "Doe", "username": "JaneDoe", "password": "SecureP@ss123", "email": "jane.doe@company.com", "enabled": true, "userType": { "id": 456789 }, "departments": [{ "id": 123456, "isPrimary": true }], "address": { "address1": "55 Summer St", "city": "Boston", "state": "MA", "zip": "02110", "countryID": 1 } }' \ "https://rest42.bullhornstaffing.com/rest-services/e999/services/CorporateUser?BhRestToken=TOKEN" # Response: { "changedEntityType": "CorporateUser", "changedEntityId": 123456, "changeType": "INSERT", "data": {} } # Update an existing CorporateUser (POST) curl -X POST \ -H "Content-Type: application/json" \ -d '{ "email2": "jane.alt@company.com", "mobile": "6175550199" }' \ "https://rest42.bullhornstaffing.com/rest-services/e999/services/CorporateUser/123456?BhRestToken=TOKEN" # Response: { "changedEntityType": "CorporateUser", "changedEntityId": 123456, "changeType": "UPDATE", "data": {} } ``` --- ## PUT /services/BillableCharge — Create a billable charge Create a billable charge with bill master transactions, earn codes, and customer required fields. Requires either a placement or billing profile. ```shell curl -X PUT \ -H "Content-Type: application/json" \ -d '{ "description": "Consulting Services - April 2024", "periodEndDate": "2024-04-30", "placement": { "id": 272 }, "billMasters": [ { "quantity": 80.0, "transactionDate": "2024-04-30", "amount": 12000.0, "earnCode": { "id": 5 }, "rate": 150.0, "customerRequiredFields": [ { "customerRequiredFieldMeta": { "id": 1 }, "textValue": "Project Alpha" } ] } ] }' \ "https://rest42.bullhornstaffing.com/rest-services/e999/services/BillableCharge?BhRestToken=TOKEN" ``` --- ## POST /services/PlacementChangeRequest/approve/{id} — Approve a placement change request Approve a PlacementChangeRequest and apply its field changes to the associated Placement. Can be called with an empty body or with override fields. ```shell # Approve with system defaults (current user, current timestamp, default status) curl -X POST \ "https://rest42.bullhornstaffing.com/rest-services/e999/services/PlacementChangeRequest/approve/123?BhRestToken=TOKEN" # Response: { "message": "success", "placementID": 70695, "placementChangeRequest": { "requestStatus": "Approved", "id": 123 } } # Approve with custom approving user and date curl -X POST \ -H "Content-Type: application/json" \ -d '{ "approvingUser": { "id": 24 }, "dateApproved": 1716523200001, "requestStatus": "Approved - Verified" }' \ "https://rest42.bullhornstaffing.com/rest-services/e999/services/PlacementChangeRequest/approve/123?BhRestToken=TOKEN" ``` --- ## PUT /services/IssueReport — Report integration issues Create user-facing issue records linked to entities for integration error reporting (e.g., payroll export failures, missing required fields from external systems). ```shell curl -X PUT \ -H "Content-Type: application/json" \ -d '{ "issues": [ { "action": "Payroll Export", "actionEntity": "Placement", "actionEntityId": 70695, "externalSystemName": "ADP Workforce", "issueItems": [ { "severity": "Error", "errorType": "MISSING-DATA", "fieldReference": "taxForm", "description": "Federal tax form is required for payroll export." }, { "severity": "Warning", "errorType": "INVALID-DATA", "fieldReference": "address.zip", "description": "Work location zip code format is invalid." } ] } ] }' \ "https://rest42.bullhornstaffing.com/rest-services/e999/services/IssueReport?BhRestToken=TOKEN" # Response: [{ "changedEntityType": "Issue", "changedEntityId": 1234, "changeType": "INSERT", "data": {} }] ``` --- The Bullhorn REST API is the foundation for building integrations with the Bullhorn staffing platform. Common integration patterns include: ATS synchronization (syncing Candidates and JobOrders with external talent platforms using the entity CRUD endpoints and event subscriptions for real-time change detection), payroll and billing system integrations (using the Pay & Bill entity endpoints, BillableCharge service, and PayExportBatch status reporting to connect with downstream financial systems), and resume ingestion pipelines (using the resume parse endpoints to ingest resumes from job boards or email, then creating Candidate records with the parsed structured data). For robust production integrations, the recommended pattern is to use `PUT /event/subscription` to create a named subscription on relevant entity types, poll `GET /event/subscription/{id}` regularly for batches of change events, look up the affected entities via `GET /entity/{type}/{id}`, and use the `TRANSACTION_ID` from event metadata to correlate changes with audit history. Always resolve the correct `restUrl` at login time rather than hardcoding a data-center URL, implement 429 retry-with-backoff logic for rate limiting, refresh the `BhRestToken` when a 401 is received, and prefer `POST /search` or `POST /query` over the GET forms when filter expressions exceed 7500 characters.