Disclosure: we built the Apify Actor mentioned near the end, and it's a paid tool. Everything before that section works with plain HTTP calls and no account. This post was drafted with AI assistance and reviewed by a person before publishing.
Job posts are some of the most honest public data a company publishes. A posting for an "Analytics Engineer (dbt, Snowflake)" tells you what's in that company's data stack. A line like "help us migrate off our legacy CRM" tells you they may be shopping. If you sell a tool, consult on one, or just track a market, that's useful intent data.
The nice part is that many companies host their careers page on an applicant tracking system (ATS) that exposes a documented, public job-board API. No scraping HTML, no login.
Where the data lives
| ATS | Endpoint | Docs |
|---|---|---|
| Greenhouse | GET https://boards-api.greenhouse.io/v1/boards/{token}/jobs?content=true | Greenhouse Job Board API |
| Lever | GET https://api.lever.co/v0/postings/{site}?mode=json | Lever Postings API |
| Ashby | GET https://api.ashbyhq.com/posting-api/job-board/{board} | Ashby Job Postings API |
The token is usually the last part of the careers URL: job-boards.greenhouse.io/duolingo, jobs.lever.co/spotify, jobs.ashbyhq.com/linear. If you only know a company name, try the obvious slugs (examplecorp, example-corp) and treat a 404 as "not on this ATS."
Fetch and match in about 40 lines
import re, time, requests def greenhouse(token): r = requests.get(f"https://boards-api.greenhouse.io/v1/boards/{token}/jobs", params={"content": "true"}, timeout=30) r.raise_for_status() for j in r.json()["jobs"]: yield {"title": j["title"], "url": j["absolute_url"], "text": j.get("content", "")} def lever(token): r = requests.get(f"https://api.lever.co/v0/postings/{token}", params={"mode": "json"}, timeout=30) r.raise_for_status() for j in r.json(): yield {"title": j["text"], "url": j["hostedUrl"], "text": j.get("descriptionPlain", "")} def ashby(token): r = requests.get(f"https://api.ashbyhq.com/posting-api/job-board/{token}", timeout=30) r.raise_for_status() for j in r.json()["jobs"]: yield {"title": j["title"], "url": j["jobUrl"], "text": j.get("descriptionPlain", "")} FETCHERS = {"greenhouse": greenhouse, "lever": lever, "ashby": ashby} def find_mentions(boards, tool): pattern = re.compile(rf"\b{re.escape(tool)}\b", re.IGNORECASE) for ats, token in boards: for job in FETCHERS[ats](token): if pattern.search(job["title"] + " " + job["text"]): yield token, job["title"], job["url"] time.sleep(1) # be polite: about 1 request per second per host if __name__ == "__main__": boards = [("greenhouse", "duolingo"), ("lever", "spotify"), ("ashby", "linear")] for hit in find_mentions(boards, "snowflake"): print(*hit, sep=" | ") When I ran this on Nov 10, 2026, it printed two matches for "snowflake": a Duolingo data science internship and a Linear analytics engineer role. Your output will differ, because job boards change daily.
Why the naive version lies to you
A word-boundary regex is a start, but real posts break it fast. These are made-up examples of the failure types:
- "Go above and beyond for customers" matches the language Go.
- "Our winter party has a snowflake theme" matches Snowflake.
- "No Salesforce experience needed" is the opposite of a signal.
- Every post from one company may share the same "About us" paragraph that mentions a tool, so one sentence turns into 200 "signals."
What helped us:
- Context words. For ambiguous names like Go, require a nearby word such as "golang," "backend," or "services."
- Skip boilerplate sections. Benefits, EEO statements, and "About us" text rarely tell you about the stack.
- Negation windows. Drop matches with "no," "not required," or "don't need" just before the tool name.
- Repeated text. If a sentence appears in at least half of a company's posts, it's boilerplate. Ignore it.
- A confidence score. A tool in the job title or the requirements counts more than a tool in a long list of "nice to haves."
- A small labeled test set. Twenty hand-checked posts will catch most regressions.
Run it weekly and only look at changes
The useful question is rarely "who mentions Snowflake?" It's "who started mentioning Snowflake this week?" Store a hash of each matching sentence with a first-seen date, then on the next run output only new or changed matches, plus a per-company count so you can see a trend.
Responsible use
Stick to public job posts. Strip any recruiter emails or phone numbers you come across, and don't build lists of people from this. Respect each API's rate limits (Lever asks for one request per second). If you use the results for outreach, follow the email and privacy laws where you and your prospects are.
If you'd rather not maintain it
We packaged the approach above as an Apify Actor: Job Board Hiring Signals: Greenhouse, Lever, Ashby. You give it board tokens or careers URLs and a list of tools. It returns each matching post with the sentence that matched as proof, a confidence score, and a per-company rollup. On weekly schedules it returns only new or changed signals by default.
In our tests, the prefilled input (4 boards, about 250 posts) finishes in roughly 10 to 12 seconds on Apify. A local run across 50 boards scanned 12,493 posts in about 92 seconds.
Pricing is pay-per-event: $0.003 per signal, $0.001 per job in plain jobs mode, and $0.01 per company rollup (only for companies with at least one current signal). You need a free Apify account to run it. It only covers Greenhouse, Lever, and Ashby, and Lever and Ashby don't publish a company name, so you supply your own label.
Over to you
Which other ATS platforms with public APIs should we look at? And if you've seen false positives that the list above wouldn't catch, I'd love to hear them in the comments.