The PHP SDK
Everything in bifrost-orbit/careers-sdk for PHP websites, from setup and every client method to applications with files, the candidate portal, refreshing on job changes, errors and tests.
Reference7 min read
The PHP SDK, bifrost-orbit/careers-sdk, puts your jobs on a PHP website: a client for the delivery API, applications with a CV and files, talent pool sign-ups, the candidate portal, a webhook check for refreshing cached pages and a fake transport for tests. It works with Laravel, Symfony, WordPress or plain PHP, and is open source under the MIT licence.
It reads the same endpoints and answers with the same shapes as the JavaScript Careers SDK.
Install
composer require bifrost-orbit/careers-sdkIt needs PHP 8.1 or later and ext-json. Requests go through ext-curl when it is installed and PHP streams when it is not, so there is nothing else to install. To use your own HTTP client, pass any PSR-18 client.
Set up
Create a public key under API keys.
CAREERS_API_URL=https://api.bifrostorbit.com/careers/v1CAREERS_PUBLIC_KEY=careers_pk_...CAREERS_WEBHOOK_SECRET=whsec_... # for the website webhookuse BifrostOrbit\Careers\Client;$careers = new Client( publicKey: getenv('CAREERS_PUBLIC_KEY'), apiUrl: getenv('CAREERS_API_URL'), locale: 'de',);// or, reading CAREERS_API_URL, CAREERS_PUBLIC_KEY and CAREERS_LOCALE:$careers = Client::fromEnvironment();publicKeycareers_pk_…). Required.apiUrlhttps://api.bifrostorbit.com/careers/v1 by default; https://api-stage.bifrostorbit.com/careers/v1 for staginglocalede. Without it, the career site's main language answers.timeoutretrieshttpClientTransport, or any PSR-18 client with requestFactory and streamFactory| Option | What it does |
|---|---|
publicKey | A public key (careers_pk_…). Required. |
apiUrl | https://api.bifrostorbit.com/careers/v1 by default; https://api-stage.bifrostorbit.com/careers/v1 for staging |
locale | Used when a call names no language, such as de. Without it, the career site's main language answers. |
timeout | Per attempt, in milliseconds. 5000 by default. |
retries | Extra attempts at a read after a network or server error. 2 by default. Applications and sign-ups are sent once. |
httpClient | A Transport, or any PSR-18 client with requestFactory and streamFactory |
Client::create([...]) takes the same options as an array. An unknown option is refused, so a typo never goes unnoticed.
Client methods
Every method answers with plain associative arrays, in the shapes the delivery API describes.
getJobs([department, location, remote, type, q, page, perPage, locale])items, locale, total, page, perPagegetJob($slug, [locale])NotFoundException when it is closed or gone.findJob($slug, [locale])nullgetFilters([locale])getSite([locale])getSitemap()apply($slug, $application, [cv, questions])applicationId and updatedjoinTalentPool($signup)applicationUrl($slug)| Method | Answers with | |
|---|---|---|
getJobs([department, location, remote, type, q, page, perPage, locale]) | Live jobs, newest first: items, locale, total, page, perPage | |
getJob($slug, [locale]) | One job with its description, form, privacy notice and JobPosting data. Throws NotFoundException when it is closed or gone. | |
findJob($slug, [locale]) | The same, or null | |
getFilters([locale]) | Departments, locations, job types and workplaces that have live jobs, with counts | |
getSite([locale]) | The career site's name, introduction, logo, colours, languages and current privacy notice | |
getSitemap() | Every live job with its address in each language | |
apply($slug, $application, [cv, questions]) | Sends an application: applicationId and updated | |
joinTalentPool($signup) | Keeps someone's details for future jobs | |
| `trackJob($slug, 'view'\ | 'start', [source, utmSource])` | Counts a view or an apply start for the reports. Never throws. |
applicationUrl($slug) | Where applications for the job are sent |
$list = $careers->getJobs(['q' => 'guide', 'remote' => 'hybrid']);foreach ($list['items'] as $job) { echo $job['title'], ' ', $job['url'], "\n";}Text that isn't translated falls back to the main language field by field. $job['fallback'] lists those fields, and $job['locales'] the languages the job is written in, for hreflang links.
Applications
use BifrostOrbit\Careers\Upload;$job = $careers->getJob('mountain-guide-lofoten');$result = $careers->apply('mountain-guide-lofoten', [ 'firstName' => 'Kari', 'lastName' => 'Nordmann', 'email' => 'kari@northwilds.com', 'phone' => '+47 912 34 567', 'answers' => ['q-years' => 6, 'q-areas' => ['lofoten', 'lyngen'], 'q-first-aid' => 'yes'], 'privacyVersion' => $job['privacy']['version'], 'privacyAccepted' => true, 'talentPool' => true, 'source' => 'northwilds.com', 'utm' => ['source' => $_GET['utm_source'] ?? null], 'referral' => $_GET['ref'] ?? null,], [ 'cv' => Upload::fromPath($_FILES['cv']['tmp_name'], $_FILES['cv']['name']), 'questions' => ['q-certificate' => Upload::fromPath($_FILES['certificate']['tmp_name'], $_FILES['certificate']['name'])],]);- Send the standard fields
$job['form']['fields']asks for, andanswerskeyed by question id. Choice answers are option ids; yes or no answers are'yes'or'no'. privacyAcceptedmust betrueandprivacyVersionthe version the candidate read. If the notice changed since, the call fails with a request to reload.- Files are PDF or Word documents up to 10 MB.
new Upload($name, $contents, $type)works for files you already hold in memory. - A second application to the same job from the same email updates the first, and answers
'updated' => true.
Candidate portal
PortalClient lets candidates follow their applications on your own website. Keep the session token in something that lasts between requests, such as the PHP session, by implementing Portal\SessionStore; Portal\MemorySession keeps it in memory.
use BifrostOrbit\Careers\Portal\SessionStore;use BifrostOrbit\Careers\PortalClient;final class PhpSession implements SessionStore{ public function get(): ?string { return $_SESSION['careers_portal'] ?? null; } public function set(?string $token): void { $_SESSION['careers_portal'] = $token; }}$portal = new PortalClient(publicKey: getenv('CAREERS_PUBLIC_KEY'), locale: 'de', session: new PhpSession());$portal->requestCode('solveig@northwilds.com');$portal->signIn('solveig@northwilds.com', '123456');$me = $portal->me(); // null when signed outrequestCode($email)signIn($email, $code)me()null when signed outwithdraw($applicationId, $reason)addDocument($applicationId, Upload $file)openOffers()acceptOffer($offerId, ['name' => …, 'consent' => true, 'documentHash' => …])declineOffer($offerId, $reason)offerPdf($offerId)exportData()askToCorrect($detail), askToErase()stayInTalentPool(), leaveTalentPool()signOut()| Method | What it does | |
|---|---|---|
requestCode($email) | Emails a six-digit code if the address has applied | |
signIn($email, $code) | Keeps the session token, which lasts two hours, and answers with it | |
me() | Applications, offers, talent pool and requests, or null when signed out | |
withdraw($applicationId, $reason) | Withdraws an open application | |
addDocument($applicationId, Upload $file) | Adds a PDF or Word document | |
openOffers() | Notes that the candidate has seen their open offers | |
acceptOffer($offerId, ['name' => …, 'consent' => true, 'documentHash' => …]) | Accepts an offer with a simple electronic signature | |
declineOffer($offerId, $reason) | Declines an offer | |
offerPdf($offerId) | The offer as a PDF, signed once accepted | |
exportData() | The candidate's data as JSON | |
askToCorrect($detail), askToErase() | Opens a data request | |
stayInTalentPool(), leaveTalentPool() | Renews or withdraws talent pool consent | |
| `stopTexts('sms'\ | 'whatsapp')` | Stops text messages on one channel |
signOut() | Ends the session |
Calls that change something answer with the whole portal again. A 401 throws SignedOutException and clears the session store. Leave out publicKey when apiUrl is your own server, which adds the key.
Refresh on job changes
Point the website webhook at a route like this one:
use BifrostOrbit\Careers\Webhook;$body = file_get_contents('php://input');if (!Webhook::verify(getenv('CAREERS_WEBHOOK_SECRET'), $_SERVER['HTTP_X_ORBIT_TIMESTAMP'] ?? '', $body, $_SERVER['HTTP_X_ORBIT_SIGNATURE'] ?? '')) { http_response_code(401); exit;}$message = json_decode($body, true);foreach (Webhook::tags($message['event'] ?? '', $message['data'] ?? []) as $tag) { $cache->invalidateTags([$tag]); // careers, careers:jobs or careers:job:<slug>}Webhook::verify also refuses messages older than five minutes.
Errors
Failed calls throw BifrostOrbit\Careers\Exception\CareersException, or one of its kinds. Each has the HTTP status and the API's errorCode, and a message candidates can read in the language of the request.
NotFoundExceptionAuthExceptionSignedOutExceptionRateLimitExceptionretryAfter is the number of seconds to wait, 60 unless the answer says otherwise.SubscriptionInactiveExceptionUnavailableExceptionCareersException422 invalid) or a website not on the allowed list (403)| Exception | When |
|---|---|
NotFoundException | No such job, or it is closed, scheduled or unpublished (404) |
AuthException | No key, or a key that is wrong, revoked or expired (401) |
SignedOutException | The candidate's portal session is missing or has expired (401) |
RateLimitException | Too many requests (429). retryAfter is the number of seconds to wait, 60 unless the answer says otherwise. |
SubscriptionInactiveException | The organisation's Careers Orbit trial or plan has ended, so the career site is paused (402). Not retried. Show a calm "temporarily unavailable" page with your own name; jobs come back as soon as an owner chooses a plan. |
UnavailableException | Careers Orbit could not be reached, did not answer in time, or failed on its side, after every retry |
CareersException | Anything else, such as a missing field (422 invalid) or a website not on the allowed list (403) |
See Troubleshooting for the messages and their fixes.
Tests
BifrostOrbit\Careers\Testing\FakeTransport answers from a queue and records each request, so your tests never call Careers Orbit:
use BifrostOrbit\Careers\Client;use BifrostOrbit\Careers\Testing\FakeTransport;$transport = (new FakeTransport())->json(['data' => [], 'meta' => ['total' => 0]]);$careers = new Client(publicKey: 'careers_pk_test', httpClient: $transport);$careers->getJobs(['remote' => 'hybrid']);$transport->lastRequest()->url; // https://api.bifrostorbit.com/careers/v1/jobs?remote=hybridWas this page helpful?