# JazzHR Jobs API

> Query jobs from JazzHR boards through one API — and why JazzHR has no JSON API, only an HTML table and per-job JSON-LD.

Source: https://hyperjobs.io/ats/jazzhr

JazzHR is recruiting software for small teams, with boards at
`<slug>.applytojob.com`. It's the one platform in this pool with **no JSON API at
all** — no feed, no widget endpoint, no public REST. There's an HTML listing page
and there are per-job detail pages carrying JobPosting JSON-LD, and that's the
whole surface. You are parsing markup, and the markup is what you get.

## Try it

```bash
curl -G https://api.hyperjobs.io/v1/jobs \
  -H "Authorization: Bearer $HYPERJOBS_KEY" \
  --data-urlencode "source=jazzhr" \
  --data-urlencode "time_frame=7d" \
  --data-urlencode "limit=2"
```

```json title="Illustrative — the shape, not a live response"
{
  "data": [
    {
      "id": "jazzhr:aBcD3fGh1J",
      "title": "Field Service Technician",
      "company": {
        "slug": "ridgeline-hvac",
        "name": "Ridgeline HVAC",
        "domain": "ridgelinehvac.com",
        "website": "https://ridgelinehvac.com",
        "logo": "https://media.licdn.com/dms/image/…",
        "industry": "Construction",
        "employee_count": 38
      },
      "location": {
        "remote": false,
        "countries": ["United States"],
        "cities": ["Reno"],
        "regions": ["NV"],
        "raw": ["Reno, NV, United States"]
      },
      "employment_type": ["FULL_TIME"],
      "work_arrangement": null,
      "seniority": "Mid-Senior level",
      "salary": {
        "min": 28,
        "max": 34,
        "currency": "USD",
        "unit": "HOUR",
        "annual_min": 58240,
        "annual_max": 70720,
        "summary": null
      },
      "skills": ["hvac", "refrigeration"],
      "taxonomies": ["Skilled Trades"],
      "apply_url": "https://ridgelinehvac.applytojob.com/apply/jobs/details/aBcD3fGh1J",
      "url": "https://ridgelinehvac.applytojob.com/apply/jobs/details/aBcD3fGh1J",
      "source": "jazzhr",
      "posted_at": "2026-07-10T00:00:00.000Z",
      "expires_at": null,
      "updated_at": "2026-07-13T20:16:44.000Z"
    }
  ],
  "total": 2,
  "limit": 2,
  "offset": 0
}
```

## How JazzHR boards actually work

An HTML table, then a page per job:

```bash
# 1. Listing — an HTML table, one <tr id="row_job_…"> per posting
curl -L "https://<slug>.applytojob.com/apply/jobs"

# 2. Detail — JobPosting JSON-LD in the page head
curl -L "https://<slug>.applytojob.com/apply/jobs/details/<id>"
```

`<slug>` is the subdomain. Both need redirects followed.

The listing rows are the id source. Each `<tr id="row_job_…">` contains an anchor
with class `job_title_link` whose href is `/apply/jobs/details/<id>` — that's the
posting id and the title in one element. A `resumator_department` span carries the
department, and the row's last cell carries the location.

Everything else comes from the detail page's JSON-LD, which is where JazzHR
quietly redeems itself.

### The JSON-LD is better than you'd expect

Because it's schema.org rather than a bespoke JSON shape, you get standard fields
from a standard parser:

| JSON-LD field | Gives you |
| --- | --- |
| `description` | The full body |
| `datePosted` | The posting date — the listing has none |
| `validThrough` | A real declared expiry. Rare, but structured |
| `baseSalary` | Structured pay when the employer published it |
| `jobLocation.address` | Locality, region, country — better than the table cell |
| `jobLocationType` | `TELECOMMUTE` when remote. Absent otherwise |
| `experienceRequirements` | A plain level label — near 100% populated |
| `hiringOrganization.sameAs` | The employer's homepage — 100% |
| `hiringOrganization.logo` | The employer's logo — around 81% |

`experienceRequirements` is worth calling out: schema.org allows an object there,
but JazzHR only ever emits the string form — `Mid Level`, `Experienced`,
`Manager/Supervisor` — and it's on essentially every posting. That's free
seniority from a field most platforms don't populate at all.

`jobLocationType` is **only present on remote postings**. Its absence is not a
signal that the job is on-site; it's just absence. That's why
`work_arrangement` is null rather than `On-site` on most JazzHR rows.

## What bites you

<Warning title="Cloudflare-gated tenants come back hollow, not failed">
  Some JazzHR tenants sit behind Cloudflare and return a plain 403 to an ordinary
  client. The listing page often still parses — so you get rows, with ids and
  titles — while every JSON-LD detail fetch fails.

  Because the detail carries description, date, location **and** salary together,
  a blocked tenant yields a full board of hollow postings. Titles and nothing
  else. It looks like a company that publishes terrible job ads, not like a fetch
  problem.

  A browser-fingerprint tier clears many of them. Residential IPs are the real
  fix, and neither is a code change — this is the failure mode to know about
  rather than one we can parse our way out of.
</Warning>

**The listing is a table, and tables change.** Every field on the listing side is
a regex against markup: row ids, an anchor class, a span class, the last cell.
JazzHR owes you nothing here — there's no contract, no version, no deprecation
notice. It works until a template ships.

**The last cell isn't reliably the location.** It's the location because that's
where JazzHR puts it, not because anything says so. The JSON-LD's structured
address is better whenever the detail fetch succeeds, which is exactly when the
listing value matters least.

**`uniqueJobCode` is the requisition id**, and it's in the JSON-LD under a
non-obvious name.

## Or use ours

JazzHR is markup parsing plus an N+1, against 2,689 tracked boards, where a
meaningful slice of tenants will hand you title-only rows unless you're coming
from the right kind of client.

You get the rows joined to their JSON-LD, the structured address preferred over
the table cell, `experienceRequirements` mapped to a real seniority, salary parsed
from `baseSalary`, the employer's homepage and logo carried through, and the
hollow-tenant problem absorbed on our side rather than yours. Blocked tenants stay
in the rotation and yield sparse rows rather than poisoning a run — which is a
tradeoff we're making on your behalf and would rather you knew about.

Plus dedup, expiry by board diffing, and the [company
join](/docs/objects/company).

JazzHR is in the fast refresh tier, attempted every cycle — see
[coverage](/docs/reference/coverage).

[Quickstart](/docs/quickstart) · [Pricing](/pricing)
