All platforms

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.

?source=ycombinatorsource reference

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#

BASH
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"
Illustrative — the shape, not a live response
{
  "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:

FieldFill on ycombinator
description100%
Company resolved to a real employer URL100%
work_arrangement~100%
visa_sponsorship86%
salary80%
Experience72%

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 &amp; last, or &amp;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