Y Combinator Jobs API
Query jobs from Y Combinator's Work at a Startup boards through one API with ?source=ycombinator — plus how the platform's data-page attribute really behaves.
Y Combinator is not an ATS, and "Work at a Startup" is not a company's own
careers page. It's YC's hiring portal for its portfolio: one board per YC
company at workatastartup.com/companies/<slug>, where a founder posts openings
directly.
So a posting here is a third kind of object, and it's worth naming precisely. It wasn't aggregated the way a LinkedIn listing was — the employer wrote this one and manages the applicants. But it isn't the Greenhouse or Ashby board the company runs for itself either. It's a portal posting, and the apply link goes through a YC account rather than the employer's own form.
That narrowness is why it's the best-populated source on this site. A YC company
is a known, funded, findable entity, and the portal names it on every page. We
serve 5,273 ycombinator postings — a number that moves, so read it from
/v1/meta rather than trusting this sentence.
Try it#
curl -G https://api.hyperjobs.io/v1/jobs \
-H "Authorization: Bearer $HYPERJOBS_KEY" \
--data-urlencode "source=ycombinator" \
--data-urlencode "has_salary=true" \
--data-urlencode "limit=2"{
"data": [
{
"id": "ycombinator:99318",
"title": "Software Engineer",
"company": {
"slug": "tessellate",
"name": "Tessellate",
"domain": "tessellate.io",
"website": "https://tessellate.io",
"logo": "https://logo.hyperjobs.io/tessellate.io",
"industry": "Industrials",
"employee_count": 58
},
"location": {
"remote": false,
"countries": ["United States"],
"cities": ["Los Angeles", "El Segundo"],
"regions": null,
"raw": ["Los Angeles, CA, US", "El Segundo, CA, US"]
},
"employment_type": ["FULL_TIME"],
"work_arrangement": "On-site",
"seniority": null,
"experience_level": "2-5",
"salary": {
"min": 120000,
"max": 180000,
"currency": "USD",
"unit": "YEAR",
"annual_min": 120000,
"annual_max": 180000,
"summary": "$120K - $180K • 0.05% - 0.20% equity"
},
"skills": ["angular", "javascript", "react"],
"taxonomies": ["Software"],
"visa_sponsorship": false,
"apply_url": "https://account.ycombinator.com/authenticate?continue=…",
"url": "https://www.workatastartup.com/jobs/99318",
"source": "ycombinator",
"expires_at": null
}
],
"total": 2,
"limit": 2,
"offset": 0
}Two things in that payload are the whole story. salary.summary carries the
equity band alongside the cash, because YC asks founders for both. And
apply_url is a YC login, not an employer form — see below.
The fill rates are the reason to care#
Measured across the full sweep on 2026-07-16:
| Field | Fill on ycombinator |
|---|---|
description | 100% |
| Company resolved to a real employer URL | 100% |
work_arrangement | ~100% |
visa_sponsorship | 86% |
salary | 80% |
| Experience | 72% |
80% salary is the highest in the pool
Across all sources, salary is populated on 36.0%
of postings, and has_salary=true throws away roughly two thirds of the
dataset before your other filters apply. On ycombinator it throws away a
fifth. If you're building anything that needs compensation, this source is
denser than everything else here by a factor of two.
The company row is the other unusual one. Every YC job page names the employer's
real website, so every ycombinator posting reaches a company — the only
source here where that's true. A Lever board hands you the slug
northwind and leaves you to work out who that is; a YC page hands you
northwind.io in the same response. See the
company object.
How Work at a Startup actually works#
It's a Rails app running Inertia.js, and that detail is the entire integration.
Every page embeds its full server-side props as JSON in a data-page HTML
attribute. The HTML page is the API. There's no JSON-LD anywhere on the site,
and you don't need it.
Two GETs, both HTML:
# props.company.jobs[] — ids and titles, no bodies
curl "https://www.workatastartup.com/companies/<slug>"<slug> is the company slug from workatastartup.com/companies/<slug> — for
example epsilon3-inc. Decode the attribute's HTML entities, JSON.parse the
result, and you have the props the page rendered from. Decode & last,
or &quot; collapses into a quote and takes your parse with it.
Both of those curls return a body-less 406
Neither command above works as written, and that's the point of showing them. Work at a Startup's load balancer fingerprints TLS. A plain client — curl, undici, a script sending a real Chrome user-agent — gets a 406 with no body, the same wall Polymer puts up. A browser-impersonating client (Chrome and Firefox profiles both pass) goes straight through with no JS challenge.
So there's no cheap rung on this ladder. Impersonation isn't an escalation you
fall back to when the direct fetch starts failing, it's request one. And an
adapter that returns [] when the tier is off doesn't degrade — it hands the
orchestrator an empty live set and expires every job on a healthy board. It has
to throw.
The protocol's own JSON route is walled off#
The trap that costs an afternoon. Inertia has a documented way to ask for bare
JSON instead of HTML: send X-Inertia: true. It works in a browser. Send it from
a script and you get the same body-less 406. Don't reach for the clean JSON
endpoint, scrape the attribute — it carries an identical payload. The route that
looks like the right answer is the one the WAF is watching.
What bites you#
`apply_url` is a YC account wall, not the employer's form
On every ATS source here, apply_url is the employer's real application form.
On ycombinator it's account.ycombinator.com/authenticate?continue=… —
applying means signing in to Work at a Startup. That's the portal's model, not
a scraping artifact, and it's the practical difference between this source and
a company's own board. Check source before you assume a URL is an ATS.
The platform publishes no posting date. Anywhere. Not on the company page,
not on the job page, not in the props. The home feed carries
companyLastActiveAt, which is relative prose ("about 7 hours ago") and is
company-level, not job-level. So posted_at on a ycombinator row cannot be
YC's publish date, because YC doesn't have one to give — the field
falls back rather than
going null. Don't read time_frame=24h on this source as "posted on YC
yesterday". It isn't.
Titles carry the company as a suffix. Software Engineer (Epsilon3). Strip
it only when the parenthesis is the company name — Engineer (Remote) is a
real title and a blind regex eats it.
sponsorsVisa has three values, not two. Will sponsor is true and
US citizen/visa only is false, but US citizenship/visa not required is
neither. It's a statement about international remote work, not a promise to
sponsor anyone. Mapping it to true invents a commitment the employer never made;
it maps to null.
Salary and equity are separate strings, and only one is money. salaryRange
is $120K - $180K; equityRange is 0.25% - 0.75%. Concatenate them before
parsing and the percentages read as salary figures. Only the cash string goes
through a number parser. The equity survives in the display summary, where it
belongs.
There is no work-arrangement field. YC never added one. But its location tags
are ground truth and it labels explicitly: Hybrid, In-person,
Remote (US). Read arrangement off the tags and fill goes from 35% to about 95%.
One judgment call worth stating: SF / Remote (US) is Remote, not Hybrid.
YC tags true hybrid when it means it, so an unlabelled office-or-remote posting
is remote-friendly, and calling it Hybrid invents a constraint nobody stated.
A 404 here is real, unusually. A dead slug returns YC's static "File Not Found" page and genuinely means the board is gone. Polymer's identically-shaped 404 is a disguised rate limit; YC's isn't. Treat any other non-200 as transient anyway.
Or use ours#
The fetch is two GETs. Everything around it isn't.
Boards first: there's no index of YC companies that are hiring, but there is an
open dataset, yc-oss, whose slugs are the same slugs Work at a Startup uses.
Filtered to isHiring, that's 1,497 boards — the work list nobody hands you.
Then the sweep, measured on 2026-07-16: all 1,497 boards in 156 seconds, 1,497 of 1,497 OK, zero blocks and zero 429s at concurrency 20. YC doesn't rate-limit, which makes it the rare source where the constraint is purely the impersonation tier and not a throttle budget. A re-scrape of 150 boards takes 8 seconds and fetches no job pages at all, because the list rows are enough to know which postings are new.
The part you'd care about most is the one you can't see in a response:
15% of YC postings are cross-posted to an ATS we already scrape, and those
fold into one row with the employer's own board
winning as canonical. The other 85% are net-new — roles that exist here and
nowhere else in the dataset. Which also means ?source=ycombinator&limit=1 is
not a count of jobs on Work at a Startup. It's a count of jobs where YC won
dedup, and any breakdown you publish off it is a breakdown of surviving rows.
Query it with ?source=ycombinator.
Quickstart, then pricing.
Every source above normalises onto the same schema, with the hiring company already joined onto each posting.
Get an API key