# How to measure remote hiring share

> The query that answers it, and the ways it still lies to you: two remote fields, sparse classifiers, provenance mix, and the archive tail.

Source: https://hyperjobs.io/blog/measuring-remote-hiring

*HyperJobs Team · 30 June 2026 · 7 min read*

"What share of software jobs are remote" is a two-request question. Both requests
are `limit=1`, because you only want `total`:

```bash
# Numerator: remote software postings in the last 30 days.
curl -G https://api.hyperjobs.io/v1/jobs \
  -H "Authorization: Bearer $HYPERJOBS_KEY" \
  --data-urlencode "taxonomies=Software" \
  --data-urlencode "work_arrangement=Remote" \
  --data-urlencode "time_frame=30d" \
  --data-urlencode "limit=1"

# Denominator: all software postings in the same window.
curl -G https://api.hyperjobs.io/v1/jobs \
  -H "Authorization: Bearer $HYPERJOBS_KEY" \
  --data-urlencode "taxonomies=Software" \
  --data-urlencode "time_frame=30d" \
  --data-urlencode "limit=1"
```

Divide the two `total`s. That is the measurement. This post is not about the
answer — it is about why that answer is probably wrong, and what to do about it.

<Note>
  Revised 2026-07-16 for the v1 launch. The original version opened with "this
  API never returns a 400" and spent half its length on input traps — the AND
  comma, `remote=true` doing nothing, ISO codes matching zero rows. Those
  described a pre-release draft that never shipped: v1 rejects bad input with a
  [`400` naming the parameter](/docs/errors), commas mean OR, and `country=US`
  works. What's left is the interesting half — the mistakes that are about
  *measurement*, not syntax, and that no validator can catch for you.
</Note>

## Trap 1: there are two remote fields

`remote=true` and `work_arrangement=Remote` are not the same query:

| Filter | Reads | Can express Hybrid? |
| --- | --- | --- |
| `?work_arrangement=Remote` | `job.work_arrangement`, the classification | Yes — `Remote`, `Hybrid`, `On-site` |
| `?remote=true` | `job.location.remote`, a boolean on the location object | No |

They are separate signals derived from separate evidence, and they return
different-sized populations — the [enums reference](/docs/reference/enums)
publishes both snapshot counts, and the gap between them is wide. Neither is
wrong. But "remote share" means one of them, and you have to say which.

<Tip>
  Prefer `work_arrangement`. It is the classification, it is the more precise of
  the two, and it is the only one that can express hybrid — which is usually the
  interesting category. Use `remote=true` as well only if you want the stricter
  intersection, and then say that is what you measured.
</Tip>

Casing mistakes at least fail loud now — `work_arrangement=remote` is a `400`
listing the real values (`On-site`, with the hyphen and the lower-case `s`, is
the one people miss). The measurement mistake above produces no error at all,
which is why it outranks every typo.

## Trap 2: your denominator is a classifier's opinion

Broadening the denominator is one request now — commas are
[OR](/docs/guides/filtering), so `taxonomies=Software,Engineering,Technology`
is the union, and its `total` (or [`/v1/jobs/count`](/docs/api-reference/jobs-count))
counts distinct postings with no overlap arithmetic on your side.

What the union cannot fix is what the taxonomy *is*: a classifier output with
[measured coverage](/docs/api-reference/meta) below 100%. A posting the
classifier never tagged is in neither your numerator nor your denominator — and
untagged postings are not a random sample. Widen the family list and your
population changes composition, not just size; your remote share can move with
it, in the direction of a conclusion, without a single error. Pick one
denominator expression, write it down, and never compare shares computed over
different ones.

The one comma that still intersects is `skill` — `skill=python,aws` means both.
If your slice is skill-defined, that is usually what you want ("Rust roles"
really does mean postings tagged `rust`), but say so in the writeup.

## Trap 3: whose location is it?

`country=US`, `country=United States` — both work, and an unknown code is a
`400`. The geographic trap left is not syntax; it is that a payload carries
**two** geographies and a share calculation must pick one.

<Warning title="A country slice measures the posting's location, not the employer's">
  `?country=` filters the job's location; `?company_country=` filters the
  employer's HQ. A US-headquartered company posts roles worldwide, so the two
  answer different questions — and for a *remote* share the gap is the whole
  subject, since a remote role's `location.countries` is often a remote-from
  restriction rather than an office. For "remote hiring in country X" you
  almost certainly want the job's location; `remote_location` on the
  [Job object](/docs/objects/job) is the finer signal when it's present.

  There is still no geo search — no radius, no bounding box — though postings
  carry `location.coords` for client-side distance work.
</Warning>

## Trap 4: the window, and what is in it without one

`/v1/jobs` excludes expired postings on every query. That is the good case. The
trap is what remains: the dataset reaches back years, and old rows survive when
nothing forces them out. A board that quietly went dark leaves postings behind
with no expiry to trigger on — [coverage](/docs/reference/coverage) is explicit
about this. Without a `time_frame`, your denominator is an archive, and archives
have a different remote-work composition than the current market. That is not a
finding you want to accidentally publish.

So always window it:

```bash
--data-urlencode "time_frame=30d"    # ✓ 30 days
--data-urlencode "time_frame=1week"  # ✗ 400 — must be <n><m|h|d>, m = minutes
```

An unparseable window is a `400` rather than a silent all-time query, so the
archive can no longer sneak into your denominator through a typo — only through
omission. (`posted_gte`/`posted_lt` give you fixed windows when you need two
measurements to share exact bounds.)

<Note>
  Numerator and denominator must also come from the **same endpoint**.
  [`/v1/jobs`](/docs/api-reference/jobs-list) and
  [`/v1/jobs/feed`](/docs/api-reference/jobs-feed) serve the same live
  population as of v1, but the feed windows on when we *indexed* a posting and
  `time_frame` windows on when it was *posted* — a ratio across the two windows
  is not a ratio of anything.
</Note>

## Trap 5: provenance mixes two populations

Every posting carries a `source_type` — `ats` for boards the employer authors,
`jobboard` for third-party boards like LinkedIn. The current pool is roughly
616k `ats` to 87k `jobboard`, and the two populations differ in exactly the
ways that bias a remote share: job-board postings skew toward roles employers
chose to advertise widely, and their location fields follow the board's
conventions rather than the employer's.

```bash
--data-urlencode "source_type=ats"   # employer-authored postings only
```

Computing your share on `source_type=ats` — or computing it per class and
reporting both — turns an invisible composition effect into a stated choice.
It's one more filter, and it's the difference between "remote share of
postings" and "remote share of postings, mostly wherever LinkedIn was that
month".

## The trap under all of them: postings are not jobs

Even with every parameter correct, `total` counts **advertised postings**. It
does not count filled roles, headcount, or people.

An employer that files one posting per city contributes N observations for one
role. If that filing style correlates with work arrangement — and there is every
reason to think it might, since on-site roles are the ones with a city to
enumerate — then posting counts are a biased estimator of exactly the thing you
are measuring. `collapse=true` gives you employer × title as the unit instead,
which is usually the honest denominator. [Collapsing](/docs/guides/deduplication)
explains what it costs you.

And remember `work_arrangement` is a classifier output, populated from what the
posting said. A remote role whose posting never says so is not in your numerator.

## A defensible procedure

<Steps>
  <Step title="Fix the population first">
    One `time_frame`, one taxonomy vocabulary, one location expression. Write the
    denominator query before the numerator query.
  </Step>
  <Step title="Test every filter's effect before you trust it">
    Typos fail loud now, but a filter can still be *semantically* inert — add
    them one at a time and watch [`/v1/jobs/count`](/docs/api-reference/jobs-count).
    A filter that doesn't move the count is measuring something you didn't
    mean, and the response will never say so.
  </Step>
  <Step title="Change one thing between numerator and denominator">
    Exactly one: the `work_arrangement` filter. If anything else differs, you are
    not measuring a share.
  </Step>
  <Step title="Say which field you measured">
    "Share of postings classified `work_arrangement=Remote`" is a claim the data
    supports. "Remote hiring" is not — it is an inference layered on top of a
    classifier, a scrape, and a filing convention.
  </Step>
</Steps>

Run the same procedure on two windows and you have a trend rather than a level,
which is usually the more robust thing anyway — the biases above are largely
constant across windows and mostly cancel when you difference them.

The queries are the easy part. Knowing which of the four traps ate your result is
the job.
