How to Find Companies That Just Started Hiring (Greenhouse, Lever & Ashby APIs)
How to Find Companies That Just Started Hiring (Greenhouse, Lever & Ashby APIs)
如何寻找刚开始招聘的公司(利用 Greenhouse、Lever 和 Ashby API)
When a company posts a job, that posting comes with a timestamp, and the timestamp is the interesting part. A new “VP of Sales” role usually means budget just got approved and a decision-maker seat is about to be filled. A first “Data Engineer” opening means someone is standing up a data platform right now. Three “Account Executive” listings in one week means a team is scaling its go-to-market this quarter. 当一家公司发布职位时,该职位会附带一个时间戳,而这个时间戳正是最有趣的部分。一个新的“销售副总裁”职位通常意味着预算刚刚获批,一个决策者岗位即将填补。第一个“数据工程师”职位的出现,意味着有人正在搭建数据平台。一周内发布三个“客户经理”职位,则意味着某个团队本季度正在扩大其市场推广规模。
People call this hiring intent data, and it’s one of the more useful buying signals around because it’s timed. A role that shows up today is a window that’s open today. Six weeks later the tool has been picked and the problem is quietly solved. You don’t need an expensive intent-data subscription to see it. Three of the biggest applicant tracking systems (Greenhouse, Lever, and Ashby) serve their customers’ job boards as public, unauthenticated JSON. If you can make an HTTP request, you can build your own “who’s hiring” feed. This post covers how to pull the data, and then the part people usually get wrong, which is working out what’s actually new. 人们称之为“招聘意向数据”,它是目前最有用的购买信号之一,因为它具有时效性。今天出现的职位是一个今天敞开的窗口。六周后,工具已被选定,问题也已悄然解决。你不需要昂贵的意向数据订阅服务就能看到这些。三大招聘管理系统(Greenhouse、Lever 和 Ashby)都以公开、无需身份验证的 JSON 格式提供其客户的招聘页面。只要你能发起 HTTP 请求,就能构建自己的“谁在招聘”信息流。本文将介绍如何获取这些数据,以及人们通常容易出错的部分:如何判断哪些职位是真正“新”的。
ATS job boards are public JSON APIs
ATS 招聘页面即公开的 JSON API
The careers page on a company’s site is usually just a front end for a JSON feed the ATS serves publicly. No key, no OAuth. You just need the company’s board slug. 公司网站上的招聘页面通常只是 ATS(申请人跟踪系统)公开提供的 JSON 数据源的前端。无需密钥,无需 OAuth。你只需要公司的招聘页面标识符(slug)。
- Greenhouse:
https://boards-api.greenhouse.io/v1/boards/{token}/jobs({token} is the slug, likestripe. Add?content=truefor full descriptions.) - Lever:
https://api.lever.co/v0/postings/{company}?mode=json - Ashby:
https://api.ashbyhq.com/posting-api/job-board/{org}?includeCompensation=true
You can usually find the slug by looking at a company’s “Careers” link (boards.greenhouse.io/acme, jobs.lever.co/acme, jobs.ashbyhq.com/acme).
你通常可以通过查看公司的“招聘(Careers)”链接找到这个标识符(例如 boards.greenhouse.io/acme、jobs.lever.co/acme、jobs.ashbyhq.com/acme)。
Pulling and filtering jobs
获取并筛选职位
Here’s a small Node script (built-in fetch, Node 18+) that hits a Greenhouse board and keeps only the roles matching a keyword list. 这是一个小型 Node 脚本(使用内置 fetch,Node 18+),它会访问 Greenhouse 招聘页面,并仅保留匹配关键词列表的职位。
const BOARD_TOKEN = "stripe";
const KEYWORDS = ["sales", "revops", "account executive"];
async function getJobs(token) {
const url = `https://boards-api.greenhouse.io/v1/boards/${token}/jobs`;
const res = await fetch(url);
if (!res.ok) throw new Error(`Greenhouse ${res.status} for ${token}`);
const { jobs } = await res.json();
return jobs; // { id, title, updated_at, location, absolute_url }
}
function matches(title, keywords) {
const t = title.toLowerCase();
return keywords.some((k) => t.includes(k.toLowerCase()));
}
(async () => {
const jobs = await getJobs(BOARD_TOKEN);
for (const j of jobs.filter((x) => matches(x.title, KEYWORDS))) {
console.log(`- ${j.title} (${j.location?.name ?? "N/A"})`);
console.log(` ${j.absolute_url}`);
}
})();
Lever and Ashby return slightly different shapes. Lever uses text and hostedUrl, and Ashby nests its postings under jobs. So you write one small normalizer per source that maps each into a common { id, title, location, url }. Once that’s done, everything downstream works the same regardless of where the data came from.
Lever 和 Ashby 返回的数据结构略有不同。Lever 使用 text 和 hostedUrl,而 Ashby 将职位嵌套在 jobs 下。因此,你需要为每个来源编写一个小型标准化程序,将它们映射为统一的 { id, title, location, url } 格式。一旦完成,无论数据来自何处,下游的所有处理逻辑都将保持一致。
The hard part is detecting what’s new
难点在于检测“新”职位
This is where most homemade trackers break. These APIs give you a current snapshot, meaning the roles that are open right now. What they don’t give you is a reliable “posted at” field you can trust across all three providers. Greenhouse especially has no dependable creation date on the board endpoint, and updated_at changes every time someone edits a description. So if you want new postings, which is the actual signal, you have to work out the difference yourself.
这是大多数自制追踪器失效的地方。这些 API 提供的是当前快照,即当前开放的职位。它们没有提供一个在三个平台间都可靠的“发布时间”字段。尤其是 Greenhouse,其接口没有可靠的创建日期,且每次有人编辑描述时 updated_at 都会改变。因此,如果你想要获取“新职位”(这才是真正的信号),你必须自己计算差异。
You store what you saw last time and compare it against this run. 你需要存储上次看到的数据,并将其与本次运行的结果进行比较。
seen = load_state(company) # set of job IDs from previous runs
current = fetch_jobs(company) # today's snapshot
new_jobs = [j for j in current if j.id not in seen]
closed_jobs = [id for id in seen if id not in current.ids]
emit(new_jobs) # the hiring signal you want
save_state(company, current.ids)
A few things that matter once you run this for real: 当你真正运行此程序时,有几点至关重要:
- Persist the state somewhere durable: Even if it’s just a set of job IDs per company. Lose it, and the next run treats every open role as new and floods you with false positives. 将状态持久化存储: 即使只是每个公司的职位 ID 集合。如果丢失了状态,下一次运行会将所有开放职位视为新职位,从而产生大量误报。
- Key off the provider’s job ID, not the title: Titles get edited, and a renamed role shouldn’t show up as new. 以提供商的职位 ID 为键,而非标题: 标题会被编辑,重命名的职位不应被视为新职位。
- The first run is a cold start: You have no history yet, so seed the state and expect everything to look new that one time. 首次运行是冷启动: 你还没有历史记录,所以请初始化状态,并预料到第一次运行时所有职位都会被视为新的。
- Normalize before you diff: If you compare raw responses from three different APIs, your logic turns into a pile of special cases. 先标准化再对比: 如果直接对比三个不同 API 的原始响应,你的逻辑会变成一堆特殊情况的集合。
- Watch for roles that disappear, too: A req that drops off usually means it was filled, which is its own useful signal. 同时关注消失的职位: 一个职位下架通常意味着它已被填补,这也是一个有用的信号。
What to do with the signal
如何利用这些信号
Once you have a clean feed of new roles, the uses are fairly obvious. You can watch a list of target accounts and get pinged the moment a relevant role opens. You can rank by seniority, since a “VP” opening in your buyer’s department is a stronger, budget-backed trigger than a backfill. Or you can spot scaling motions, like three new AE reqs in a week from a company that’s clearly investing in sales. 一旦你拥有了干净的新职位信息流,用途就显而易见了。你可以监控目标客户列表,并在相关职位开放时立即收到提醒。你可以按资历排序,因为你目标客户部门的“副总裁”职位空缺,比普通填补空缺的信号更强、更有预算支持。或者,你可以发现扩张动作,比如某家公司在一周内发布了三个新的客户经理(AE)职位,这显然说明他们在加大销售投入。
It holds up at scale too. I recently ran this across ten companies at once (Stripe, OpenAI, GitLab, Anthropic, Databricks, Coinbase, Airbnb, Discord, Ramp, and Notion) and pulled 3,664 normalized job postings across all three ATS platforms in about a second, each tagged as new, updated, or removed against the previous run. The data is right there in public JSON. The real work is the normalizing and the change-detection state. 这种方法在大规模应用时同样有效。我最近同时对十家公司(Stripe、OpenAI、GitLab、Anthropic、Databricks、Coinbase、Airbnb、Discord、Ramp 和 Notion)运行了此程序,在大约一秒钟内从三个 ATS 平台拉取了 3,664 个标准化职位,并根据上次运行的结果将每个职位标记为“新增”、“更新”或“已移除”。数据就在公开的 JSON 中。真正的工作在于标准化和变更检测状态的维护。
If you’d rather not maintain the plumbing
如果你不想维护这些基础设施
All of this is doable in an afternoon. Keeping it running is the annoying part: the change-detection state, the per-provider normalizing, the scheduling, the retries when a board throws a 404. That’s the stuff that eats weekends. If you’d rather skip it, I packaged this same approach as an Apify actor that watches Greenhouse, Lever, and Ashby boards for a list of companies and returns only the new roles run over run: ATS Job Scraper for Greenhouse, Lever & Ashby. You give it your target accounts, turn on monitor mode, and put it on a schedule. Either way, the data is public. 这一切在一个下午内就能完成。但保持其持续运行很麻烦:变更检测状态、针对不同提供商的标准化、调度、以及当招聘页面返回 404 时的重试机制。这些才是占用周末时间的工作。如果你想跳过这些,我将同样的方法封装成了一个 Apify Actor,它可以监控 Greenhouse、Lever 和 Ashby 的招聘页面,并仅返回每次运行中新增的职位:ATS Job Scraper for Greenhouse, Lever & Ashby。你只需输入目标账户,开启监控模式,并设置定时任务即可。无论哪种方式,数据都是公开的。