Frequently asked questions

What is darvis/bullhorn-api?

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 when Bullhorn ends it, and gives you get(), post(), put(), delete() and helpers for entities, search, query, meta and file uploads. It works in any PHP project; in Laravel it adds a service provider, a facade, an encrypted session cache and php artisan bullhorn:test.

What do I need to use it?

PHP 8.2 or newer and REST API credentials from Bullhorn: a client id, a client secret, and the username and password of an API user. Without Laravel the default transport needs the cURL extension. The Laravel integration supports Laravel 11, 12 and 13.

Why does the session have to be shared between requests?

Bullhorn limits how often an API user may log in and may block logins that come too often. A login takes four requests. In Laravel the session is kept, encrypted, in your cache, so every web request, worker and command uses the same one. Without Laravel, pass a FileTokenStore with a directory outside the web root; the default MemoryTokenStore logs in once per PHP process.

What happens when the session expires?

Bullhorn answers 401. The client then looks in the store for a session another process made in the meantime, and else renews with the refresh token, and repeats the call once. Bullhorn replaces the refresh token on every renewal, so the renewal runs under a lock: two processes never use the same refresh token. When the refresh token is no longer accepted, the client logs in again.

How do I check whether Bullhorn is up?

Call isAlive(). It logs in when needed, pings Bullhorn and returns true or false; it never throws. Without a session it costs a login, so keep the answer for a minute, for example with Cache::remember('bullhorn-alive', 60, fn () => Bullhorn::isAlive()). To see why it says false, call ping() in a try/catch or run php artisan bullhorn:test.

How do I create or update an entity?

Bullhorn uses PUT to create and POST to update, the other way round from most APIs. Use createEntity('Candidate', $data) and updateEntity('Candidate', $id, $data), which pick the right verb. A rejected entity throws a BullhornException with errorKey() (such as errors.cannotPersistEntity) and invalidFields(), the names of the fields Bullhorn refused.

Are my credentials safe?

The client secret only goes in the body of the token request and the session token only in the BhRestToken header. Bullhorn documents the username and password as query parameters of its authorize step and the access token as a query parameter of its REST login, so those two URLs carry them; the client never quotes them. Secrets and tokens are removed from every exception message, and the Laravel cache holds the session encrypted with your application key.

Can I use it without Laravel?

Yes. The client is plain PHP with no dependencies. Build it with new Client(new Config(...), store: new FileTokenStore('/var/lib/myapp/bullhorn')) or Client::fromEnv(). Nothing outside the Laravel folder imports Laravel, and a test in the package checks that.

How do I test code that calls Bullhorn?

In Laravel, use Http::fake(): the client sends through Laravel’s HTTP client. Without Laravel, pass a FakeTransport, which answers the four login steps by itself and records what you sent, so your test only describes the API calls.

How do I move from jonathanraftery/bullhorn-rest-client?

Replace rawRequest($method, $path, ['body' => json_encode($data)]) with post(), put() or the entity helpers, which take an array. Resolve the client from the container instead of constructing it, and drop the data store and credentials provider: the session lives in the token store and the credentials in the config. The migration page lists every change.