# JobScore Jobs API

> Query jobs from JobScore boards through one API with ?source=jobscore — plus how JobScore's Atom feed and its structured salary really behave.

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

JobScore is an applicant tracking system for small and mid-sized employers.
Boards live on a shared host at `careers.jobscore.com/careers/<company>`, and
every one of them publishes an Atom feed carrying the whole board with
descriptions inline. One unauthenticated GET, no token, no detail pass. On the
board we tested that was 30 jobs, all 30 with descriptions.

Thirty. Nobody is building an ATS integration for thirty jobs, which is the
honest pitch for a platform this size: the work isn't the fetch, it's that this
is one of dozens of boards that each need their own quirks learned, and JobScore
has two worth learning.

## Salary, and why this one is different

Salary is the weakest field we serve. Across the whole dataset it's populated on
[36.0% of postings](/docs/reference/coverage#field-completeness) — `has_salary=true`
throws away roughly two thirds of everything before your other filters even run.
That's not a collection failure so much as a publishing one: most employers don't
post pay, and most of the platforms that let them don't model it. Where salary
does exist, it usually exists as **prose**, in the body, and getting a number out
means mining a sentence and hoping.

JobScore publishes a real one. It extends Atom with a `j:` namespace, and inside
each entry sits a `j:publicCompensation` block:

```xml title="Illustrative — the shape of an entry's compensation block"
<j:publicCompensation>
  <j:salaryMinimum>95000</j:salaryMinimum>
  <j:salaryMaximum>125000</j:salaryMaximum>
  <j:currencyCode>USD</j:currencyCode>
  <j:interval>year</j:interval>
  <j:formatted>$95,000 - $125,000 / year</j:formatted>
</j:publicCompensation>
```

Minimum, maximum, currency, interval, and the employer's own formatted string —
structured, typed, no regex over a paragraph. That is rare for an ATS at this end
of the market and it's the most interesting thing about the platform.
[Ashby](/ats/ashby), [Rippling](/ats/rippling) and [Pinpoint](/ats/pinpoint)
publish structured comp too; a small-business ATS bothering to is unusual.

## How JobScore boards actually work

One GET, no auth, the whole board:

```bash
curl "https://careers.jobscore.com/jobs/<company>/feed.atom"
```

Note the path: the feed is under `/jobs/<company>/`, while the human-facing board
is `/careers/<company>` on the same host. `<company>` is the slug either way, and
it's a **path segment on a shared host** rather than a per-tenant subdomain like
most of this pool — which is why JobScore has something almost nobody here does:
a native index. `careers.jobscore.com/sitemaps/careers.xml.gz` lists 504 slugs,
live and sanctioned by robots.txt. On most platforms every customer is an isolated
subdomain and the board list has to be assembled from nothing. Here you can just
ask.

**There is no pagination cap**, which we probed rather than assumed: one board
returned 60 entries in a single document, and every board's Atom count matched its
careers page exactly — 60/60, 30/30, 11/11, 5/5. The feed has no paging controls
because it doesn't need any.

Per entry you get `<title>`, `<content>` with the full body, `<updated>` as an ISO
8601 timestamp with a zone, and a `rel="alternate"` link to the posting.

## What bites you

**Categories are scheme-keyed, not order-keyed.** Location and department both
arrive as `<category>` elements, distinguished only by their `scheme` attribute:

```xml
<category scheme="…#department" term="Clinical"/>
<category scheme="…#location" term="Remote in Joinville, Santa Catarina, Brazil"/>
```

Boards emit several categories and there is no fixed order, so reading "the first
category" or "the second" gets you a department in your location field on some
boards and not others. Match the scheme.

**An empty feed is a live board, and this one cost us.** A JobScore tenant with
zero openings serves a **valid, empty feed at `200`**. Only a `404` means the
board is gone. Requiring at least one `<entry>` as proof of life scored the
platform at 5% live and discarded real boards that simply weren't hiring that
week — a healthy employer between roles is not a dead tenant, and treating it as
one deletes it permanently.

**The id is buried in a tag URI.** Entries identify themselves as
`tag:jobscore.com,2026-04-22:/dFF1n7ML1lS7CdX9T3vco-`. The job id is the trailing
path segment, and unlike most of this pool it's globally unique rather than
tenant-local, so it doesn't need scoping to dedupe.

## Or use ours

JobScore is one of the few platforms here you could reasonably do by hand: one
GET, no auth, no pagination, and a sanctioned sitemap so even the board list is
just a download. If that's your whole requirement, take the feed.

`?source=jobscore` is served, and what it adds is the normalisation and the
judgement calls. `j:publicCompensation` lands as the same
[`salary`](/docs/objects/job) object with `annual_min` and `annual_max` that every
other source normalises onto, so JobScore's typed band is directly comparable to a
number mined out of prose elsewhere — and [salary data](/docs/guides/salary) is
honest about where that comparison strains. Categories are read by their `scheme`
rather than their position. An empty feed is treated as a live board between
roles, not a dead tenant, which is the mistake that once scored this platform at
5% live. Ids come off the tag URI, already globally unique.

One caveat that outlives all of this:
[`/v1/meta`](/docs/api-reference/meta) → `sources` is the authority on what you can
filter on. `source` is a csv filter, not an enum, so a miscased `?source=JobScore`
returns an empty array rather than a `400` — indistinguishable from a board that
simply isn't hiring.
