Developer guide

Integrate with the Reach Vacancy API

The Reach Vacancy API is a RESTful toolkit for surfacing live recruitment data across your career site or back-office workflows. Each request is built from simple URL segments, making it easy to fetch vacancies, metadata and supporting content in the format your application needs.


API Key

Every account has a unique API key that is supplied to them upon request. During development you can use our dummy data key in lieu of recieving the live data key.
a83d66d29041c407e8abf4187c533053.
The API key forms part of the hostname, all requests should be over HTTPS. For example, development requests can target https://a83d66d29041c407e8abf4187c533053.reach-ats.com/.


TLDR

A condensed reference to the most commonly used JSON endpoints.

Vacancy listings

All live vacancies

External feed (use internalListing for internal sites).

View JSON listing

https://a83d66d29041c407e8abf4187c533053.reach-ats.com/get/job/listing/-/-/-/-/JSON

Radius search listing

Add postcode and range; internal variant available via internalListingByPostcode.

View postcode search JSON

https://a83d66d29041c407e8abf4187c533053.reach-ats.com/get/job/listingByPostcode/B610GD/250/-/-/-/-/JSON

Vacancy detail (combine for full detail)

Vacancy information

Use internalInformation for intranet flows.

View vacancy info

https://a83d66d29041c407e8abf4187c533053.reach-ats.com/get/job/information/129436/JSON

Vacancy advert text

Raw HTML copy for the advert.

View advert JSON

https://a83d66d29041c407e8abf4187c533053.reach-ats.com/get/job/advert/129436/JSON

Vacancy files

Supporting documents and metadata.

View file list

https://a83d66d29041c407e8abf4187c533053.reach-ats.com/get/job/fileurls/129436/JSON

Vacancy alerts signup

Embed via iframe to collect job alert registrations.

Load iframe demo

https://a83d66d29041c407e8abf4187c533053.reach-ats.com/jobalert/load/6/2

List retrievals (JSON)

Populate dropdowns and filters with live account metadata.

Fetch offices

https://a83d66d29041c407e8abf4187c533053.reach-ats.com/get/job/offices

Also available: locations, roles, types, counties.


Overview

You can query the API from any modern language, including PHP, Python, .NET, Perl and Java. and recieve the payload in the format that works best for you.

Default call (XML)

Returns all live jobs in XML format.

View example

N.B. View the page source in your browser; XML responses render as raw markup.

JSON call example

Use positional segments to switch the response type.

View JSON response

Anatomy of a request

Each request is constructed from five segments:

  1. Action — usually get.
  2. Package — the module you are querying, e.g. job.
  3. Data — the resource within that package, e.g. listing or advert.
  4. Parameters — positional arguments separated by slashes. Use a hyphen (-) for empty values.
  5. Response type (Encoding) (optional) — override the default format with XML, JSON, CSV, PIPE or RAW. This is always the last parameter of the URL.

Example: /get/job/listing/-/-/-/-/JSON fetches the job listing, skips optional filters, and requests JSON.

Sample XML response

<response generated="2011-06-30T16:48:07+01:00" records="3">
	<record>
		<id><![CDATA[25618]]></id>
		<title><![CDATA[Assistant Store Manager - Camden]]></title>
		<category><![CDATA[Sales]]></category>
		<role><![CDATA[Sales]]></role>
		<type><![CDATA[Permanent]]></type>
		<hours><![CDATA[35]]></hours>
		<salarydescription><![CDATA[25,000]]></salarydescription>
		<closingdate><![CDATA[2013-01-04]]></closingdate>
	</record>
</response>

Vacancy API

The vacancy module exposes all live job data. Calls are grouped into packages, with the job package covering everything from vacancy listings to supporting media. Use filters and alternate encodings to power different channels such as career sites, aggregators or partner feeds.

Job package endpoints

  • listing — returns all live vacancies with optional keyword, location, type, role and encoding arguments.
  • listingByPostcode — adds mandatory postcode and range parameters for radius-based searches.
  • information — fetches a single vacancy record by job ID, including contact and workflow data.
  • advert (alias text or description) — returns the full advert copy in RAW format.
  • applyurl / applyurlsource / applysource — resolve application URLs for external or internal sources.
  • fileurls — lists downloadable assets tied to the vacancy.

Listing variations

Endpoint Purpose Key parameters
/get/job/listing/KEYWORDS/LOCATION/TYPE/ROLE/ENCODING Default vacancy feed (XML by default). All filters optional; use - to skip and append the desired format.
/get/job/listingByPostcode/POSTCODE/RANGE/KEYWORDS/LOCATION/TYPE/ROLE/ENCODING Radius search by full or partial postcode. Provide postcode (max 15 chars) and range in miles; remaining filters mirror the base listing.
/get/job/listingBySource/SOURCES/KEYWORDS/LOCATION/TYPE/ROLE/ENCODING Limit results to one or more sources. First segment accepts comma-separated source IDs or short codes.
/get/job/listingBySourceAndPostcode/SOURCES/POSTCODE/RANGE/KEYWORDS/LOCATION/TYPE/ROLE/ENCODING Combines source filters with postcode radius. Adds postcode and range parameters to the source listing signature.
/get/job/internalListing/KEYWORDS/LOCATION/TYPE/ROLE/ENCODING Surfacing vacancies for internal audiences. Internal variants exist for postcode and source-based filtering.
/get/job/internalListingByPostcode/POSTCODE/RANGE/KEYWORDS/LOCATION/TYPE/ROLE/ENCODING Internal-only vacancies filtered by postcode radius. Matches the public postcode signature but limits sources to internal types.
/get/job/internalListingBySource/SOURCES/KEYWORDS/LOCATION/TYPE/ROLE/ENCODING Internal jobs available through specific sources. Accepts comma-separated internal source IDs or short codes.
/get/job/internalListingBySourceAndPostcode/SOURCES/POSTCODE/RANGE/KEYWORDS/LOCATION/TYPE/ROLE/ENCODING Hybrid filter for internal roles by source and geography. Combines the source list with postcode radius parameters.
/get/job/listingCounty/KEYWORDS/COUNTY/TYPE/ROLE/ENCODING County-specific vacancy search. Swap LOCATION for COUNTY to align with data surfaced by /get/lists/counties.
/get/job/referralListing/KEYWORDS/LOCATION/TYPE/ROLE/ENCODING Feed of roles assigned to employee referral programmes. Filters vacancies that have referral apply sources.
/get/job/speculativeListing/KEYWORDS/LOCATION/TYPE/ROLE/ENCODING Speculative opportunities open for general interest applications. Ideal when pairing with /get/lists/types filters.
/get/job/openDayListing/KEYWORDS/LOCATION/TYPE/ROLE/ENCODING Events and open day listings published as vacancies. Keeps the core filter arguments while surfacing event-specific metadata.

Job details & media

Endpoint Purpose Notes
/get/job/information/{JOB_ID}/ENCODING Full vacancy record, including contact and workflow data. Defaults to XML; pass /JSON etc. to change format.
/get/job/advert/{JOB_ID}/ENCODING Advert copy returned as RAW HTML. Aliases: /text, /description. Default encoding is RAW
/get/job/fileurls/{JOB_ID}/ENCODING Supporting documents with display names. XML by default; use /JSON for client apps.
/get/job/files/{JOB_ID}/ENCODING Detailed metadata for each vacancy file. Pairs with /file/{FILE_UID} to download content.
/get/job/webimage/{JOB_ID}/ENCODING Retrieves the primary job image. Streams the binary directly for hero imagery. Default encoding is RAW
/get/job/video/{JOB_ID}/ENCODING Job video embed markup. Use /video_embed for iframe-ready content. Default encoding is RAW
/get/job/applyurl/{JOB_ID}/ENCODING External application URL. See /applyurlsource/{JOB_ID}/{TYPE} for internal vs external sources. Default encoding is RAW
/get/job/referralurl/{JOB_ID}/ENCODING Referral programme application link. Targets referral source type. Default encoding is RAW

Job metadata (similar to the new Lists API below)

Endpoint Returns Notes
/get/job/locations Distinct location values across all jobs. Use for search filters and job alerts.
/get/job/counties Unique county list. Pairs with listingCounty for search.
/get/job/roles Distinct job roles. Provides options for discipline selectors.
/get/job/types Distinct employment types. Excludes the registration placeholder.
/get/job/live_locations Locations with currently live vacancies. Restrict filters to active options.
/get/job/live_roles Roles with live vacancies. Ideal for job alert pickers.
/get/job/regions Distinct regions from office addresses. Useful for multi-region sites.
/get/job/offices Office records including logos and CDN URLs. Power branded job cards or directory pages.
/get/job/groups Department or group identifiers. Map vacancies to internal teams.
/get/job/all_categories Managed category list with IDs. Returns inactive options too (status 0 filters applied).

Lists API

The lists module provides lightweight endpoints for populating filters, dropdowns and form pickers. Each call mirrors a metadata query from the Job controller but exposes it under the /get/lists namespace, returning JSON by default for easy client-side consumption.

Core list endpoints

Endpoint Returns Notes
/get/lists/categories Distinct job categories currently in use. Pulls from live job data so it always reflects published vacancies.
/get/lists/locations Unique list of locations. Use for geography filters on search forms.
/get/lists/counties Distinct counties across the account. Ideal for regional breakdowns alongside locations.
/get/lists/roles All role names referenced by vacancies. Populate speciality/discipline selectors.
/get/lists/types Distinct job types (e.g. Permanent, Part Time). Excludes the registration placeholder.

Live-only variants

Use the live_* endpoints to limit filter options to values that have active vacancies.

Endpoint Returns Notes
/get/lists/live_locations Locations associated with live jobs. Ideal for map filters or alert sign-up forms.
/get/lists/live_roles Role values that have at least one live vacancy. Keeps drop-downs free of empty categories.

Organisation lookups

Endpoint Returns Notes
/get/lists/offices Full office records including branding assets. Perfect for office directories or branded cards.
/get/lists/regions Distinct regions defined in office addresses. Align regional career pages with data.
/get/lists/groups Department or group IDs with display names. Synchronise internal team filters.
/get/lists/all_categories Managed category options (ID and label). Includes inactive catalogue entries for administration.

When to use the Lists controller

  • Seed search filters and job alert preferences without duplicating enumeration tables in your CMS.
  • Build client-side autocomplete or dropdown controls with a lightweight JSON payload.
  • Pair List responses with Job endpoints (e.g. listing) to offer contextual filtering.

Job Alerts

Job alert sign-up forms typically ask candidates to opt into roles, locations or employment types they are interested in. The vacancy metadata endpoints provide the lookup data you need to build these filters dynamically, ensuring your alert preferences always reflect the current catalogue.

Pre-built alert experience

Reach ships a responsive HTML page for job alert sign-up. Embed it inside an iframe or surface it within a modal/popup to collect alert registrations.

Preview alert page

It may be possible for us to create a bespoke sign up template for you mimicing your branding styles, please let us know if this is something you require.

Alert embed endpoints

The Jobalert module exposes helper routes for pre-built signup forms that you can iFrame or load in a modal. Swap the rows, version, client, or group parameters as needed.

Endpoint Purpose Key Options
/jobalert/load/{ROWS}/{VERSION} Default alert form with type/role filters. ROWS controls number item in the multple select boxes; VERSION picks template (1 legacy, 2 updated).
/jobalert/loadWithLocation/{ROWS}/{VERSION} Signup including location dropdown. VERSION 1 (legacy), 2 (jobalertlocation2), 3 (jobalertlocation3).
/jobalert/loadWithTown/{ROWS}/{VIEW} Signup using town list instead of region. Set VIEW to a template name (e.g. jobalertlocation2) or leave numeric for defaults.
Bespoke template endpoints
/jobalert/loadClient/{ROWS}/{CLIENT}/{LIVE} Serve a named client template with live or cached data. CLIENT view name (e.g. jobalert2); set LIVE=true for lists to only use current live vacancy data.
/jobalert/loadClientGroup/{ROWS}/{GROUP}/{CLIENT} Pre-filter form by group/department. GROUP slug or - for all; CLIENT selects the view template.

Code Samples

The PHP snippets below demonstrate how to consume listing data and specific vacancy detail. Swap in your account’s API key and adjust the rendering code to match your templating approach.

A basic API class implemented to display & filter vacancies (JSON)

<?php
class ReachApiClient {
	private string $host;

	public function __construct(private readonly string $apiKey) {
		$this->host = sprintf('https://%s.reach-ats.com', $apiKey);
	}

	public function get(string $path): string {
		$url = $this->host . $path;

		if (ini_get('allow_url_fopen')) {
			$response = @file_get_contents($url);
		} elseif (function_exists('curl_init')) {
			$curl = curl_init($url);
			curl_setopt_array($curl, [
				CURLOPT_RETURNTRANSFER => true,
				CURLOPT_CONNECTTIMEOUT => 5,
				CURLOPT_TIMEOUT => 10,
			]);
			$response = curl_exec($curl);
			curl_close($curl);
		} else {
			throw new RuntimeException('Unable to fetch data; enable allow_url_fopen or install cURL.');
		}

		if ($response === false) {
			throw new RuntimeException("Reach ATS request failed for {$url}");
		}

		return $response;
	}

	public function getJson(string $path): array {
		return json_decode($this->get($path), true, 512, JSON_THROW_ON_ERROR);
	}

	public static function renderOptions(array $values, ?string $selected = null): string {
		$items = array_unique(array_filter(array_map('strval', $values)));
		sort($items, SORT_NATURAL | SORT_FLAG_CASE);

		return implode('', array_map(static function (string $value) use ($selected): string {
			$isSelected = $selected !== null && strcasecmp($selected, $value) === 0;
			return sprintf(
				'<option value="%s"%s>%s</option>',
				rawurlencode($value),
				$isSelected ? ' selected' : '',
				htmlspecialchars($value, ENT_QUOTES, 'UTF-8')
			);
		}, $items));
	}

	public function listing(
		string $keywords = '-',
		string $location = '-',
		string $type = '-',
		string $role = '-',
		string $encoding = 'JSON'
	): array {
		return $this->getJson($this->buildPath('/get/job/listing/', [
			$keywords,
			$location,
			$type,
			$role,
			$this->formatEncoding($encoding),
		]));
	}

	public function listingByPostcode(
		string $postcode,
		string $range,
		string $keywords = '-',
		string $location = '-',
		string $type = '-',
		string $role = '-',
		string $encoding = 'JSON'
	): array {
		return $this->getJson($this->buildPath('/get/job/listingByPostcode/', [
			$postcode,
			$range,
			$keywords,
			$location,
			$type,
			$role,
			$this->formatEncoding($encoding),
		]));
	}

	public function listingBySource(
		string $sources,
		string $keywords = '-',
		string $location = '-',
		string $type = '-',
		string $role = '-',
		string $encoding = 'JSON'
	): array {
		return $this->getJson($this->buildPath('/get/job/listingBySource/', [
			$sources,
			$keywords,
			$location,
			$type,
			$role,
			$this->formatEncoding($encoding),
		]));
	}

	public function listingBySourceAndPostcode(
		string $sources,
		string $postcode,
		string $range,
		string $keywords = '-',
		string $location = '-',
		string $type = '-',
		string $role = '-',
		string $encoding = 'JSON'
	): array {
		return $this->getJson($this->buildPath('/get/job/listingBySourceAndPostcode/', [
			$sources,
			$postcode,
			$range,
			$keywords,
			$location,
			$type,
			$role,
			$this->formatEncoding($encoding),
		]));
	}

	public function internalListing(
		string $keywords = '-',
		string $location = '-',
		string $type = '-',
		string $role = '-',
		string $encoding = 'JSON'
	): array {
		return $this->getJson($this->buildPath('/get/job/internalListing/', [
			$keywords,
			$location,
			$type,
			$role,
			$this->formatEncoding($encoding),
		]));
	}

	public function internalListingByPostcode(
		string $postcode,
		string $range,
		string $keywords = '-',
		string $location = '-',
		string $type = '-',
		string $role = '-',
		string $encoding = 'JSON'
	): array {
		return $this->getJson($this->buildPath('/get/job/internalListingByPostcode/', [
			$postcode,
			$range,
			$keywords,
			$location,
			$type,
			$role,
			$this->formatEncoding($encoding),
		]));
	}

	public function internalListingBySource(
		string $sources,
		string $keywords = '-',
		string $location = '-',
		string $type = '-',
		string $role = '-',
		string $encoding = 'JSON'
	): array {
		return $this->getJson($this->buildPath('/get/job/internalListingBySource/', [
			$sources,
			$keywords,
			$location,
			$type,
			$role,
			$this->formatEncoding($encoding),
		]));
	}

	public function internalListingBySourceAndPostcode(
		string $sources,
		string $postcode,
		string $range,
		string $keywords = '-',
		string $location = '-',
		string $type = '-',
		string $role = '-',
		string $encoding = 'JSON'
	): array {
		return $this->getJson($this->buildPath('/get/job/internalListingBySourceAndPostcode/', [
			$sources,
			$postcode,
			$range,
			$keywords,
			$location,
			$type,
			$role,
			$this->formatEncoding($encoding),
		]));
	}

	public function listingByCounty(
		string $keywords = '-',
		string $county = '-',
		string $type = '-',
		string $role = '-',
		string $encoding = 'JSON'
	): array {
		return $this->getJson($this->buildPath('/get/job/listingCounty/', [
			$keywords,
			$county,
			$type,
			$role,
			$this->formatEncoding($encoding),
		]));
	}

	public function referralListing(
		string $keywords = '-',
		string $location = '-',
		string $type = '-',
		string $role = '-',
		string $encoding = 'JSON'
	): array {
		return $this->getJson($this->buildPath('/get/job/referralListing/', [
			$keywords,
			$location,
			$type,
			$role,
			$this->formatEncoding($encoding),
		]));
	}

	public function speculativeListing(
		string $keywords = '-',
		string $location = '-',
		string $type = '-',
		string $role = '-',
		string $encoding = 'JSON'
	): array {
		return $this->getJson($this->buildPath('/get/job/speculativeListing/', [
			$keywords,
			$location,
			$type,
			$role,
			$this->formatEncoding($encoding),
		]));
	}

	public function openDayListing(
		string $keywords = '-',
		string $location = '-',
		string $type = '-',
		string $role = '-',
		string $encoding = 'JSON'
	): array {
		return $this->getJson($this->buildPath('/get/job/openDayListing/', [
			$keywords,
			$location,
			$type,
			$role,
			$this->formatEncoding($encoding),
		]));
	}

	public function jobInformation(int|string $jobId, string $encoding = 'JSON'): array {
		return $this->getJson($this->buildPath('/get/job/information/', [
			$jobId,
			$this->formatEncoding($encoding),
		]));
	}

	public function jobAdvert(int|string $jobId): string {
		return $this->get($this->buildPath('/get/job/advert/', [$jobId]));
	}

	public function jobFileUrls(int|string $jobId, string $encoding = 'JSON'): array {
		return $this->getJson($this->buildPath('/get/job/fileurls/', [
			$jobId,
			$this->formatEncoding($encoding),
		]));
	}

	private function buildPath(string $prefix, array $segments): string {
		return $prefix . implode('/', array_map([$this, 'segment'], $segments));
	}

	private function segment(null|int|string $value): string {
		if ($value === null || $value === '') {
			return '-';
		}
		if ($value === '-') {
			return '-';
		}
		return rawurlencode((string) $value);
	}

	private function formatEncoding(?string $encoding): string {
		return strtoupper($encoding ?? 'JSON');
	}
}

$client = new ReachApiClient('a83d66d29041c407e8abf4187c533053');

$listing = $client->listing();

// Apply simple, client-side filtering beyond the API segments.
$filters = [
	'division' => $_GET['division'] ?? null,
	'officename' => $_GET['officename'] ?? null,
	'hours' => $_GET['hours'] ?? null,
	'role' => $_GET['role'] ?? null,
];

$filtered = array_filter($listing, static function (array $job) use ($filters): bool {
	foreach ($filters as $key => $value) {
		if ($value === null || $value === '') {
			continue;
		}
		if (!isset($job[$key]) || strcasecmp($job[$key], $value) !== 0) {
			return false;
		}
	}
	return true;
});

$options = [
	'division' => ReachApiClient::renderOptions(array_column($listing, 'division'), $filters['division']),
	'officename' => ReachApiClient::renderOptions(array_column($listing, 'officename'), $filters['officename']),
	'hours' => ReachApiClient::renderOptions(array_column($listing, 'hours'), $filters['hours']),
	'role' => ReachApiClient::renderOptions(array_column($listing, 'role'), $filters['role']),
];

?>

<form method="get" action="">
	<label>
		Division
		<select name="division" onchange="this.form.submit()">
			<option value="">All divisions</option>
			<?= $options['division']; ?>
		</select>
	</label>
	<label>
		Office
		<select name="officename" onchange="this.form.submit()">
			<option value="">All offices</option>
			<?= $options['officename']; ?>
		</select>
	</label>
	<label>
		Role
		<select name="role" onchange="this.form.submit()">
			<option value="">All roles</option>
			<?= $options['role']; ?>
		</select>
	</label>
	<label>
		Hours
		<select name="hours" onchange="this.form.submit()">
			<option value="">Any hours</option>
			<?= $options['hours']; ?>
		</select>
	</label>
	<noscript><button type="submit">Apply filters</button></noscript>
</form>

<p>Showing <strong><?= count($filtered); ?></strong> of <strong><?= count($listing); ?></strong> vacancies.</p>

<?php foreach ($filtered as $job): ?>
	<?php
		$jobId = $job['id'];
		$job = $client->jobInformation($jobId);
		$advertHtml = $client->jobAdvert($jobId);
		$attachments = $client->jobFileUrls($jobId);
	?>
	<section class="vacancy-summary">
		<h3><?= htmlspecialchars($job['title'], ENT_QUOTES, 'UTF-8'); ?></h3>
		<div class="vacancy-advert"><?= $advertHtml; ?></div>
		<?php if (!empty($attachments)): ?>
			<h4>Attachments</h4>
			<ul>
				<?php foreach ($attachments as $file): ?>
					<li>
						<a href="<?= htmlspecialchars($file['url'] ?? '#', ENT_QUOTES, 'UTF-8'); ?>" target="_blank">
							<?= htmlspecialchars($file['type'] ?? 'Download', ENT_QUOTES, 'UTF-8'); ?>
						</a>
					</li>
				<?php endforeach; ?>
			</ul>
		<?php endif; ?>
	</section>
<?php endforeach; ?>
?>

Fetch detail including advert text and files for a specific vacancy

<?php
try {
	$client = new ReachApiClient('a83d66d29041c407e8abf4187c533053');
	$jobId = 129436;
	$information = $client->jobInformation($jobId);
	$advertHtml = $client->jobAdvert($jobId);
	$fileAttachments = $client->jobFileUrls($jobId);
} catch (Throwable $exception) {
	error_log($exception->getMessage());
	// Handle gracefully (show fallback message, etc.)
}

if (!empty($information)) {
	$vacancy = $information[0];
	echo '<h3>' . htmlspecialchars($vacancy['title'] ?? 'Vacancy', ENT_QUOTES, 'UTF-8') . '</h3>';
	echo $advertHtml; // Already HTML

	if (!empty($fileAttachments)) {
		echo '<h4>Supporting documents</h4><ul>';
		foreach ($fileAttachments as $file) {
			$label = htmlspecialchars($file['type'] ?? 'Download', ENT_QUOTES, 'UTF-8');
			$url = htmlspecialchars($file['url'] ?? '#', ENT_QUOTES, 'UTF-8');
			echo '<li><a href="' . $url . '" target="_blank">' . $label . '</a></li>';
		}
		echo '</ul>';
	}
}

WordPress Theme Functions

Drop these helpers into your theme’s functions.php file and register shortcodes for listings, vacancy detail pages and alert signup URLs. The examples use strict typing, shared network helpers and defensive error handling.

Shared helpers

<?php
class Reach_Api_Helper {
	private string $host;

	public function __construct(private readonly string $apiKey) {
		$this->host = sprintf('https://%s.reach-ats.com', $apiKey);
	}

	public function get(string $path): string {
		$url = $this->host . $path;

		if (ini_get('allow_url_fopen')) {
			$response = @file_get_contents($url);
		} elseif (function_exists('curl_init')) {
			$curl = curl_init($url);
			curl_setopt_array($curl, [
				CURLOPT_RETURNTRANSFER => true,
				CURLOPT_CONNECTTIMEOUT => 5,
				CURLOPT_TIMEOUT => 10,
			]);
			$response = curl_exec($curl);
			curl_close($curl);
		} else {
			throw new RuntimeException('Reach request requires allow_url_fopen or cURL support.');
		}

		if ($response === false) {
			throw new RuntimeException("Reach request failed for {$url}");
		}

		return $response;
	}

	public function getJson(string $path): array {
		return json_decode($this->get($path), true, 512, JSON_THROW_ON_ERROR);
	}

	public function listing(
		string $keywords = '-',
		string $location = '-',
		string $type = '-',
		string $role = '-',
		string $encoding = 'JSON'
	): array {
		return $this->getJson($this->buildPath('/get/job/listing/', [
			$keywords,
			$location,
			$type,
			$role,
			$this->formatEncoding($encoding),
		]));
	}

	public function jobInformation(int|string $jobId, string $encoding = 'JSON'): array {
		return $this->getJson($this->buildPath('/get/job/information/', [
			$jobId,
			$this->formatEncoding($encoding),
		]));
	}

	public function jobAdvert(int|string $jobId): string {
		return $this->get($this->buildPath('/get/job/advert/', [$jobId]));
	}

	public function jobFileUrls(int|string $jobId, string $encoding = 'JSON'): array {
		return $this->getJson($this->buildPath('/get/job/fileurls/', [
			$jobId,
			$this->formatEncoding($encoding),
		]));
	}

	private function buildPath(string $prefix, array $segments): string {
		return $prefix . implode('/', array_map([$this, 'segment'], $segments));
	}

	private function segment(null|int|string $value): string {
		if ($value === null || $value === '') {
			return '-';
		}
		if ($value === '-') {
			return '-';
		}
		return rawurlencode((string) $value);
	}

	private function formatEncoding(?string $encoding): string {
		return strtoupper($encoding ?? 'JSON');
	}
}

Listing shortcode

Usage: [reach-listing key="YOUR_KEY" detail_slug="/vacancies/detail" qs_param="vacancy_id"]

function reach_listing_shortcode(array $atts): string {
	$atts = shortcode_atts([
		'key' => 'a83d66d29041c407e8abf4187c533053',
		'detail_slug' => '/vacancies/detail',
		'qs_param' => 'vacancy_id',
	], $atts, 'reach-listing');

	$api = new Reach_Api_Helper($atts['key']);

	try {
		$jobs = $api->listing();
	} catch (Throwable $exception) {
		error_log($exception->getMessage());
		return '<p>Vacancies are unavailable right now.</p>';
	}

	if (empty($jobs)) {
		return '<p>No live vacancies at the moment.</p>';
	}

	$output = '';
	foreach ($jobs as $job) {
		$advertExcerpt = !empty($job['shortdescription'])
			? $job['shortdescription']
			: substr(strip_tags($api->jobAdvert($job['id'])), 0, 200) . '...';

		$detailUrl = esc_url(add_query_arg($atts['qs_param'], $job['id'], $atts['detail_slug']));
		$applyUrl = esc_url($job['applyurl']);

		$output .= sprintf(
			'<article class="vacancy-card" data-group="%s">
				<header>
					<h3><a href="%s">%s</a></h3>
					<p><strong>Location:</strong> %s</p>
					<p><strong>Function:</strong> %s</p>
				</header>
				<p>%s</p>
				<p>
					<a class="btn btn-primary" target="_blank" href="%s">Apply now</a>
					<a class="btn btn-outline" href="%s">More detail</a>
				</p>
			</article>',
			esc_attr($job['group'] ?? ''),
			esc_url($detailUrl),
			esc_html($job['title']),
			esc_html($job['location'] ?? 'Not specified'),
			esc_html($job['role'] ?? 'Not specified'),
			esc_html($advertExcerpt),
			$applyUrl,
			esc_url($detailUrl)
		);
	}

	return $output;
}
add_shortcode('reach-listing', 'reach_listing_shortcode');

Vacancy detail shortcode

Usage: [reach-detail key="YOUR_KEY" qs_param="vacancy_id"]

function reach_detail_shortcode(array $atts): string {
	$atts = shortcode_atts([
		'key' => 'a83d66d29041c407e8abf4187c533053',
		'qs_param' => 'vacancy_id',
	], $atts, 'reach-detail');

	$vacancyId = isset($_GET[$atts['qs_param']]) ? sanitize_text_field($_GET[$atts['qs_param']]) : null;
	if (!$vacancyId) {
		return '<p>Please select a vacancy.</p>';
	}

	$api = new Reach_Api_Helper($atts['key']);

	try {
		$info = $api->jobInformation($vacancyId);
		$detail = $info[0] ?? [];
		$advert = $api->jobAdvert($vacancyId);
		$files = $api->jobFileUrls($vacancyId);
	} catch (Throwable $exception) {
		error_log($exception->getMessage());
		return '<p>We can’t load that vacancy right now.</p>';
	}

	if (empty($detail)) {
		return '<p>Sorry, we can’t find that vacancy in our system.</p>';
	}

	ob_start();
	?>
		<article class="vacancy-detail">
			<h2><?= esc_html($detail['title']); ?></h2>
			<p>
				<strong>Location:</strong> <?= esc_html($detail['location'] ?? ''); ?><br />
				<strong>Category:</strong> <?= esc_html($detail['category'] ?? ''); ?><br />
				<strong>Closing date:</strong> <?= esc_html($detail['closingdate'] ?? ''); ?>
			</p>
			<div class="vacancy-advert"><?= wp_kses_post($advert); ?></div>
			<p><a class="btn btn-primary" target="_blank" href="<?= esc_url($detail['applyurl'] ?? '#'); ?>">Apply now</a></p>

			<?php if (!empty($files)): ?>
				<h3>Supporting documents</h3>
				<ul>
					<?php foreach ($files as $file): ?>
						<li><a href="<?= esc_url($file['url'] ?? '#'); ?>" target="_blank"><?= esc_html($file['type'] ?? 'Download'); ?></a></li>
					<?php endforeach; ?>
				</ul>
			<?php endif; ?>
		</article>
	<?php
	return ob_get_clean();
}
add_shortcode('reach-detail', 'reach_detail_shortcode');

Job alert signup iframe URL

Usage: [reach-jobalert-url key="YOUR_KEY" account_id="6" template_id="2"]. Returns a ready-to-embed URL.

function reach_jobalert_url_shortcode(array $atts): string {
	$atts = shortcode_atts([
		'key' => 'a83d66d29041c407e8abf4187c533053',
		'account_id' => '6',
		'template_id' => '2',
	], $atts, 'reach-jobalert-url');

	return sprintf(
		'https://%s.reach-ats.com/jobalert/load/%d/%d',
		rawurlencode($atts['key']),
		(int) $atts['account_id'],
		(int) $atts['template_id']
	);
}
add_shortcode('reach-jobalert-url', 'reach_jobalert_url_shortcode');