{
  "markdown": "# ATS job feed recipes\n\n[![AllMCPs Verified](https://allmcps.com/api/badge/career-site-job-feed?style=shield)](https://allmcps.com/mcp/career-site-job-feed)\n\nWorking code for pulling job postings straight out of company applicant tracking systems —\nGreenhouse, Lever, Workday, Ashby, Workable, SmartRecruiters, Breezy, Personio, Recruitee,\nBambooHR, Pinpoint and Rippling — and doing something useful with them.\n\n**[Which ATS API gives you what](https://hwangbyeongseon.github.io/ats-job-feed-recipes/ats-api-fields.html)** — salary, posting dates and\nremote flags across all twelve platforms, measured across 1,549,353 live postings. Nothing else\npublishes these numbers, so start there if you are choosing which platform to build on.\n\nEvery script here runs as-is. Set `APIFY_TOKEN` and go.\n\n```bash\nexport APIFY_TOKEN=...        # https://console.apify.com/settings/integrations\npython python/new_jobs_to_csv.py\n```\n\n## Why this exists\n\nEach of these platforms publishes an open JSON endpoint that the company's own careers page reads —\nno key, no login, no scraping of rendered HTML. The awkward parts are everything around that:\n\n- **Every platform has a different shape.** Greenhouse nests offices and departments; Workday sends\n  a POST and reports posting age as the string `\"Posted 30+ Days Ago\"`; SmartRecruiters caps a\n  response at 100 rows whatever `limit` you pass; Breezy answers `403` for a board that does not\n  exist, where everyone else uses `404`.\n- **Finding out who is on which platform is the actual work.** There is no public directory of\n  Greenhouse or Personio boards. The slug is in a careers URL somewhere, if you can find the\n  careers URL.\n- **Dates are unreliable.** Three of these ten platforms publish no posting date at all, so any\n  \"posted in the last N days\" filter has to decide what to do with rows whose age is unknown.\n\nThe scripts below use [Apify Actors](https://apify.com/starbright_overlap) that have already dealt\nwith all of that and return one row shape across every platform. You can equally point them at the\nraw endpoints — the [notes](#raw-endpoints) at the bottom list them.\n\n## Recipes\n\n| File | What it does |\n|---|---|\n| [`javascript/watch-companies.mjs`](javascript/watch-companies.mjs) | Diff a shortlist between runs — what opened, what closed |\n| [`javascript/salary-data.mjs`](javascript/salary-data.mjs) | Postings that publish a pay range, normalised to an annual figure |\n| [`python/one_company.py`](python/one_company.py) | Every open role at one company, from its careers URL |\n| [`python/new_jobs_to_csv.py`](python/new_jobs_to_csv.py) | Only postings that appeared since the last run, appended to CSV |\n\nThese Actors bill per row delivered. Every script caps itself at a couple of hundred rows and\nreads `MAX_ROWS` if you want more, so running an example does not produce a surprise.\n\n## The row shape\n\nEvery recipe gets the same fields, whichever platform the job came from:\n\n```json\n{\n  \"provider\": \"greenhouse\",\n  \"company\": \"Databricks\",\n  \"companySlug\": \"databricks\",\n  \"jobId\": \"7845321\",\n  \"title\": \"Staff Software Engineer\",\n  \"location\": \"San Francisco, CA\",\n  \"department\": \"Engineering\",\n  \"employmentType\": \"Full-time\",\n  \"remote\": false,\n  \"postedAt\": \"2026-08-14T09:12:00.000Z\",\n  \"applyUrl\": \"https://boards.greenhouse.io/databricks/jobs/7845321\",\n  \"salary\": null,\n  \"descriptionText\": \"...\",\n  \"scrapedAt\": \"2026-08-19T11:02:41.883Z\"\n}\n```\n\n`companySlug:jobId` is stable while a posting is open, which is what makes the diffing recipes work.\n\n## What each platform actually publishes\n\nMeasured from live boards, not from vendor documentation. Useful before you build on any one of them:\n\n*The same table, with the reasoning behind each column, is on the\n[full reference page](https://hwangbyeongseon.github.io/ats-job-feed-recipes/ats-api-fields.html).*\n\n| Platform | Posting date | Description | Pay | Marked remote |\n|---|---|---|---|---|\n| Workday | 78.2% | no | no | no |\n| SmartRecruiters | yes | no | no | 7.7% |\n| Greenhouse | yes | yes | no | no |\n| Workable | yes | yes | no | 36.3% |\n| BambooHR | no | no | no | no |\n| Lever | yes | 94.7% | no | 4.8% |\n| Breezy HR | yes | no | 45.7% | 15.7% |\n| Personio | no | no | no | 8.8% |\n| Ashby | yes | yes | 38.9% | 54.2% |\n| Recruitee | yes | yes | 26.9% | 9.9% |\n| Rippling | no | no | no | 17.4% |\n| Pinpoint | no | yes | 42.5% | 8.8% |\n\n`title`, `company`, `location` and `applyUrl` come back on every row from every platform.\n\n**These are measured numbers, not a feature matrix.** Each row is every live board this project\ntracks for that platform — between 17,000 and 179,000 postings per platform, measured 2026-08-20.\n`yes` means 99.5% or more; `no` means the endpoint carries no such field at all.\n\nThree things the table is telling you:\n\n- **Four platforms publish pay, and none of them fills it most of the time.** Breezy and Pinpoint\n  come closest at 45.7% and 42.5%. Breezy and Ashby give you prose (\"$28 – $100 / hour\",\n  \"$211.4K – $290.6K • Offers Equity\"); Pinpoint and Recruitee give you separate minimum, maximum,\n  currency and period. If you are benchmarking, use the latter two.\n- **Half the platforms have no posting date.** Personio, Rippling, Pinpoint and BambooHR publish\n  none, and Workday dates only 78.2% of rows because it ships prose like \"Posted 30+ Days Ago\"\n  rather than a timestamp. Any freshness filter has to decide whether to drop undated rows or keep\n  them, and the honest default is to keep them.\n- **\"Remote\" is a real field on five platforms and a guess on the rest.** Ashby marks 54.2% of its\n  postings remote, which says more about who uses Ashby than about the job market. Where the\n  platform has no such field, the only option is matching the location string, which misses\n  \"Anywhere\" and misfires on \"Remote Support Engineer, London office\".\n\n## Raw endpoints\n\nIf you would rather call the platforms directly, these are the public endpoints. All of them answer\nwithout a key. Replace `{company}` with the board slug.\n\n| Platform | Endpoint |\n|---|---|\n| Greenhouse | `https://boards-api.greenhouse.io/v1/boards/{company}/jobs?content=true` |\n| Lever | `https://api.lever.co/v0/postings/{company}?mode=json` |\n| Ashby | `https://api.ashbyhq.com/posting-api/job-board/{company}?includeCompensation=true` |\n| SmartRecruiters | `https://api.smartrecruiters.com/v1/companies/{company}/postings?limit=100&offset=0` |\n| Workable | `https://apply.workable.com/api/v1/widget/accounts/{company}?details=true` |\n| Breezy | `https://{company}.breezy.hr/json` |\n| Personio | `https://{company}.jobs.personio.de/search.json` |\n| Pinpoint | `https://{company}.pinpointhq.com/postings.json` |\n| Recruitee | `https://{company}.recruitee.com/api/offers/` |\n| BambooHR | `https://{company}.bamboohr.com/careers/list` |\n| Rippling | `https://api.rippling.com/platform/api/ats/v1/board/{company}/jobs` |\n| Workday | `POST https://{tenant}.{cluster}.myworkdayjobs.com/wday/cxs/{tenant}/{site}/jobs` |\n\nFour things that will bite you if you write your own client:\n\n- **SmartRecruiters** returns at most 100 postings per response regardless of `limit`. Page with\n  `offset`, and take the real total from `totalFound` rather than counting rows.\n- **Recruitee** keeps the salary object present with zeros when the employer skipped the field, so\n  a naive reader ships `\"0 USD hour\"` as a pay range. Treat `min <= 0 && max <= 0` as absent.\n- **BambooHR** splits its careers API in two. `/careers/list` has no posting date, description or\n  compensation; those live on `/careers/{id}/detail`, one request per posting. And its `isRemote`\n  field exists but is never populated — across 50,616 postings measured, not one had it set.\n- **Breezy** returns `403`, not `404`, for a subdomain with no board on it. Treating that as rate\n  limiting and backing off turns a half-hour job into a fourteen-hour one.\n\n## Licence\n\nMIT. The Actors these scripts call are commercial and priced per row; the code here is not.\n",
  "bytes": 7973,
  "sha": "da10d4512073bff590d334d9e7fb507e3270d0b24d751159503b7a374970cb49",
  "repo_slug": "hwangbyeongseon/ats-job-feed-recipes",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_hwangbyeongseon_career_site_jo_ef9b7fcc/readme"
}