JSON endpoint
GET /hire/jobs.json?department=engineering&jobType=full-time&q=php&page=2
Anonymous and CORS-open (Access-Control-Allow-Origin: *), so a bespoke front end, a careers
widget on another domain or a static site can read it. It is the same Listing call the built-in
page and craft.hire.results() make — same filters, same semantics — so they can never disagree.
Parameters
| Parameter | |
|---|---|
<groupHandle> | A term slug, or several (?department[]=engineering&department[]=design). OR within a group, AND across |
q | Keywords |
page | Page number. Past the end returns an empty list, not a 404 |
limit | Jobs per page, capped so one request can't build every job on the site |
orderBy | postDate, expiryDate, applications, title, dateCreated, dateUpdated or a custom field handle, then asc or desc. Anything else falls back to the default sort |
workplaceType | onsite, hybrid or remote |
A term that does not exist is ignored rather than erroring, so a stale bookmark shows an unfiltered page.
Response
{
"total": 14,
"page": 2,
"perPage": 10,
"totalPages": 2,
"filters": { "department": ["engineering"] },
"jobs": [
{
"id": 104,
"code": "JOB-104",
"title": "Senior PHP Developer",
"url": "https://example.com/careers/senior-php-developer",
"location": "Bristol",
"workplaceType": "hybrid",
"salary": "£55,000–£65,000 a year",
"openings": 2,
"postDate": "2026-09-01T09:00:00+00:00",
"expiryDate": "2026-10-31T23:59:00+00:00",
"acceptingApplications": true,
"specs": [
{ "group": "department", "name": "Engineering", "slug": "engineering" }
]
}
]
}
filters is always an object, even when nothing is filtered, so filters.department never throws
on the first request.