- API reference
- Endpoints
Retrieve a job
Fetch a single posting by id, with its description if you ask for one.
https://api.hyperjobs.io/v1/jobs/{id}One posting by id. This is where you ask for a description — pulling them 200 at
a time from /v1/jobs is the usual reason that
endpoint feels slow.
curl -H "Authorization: Bearer $HYPERJOBS_KEY" \
"https://api.hyperjobs.io/v1/jobs/ashby:7c1f2a94-3e17-4d8b-9c05-1f6ab2e40d33?description_format=text"Path parameters#
idThe job's id, exactly as it appears in a /v1/jobs or /v1/jobs/feed
response. Ids are source-prefixed — ashby:7c1f…, greenhouse:12345 — and
are matched exactly, with no normalisation beyond URL-decoding.
The colon is legal in a path segment, so encoding is optional in practice.
Encode anyway (encodeURIComponent): ids come from upstream ATS systems and
aren't guaranteed to stay free of characters that need it.
Query parameters#
description_formatInclude the description, as clean plain text or sanitized html.
Omitted by default — the description key is absent from the response
entirely, not null. Any other value is a 400.
No other parameters — and strictly so: any query parameter other than
description_format is rejected with a 400. Filters don't apply to a
lookup by id. Unlike /v1/jobs, there's no
expires_at condition layered on either — an expired posting stays
retrievable by id while it's still in the pool. See the 404 semantics below.
Response#
The job object itself, not wrapped:
{
"id": "ashby:7c1f2a94-3e17-4d8b-9c05-1f6ab2e40d33",
"title": "Staff Backend Engineer",
"company": {
"slug": "linear",
"name": "Linear",
"domain": "linear.app",
"website": "https://linear.app",
"logo": "https://logo.hyperjobs.io/linear.app",
"industry": "Software Development",
"employee_count": 120
},
"location": {
"remote": true,
"countries": ["United States"],
"cities": null,
"regions": null,
"coords": null,
"timezones": ["America/New_York"],
"continent": "North America",
"raw": ["Remote (US)"]
},
"employment_type": ["FULL_TIME"],
"work_arrangement": "Remote",
"office_days": null,
"remote_location": ["United States"],
"department": "Engineering",
"team": "Sync Engine",
"requisition_id": "ENG-142",
"seniority": "Mid-Senior level",
"experience_level": "5-10",
"salary": {
"min": 190000,
"max": 240000,
"currency": "USD",
"unit": "YEAR",
"annual_min": 190000,
"annual_max": 240000,
"summary": "$190,000 – $240,000 a year"
},
"working_hours": 40,
"skills": ["typescript", "graphql", "postgresql"],
"keywords": ["distributed systems", "real-time sync"],
"taxonomies": ["Software"],
"education_requirements": ["bachelor degree"],
"visa_sponsorship": null,
"job_language": "en",
"description": "About Linear\n\nLinear is building…",
"requirements_summary": "8+ years building distributed backend systems…",
"core_responsibilities": "Own the sync engine's server-side…",
"benefits": ["Equity", "Health, dental & vision", "401(k) matching"],
"hiring_manager": null,
"apply_url": "https://jobs.ashbyhq.com/linear/7c1f2a94…",
"url": "https://jobs.ashbyhq.com/linear/7c1f2a94…",
"source": "ashby",
"source_type": "ats",
"source_domain": "jobs.ashbyhq.com",
"posted_at": "2026-07-08T14:02:11.000Z",
"expires_at": null,
"modified_at": "2026-07-12T08:30:05.000Z",
"modified_fields": ["salary_max"],
"created_at": "2026-07-08T14:10:22.000Z",
"updated_at": "2026-07-14T02:15:40.000Z"
}This response is not wrapped in `data`
/v1/jobs returns { data, total, limit, offset, next_cursor }. This endpoint returns the job at the top level. A
shared response handler that reaches for .data will get undefined here.
Every field is documented in the Job object.
employment_type, education_requirements, and benefits are real JSON
arrays — no JSON.parse needed. A set of fields is worth
knowing about: department, team, requisition_id, working_hours,
keywords, office_days, remote_location, hiring_manager,
source_domain, location.coords / location.timezones /
location.continent, and the change-tracking trio modified_at /
modified_fields / created_at. And title is still de-SHOUTed for display,
so it may not be the raw stored string that ?title= searches on the list
endpoint.
Errors#
| Status | Cause |
|---|---|
400 | Any query parameter other than description_format, or a bad value for it. |
401 | Missing, unknown, or revoked key. |
404 | No posting with that id. |
429 | Rate limit exceeded, or monthly quota exhausted. |
500 | Server error. Retry with backoff. |
{ "error": "not found" }What a 404 means — and what it doesn't
A 404 means the id is unknown, or the posting was folded into another one
as a duplicate: the same job syndicated to several boards collapses to one
surviving row, and the absorbed ids stop resolving. See
duplicates.
What it does not mean is "expired". An expired posting stays retrievable by
id while it's still in the pool — check expires_at on the payload if you
care. When you need to know which cached ids to purge, don't poll them one by
one: /v1/jobs/expired is the purge
feed. If an id does come back 404, treat it as "delete my copy" rather than
as an error to retry — retrying won't help, it'll just spend
quota.
/v1/jobs/feed is the feed endpoint, not a
job whose id is feed — the route excludes that one word explicitly. No real
id collides with it, since every id is source-prefixed.