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.
"What share of software jobs are remote" is a two-request question. Both requests
are limit=1, because you only want total:
# 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 totals. 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.
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, 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.
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 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.
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.
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, so taxonomies=Software,Engineering,Technology
is the union, and its total (or /v1/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 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.
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 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.
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 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:
--data-urlencode "time_frame=30d" # ✓ 30 days
--data-urlencode "time_frame=1week" # ✗ 400 — must be <n><m|h|d>, m = minutesAn 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.)
Numerator and denominator must also come from the same endpoint.
/v1/jobs and
/v1/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.
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.
--data-urlencode "source_type=ats" # employer-authored postings onlyComputing 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
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#
Fix the population first
One
time_frame, one taxonomy vocabulary, one location expression. Write the denominator query before the numerator query.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. A filter that doesn't move the count is measuring something you didn't mean, and the response will never say so.Change one thing between numerator and denominator
Exactly one: the
work_arrangementfilter. If anything else differs, you are not measuring a share.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.
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.
Keep reading
The comma means OR, except for skill
taxonomies=Software,Healthcare is a union now. Why v1 made the comma OR on every multi-value filter but one — and why skill still intersects.
8 min read
The skill you were shown is not the skill you filter on
Skills go out display-formatted and come in as canonical tokens. Why ?skill=Machine Learning returns zero rows, and the heuristic that isn't an inverse.
8 min read
Reading salary data honestly
What the salary fields mean, why coverage is partial by construction, and what salary_gte's range-overlap semantics do and don't promise.
8 min read