darvis / bullhorn-api
PHP client for the Bullhorn REST API (staffing and recruitment CRM): OAuth login, a shared session that survives concurrent requests, entity, search and file helpers, and a Laravel integration with facade, encrypted session cache and Http::fake() support.
Requires
- php: ^8.2
- ext-json: *
Requires (Dev)
- ext-curl: *
- larastan/larastan: ^3.0
- laravel/pint: ^1.0
- orchestra/testbench: ^9.0|^10.0|^11.0
- pestphp/pest: ^3.0|^4.0
- pestphp/pest-plugin-laravel: ^3.0|^4.0
Suggests
- ext-curl: Required by the default transport, for projects without Laravel.
- laravel/framework: Enables the service provider, the facade, the encrypted session cache and Http::fake() support (Laravel 11, 12 or 13).
Provides
None
Conflicts
- illuminate/support: <11.0
Replaces
None
README
A PHP client for the Bullhorn REST API, the API of the Bullhorn staffing and recruitment CRM. It logs in with OAuth, keeps one session for all your requests, renews it safely when Bullhorn ends it, and gives you helpers that use Bullhorn's own verbs. The client is plain PHP; in Laravel it comes with a facade, an encrypted session cache and a connection test.
Features
- Is Bullhorn alive?
isAlive()answerstrueorfalseand never throws: one line for a health check - Examples for every common task: thirteen runnable scripts and a page of Laravel recipes, all commented step by step
- The whole login: loginInfo, authorize, token and REST login, on the first call and not before
- One shared session: in Laravel's cache or a file outside the web root, so Bullhorn sees one login instead of one per request
- Safe renewal: on a 401 the client takes the session another process renewed, or renews under a lock with the refresh token, and repeats the call once
- Bullhorn's verbs:
createEntity()sends PUT,updateEntity()sends POST, plusentity(),search(),query(),meta()andupload() - Failures you can act on: the whole response in the message,
errorKey()andinvalidFields(), and a separateAuthenticationExceptionfor a failed login - Safe with your credentials: secrets and tokens are removed from every exception message
- Laravel: the
Bullhornfacade,Http::fake()in your tests andphp artisan bullhorn:test - Plain PHP: no dependencies; cURL by default, or your own transport and store
- Laravel Boost: guideline and skill included, so an AI assistant in your app knows the pitfalls
Requirements
- PHP 8.2+
- Bullhorn REST API credentials: client id, client secret, API username and password
- For the Laravel integration: Laravel 11, 12 or 13
- Without Laravel: the cURL extension
Quick start (Laravel)
composer require darvis/bullhorn-api
BULLHORN_CLIENT_ID=your-client-id BULLHORN_CLIENT_SECRET=your-client-secret BULLHORN_USERNAME=your.api.user BULLHORN_PASSWORD=the-password
php artisan bullhorn:test
use Darvis\BullhornApi\Laravel\Facades\Bullhorn; $candidate = Bullhorn::entity('Candidate', 123, ['id', 'firstName', 'email'])['data']; $id = Bullhorn::createEntity('Candidate', ['firstName' => 'Anna', 'lastName' => 'Smit', 'name' => 'Anna Smit'])['changedEntityId']; Bullhorn::updateEntity('Candidate', $id, ['phone' => '0612345678']); $found = Bullhorn::search('Candidate', 'email:"anna@example.com"', 'id,firstName');
Quick start (plain PHP)
use Darvis\BullhornApi\Client; use Darvis\BullhornApi\Config; use Darvis\BullhornApi\Token\FileTokenStore; $bullhorn = new Client( new Config(clientId: '...', clientSecret: '...', username: '...', password: '...'), store: new FileTokenStore('/var/lib/myapp/bullhorn'), ); $bullhorn->updateEntity('Candidate', 123, ['status' => 'Active']);
Is Bullhorn alive?
isAlive() logs in when needed, pings Bullhorn, and answers true or false. It never throws, so it fits in an if:
use Darvis\BullhornApi\Laravel\Facades\Bullhorn; if (Bullhorn::isAlive()) { // Bullhorn answers: show the vacancies, accept the application. } else { // Show "we are working on it" instead of an error page. }
Without a session it costs a login, and Bullhorn limits logins. Keep the answer for a minute instead of asking on every page view:
use Illuminate\Support\Facades\Cache; $alive = Cache::remember('bullhorn-alive', 60, fn () => Bullhorn::isAlive());
isAlive() tells you that something is wrong. To see what, call ping(): it throws an AuthenticationException for a login problem and a BullhornException for anything else, with the reason in the message. On the command line, php artisan bullhorn:test does both and prints the reason.
Without Laravel it is the same method: $bullhorn->isAlive().
Examples
New to Bullhorn or to this package? Start here.
-
examples/: thirteen small scripts, each doing one thing, commented step by step. Run the first one to check your credentials:
php examples/01-is-bullhorn-alive.php
The others read a candidate, search by email, list open jobs, page through results, create and update a candidate, handle a rejected update, add a note, apply for a job, parse a CV, attach a file and list the fields of an entity. The ones that change data refuse to run without
--write. -
Examples page: the same tasks in a Laravel application, with a health check URL, a Livewire form, a queued job for applications and tests.
Documentation
The full documentation is at https://arviddejong.github.io/bullhorn-api/: installation, how the login and the shared session work, the API reference, testing, troubleshooting, and migrating from jonathanraftery/bullhorn-rest-client.
Laravel Boost
This package ships a guideline and a skill for Laravel Boost. Run php artisan boost:install, or php artisan boost:update --discover in a project that already uses Boost.
Testing
composer test # Pest composer lint # Pint, check only; composer format fixes composer analyse # Larastan
Changelog
See CHANGELOG.
Support the package
If darvis/bullhorn-api saves you time, buy me a beer 🍺: sponsor me on GitHub.
Contributing
See CONTRIBUTING.
Security
Please report a vulnerability privately, as described in SECURITY, not in the issue tracker.
License
The MIT License (MIT). See LICENSE.