# Dover Jobs API

> How Dover's cross-tenant job-board feed actually works, why you cannot filter it by client, and what ?source=dover returns in the HyperJobs API.

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

Dover is a recruiting-automation ATS aimed at startups, with boards at
`jobs.dover.com/<slug>`. It has one property that almost nothing else in this
pool has, and it is worth more than its size: **a public cross-tenant feed**.
One unauthenticated call enumerates every client on the platform and every job
they have open.

## The cross-tenant feed, and why it matters

```bash
curl "https://app.dover.com/api/v1/job-board/jobs/?limit=500&offset=0"
```

That is the whole platform: 334 jobs across 167 clients, in one request. A
standard paginated envelope, `{count, next, previous, results}`, and `limit` is
genuinely respected — which is rarer than it should be. Ask for 500 and you get
all 334.

The reason this is notable is the thing almost every other page in this
collection has to explain. Most ATS platforms have **no global index**. Every
customer is an isolated subdomain or slug, nobody publishes a customer list, and
so the hard part of the work is not fetching a board but *knowing the board
exists*. [Lever](/ats/lever) and [Greenhouse](/ats/greenhouse) both make you
assemble that list yourself. Dover just hands it over. Only a handful of
platforms do.

The honest caveat: Dover is small. Around 334 jobs platform-wide, roughly two
per client. Free discovery on a small corpus is still free discovery, but it
won't move volume.

The feed rows have no body. The description is one detail call away:

```bash
curl "https://app.dover.com/api/v1/job-board/jobs/<id>/"   # user_provided_description
```

## What bites you

<Warning title="The feed cannot be filtered by client, and it pretends it can">
  `?client=`, `?client_slug=`, `?slug=`, `?client_id=` and `?company=` **all
  return HTTP 200 with the full 334-job feed**, other clients' jobs included.
  None of them error. None of them warn.

  Trust any one of them and you attribute every job on the platform to a single
  company. The only correct approach is to pull the feed once and group it by
  `client.slug` locally.
</Warning>

**The top-level `workplace_type` is authoritative, and it disagrees with
`locations[]`.** 28 jobs carry location entries marked both REMOTE *and* HYBRID.
The two fields also use different vocabularies for the same idea: the top-level
one says `ONSITE` where `locations[]` says `IN_OFFICE`. Point one shared enum map
at both and it drops whichever spelling it doesn't know.

**`compensation` carries equity bounds too, not only salary.** Read the field
without checking what kind of number it is and you will store an equity range as
a pay band.

**City and state are empty strings on country-level postings.** They aren't null,
they're `""`, so a truthiness check passes and joins them into a location that is
just commas. Fall through to the parts that are actually filled.

**There is no Cloudflare wall, despite appearances.** `jobs.dover.com/<slug>`
returns 200 with an empty React shell. That looks like a block and isn't — it's
just a client-rendered page. The data was always in the API.

## What you get

`date_posted` filled on 334 of 334 rows with a proper zoned timestamp,
descriptions complete on every tenant measured, and `client.name` giving the
employer's real name, which makes Dover one of the stronger platforms for the
[company join](/docs/objects/company). Compensation runs about 35%
platform-wide, so a tenant with none is normal rather than a parsing failure.

Query it with `?source=dover`, and the grouping the raw feed forces on you is
already done: rows arrive attributed to the client they belong to rather than to
the platform. The value is the public slug, and the same string keys
[`/v1/meta`](/docs/api-reference/meta) → `sources` — that endpoint is the live
vocabulary, and worth reading rather than trusting a list on a page.

See [all platforms](/ats) · [coverage](/docs/reference/coverage)
