Endpoints / Retrieve a job

Retrieve a job

Fetch a single posting by id, with its description if you ask for one.

GET/v1/jobs/{id}
GEThttps://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#

id
stringpathrequired

The 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_format
"text" | "html"default: omitted

Include 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:

Response
{
  "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#

StatusCause
400Any query parameter other than description_format, or a bad value for it.
401Missing, unknown, or revoked key.
404No posting with that id.
429Rate limit exceeded, or monthly quota exhausted.
500Server error. Retry with backoff.
404
{ "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.

Was this page helpful?