If you've searched "greenhouse jobs scraper date filter not working" or "greenhouse API jobs posted last 24 hours", here's the short version: Greenhouse's public job-board API has no date parameter, and neither do Lever's or Ashby's public endpoints. Any date filter you've used was applied after the jobs were downloaded. When it misbehaves, it's that layer, not the job board.
The endpoints are free and public, and getting "new jobs since yesterday" right takes three small habits.
The free endpoints
You need the company's board token, which is usually the slug in its careers URL.
| ATS | Endpoint |
|---|---|
| Greenhouse | https://boards-api.greenhouse.io/v1/boards/{token}/jobs?content=true |
| Lever |
https://api.lever.co/v0/postings/{token}?mode=json (EU companies: api.eu.lever.co) |
| Ashby | https://api.ashbyhq.com/posting-api/job-board/{token}?includeCompensation=true |
import requests
jobs = requests.get("https://boards-api.greenhouse.io/v1/boards/airbnb/jobs").json()["jobs"]
print(len(jobs), jobs[0]["title"], jobs[0]["first_published"])
Habit 1: filter on the publish date, not the update date
Each Greenhouse job carries both first_published and updated_at. updated_at changes every time the employer edits the post, so "updated in the last 24 hours" is not "new in the last 24 hours". Filter on first_published:
from datetime import datetime, timedelta, timezone
cutoff = datetime.now(timezone.utc) - timedelta(days=1)
new_today = [j for j in jobs if j.get("first_published")
and datetime.fromisoformat(j["first_published"]) > cutoff]
The equivalents are Lever's createdAt (milliseconds) and Ashby's publishedAt.
Habit 2: for alerts, diff job IDs
Dates can be missing, and a reposted job gets a fresh date. For a daily alert, the most reliable test is "which job IDs weren't there yesterday?":
new_ids = {j["id"] for j in jobs} - set(yesterdays_ids)
closed_ids = set(yesterdays_ids) - {j["id"] for j in jobs}
Habit 3: never accept a silent zero
When a company comes back with 0 jobs, it's usually one of these:
-
Wrong token.
stripe.comisn't a token. Most ATSs 404 on a bad token, but some return HTTP 200 with an empty list, which looks exactly like "not hiring". -
Wrong ATS. The company switched systems. Look for
boards.greenhouse.io,jobs.lever.coorjobs.ashbyhq.comlinks on its careers page. - Unsupported system. Workday, iCIMS and in-house careers pages don't use these endpoints.
- Over-tight filters. An exact title like "Head Marketing" misses "Head of Marketing". Match keywords with word boundaries instead.
Log a reason for every company that returned nothing, and record the board's total next to what you kept. That way "0 matched my filters" and "0 because the board wasn't found" never look the same.
Duplicates
The same role posted in three cities is often three job IDs. Decide whether you want one row per posting or per role. If per role, key on (company, normalised title, location). Across days, dedupe on job ID.
The one-call version (disclosure: we built this)
We packaged all of the above as a pay-per-use Apify Actor: Greenhouse, Lever & Ashby Jobs API.
-
Domains in, jobs out. Give it
ramp.com, a careers URL orats:token. It finds the board across Greenhouse, Lever, Ashby, Workable, SmartRecruiters, Recruitee and Workday, and returns one schema: title, department, location,isRemote,postedAt, posted salary (only when the employer publishes it), job URL and apply URL. It reads each board live on every run. -
Filters applied before you're charged:
titleKeywords,excludeTitleKeywords,locations,remoteOnly,departmentsandpostedWithinDays. Jobs that don't match are never returned or charged. -
Proof you got the whole board. Every job row carries
companyTotalJobs, the board's open-job count before filters and limits. Each run also writes aCOMPANY-STATUSCSV with one line per input: status, total, returned, filtered out, and a note. -
No silent zeros. Companies it can't find get a
not_foundrow with a reason, and they're free. A board where nothing matched getsno_matching_jobsand its total. -
New since yesterday. Turn on
monitorModeand schedule it daily forchangeType: "new"and"removed"rows, diffed by job ID.
from apify_client import ApifyClient
client = ApifyClient("<APIFY_TOKEN>")
run = client.actor("yourevenings/careers-jobs-api").call(run_input={
"companies": ["stripe.com", "ramp.com", "https://jobs.lever.co/spotify"],
"titleKeywords": ["engineer"],
"locations": ["London"],
"postedWithinDays": 1,
})
for row in client.dataset(run["defaultDatasetId"]).iterate_items():
print(row["recordType"], row["company"], row.get("title"), row.get("companyTotalJobs"), row.get("note"))
In a test run this morning with an engineer, London, last-30-days filter, one board returned 1 matching job out of 726 open roles, with the other 725 filtered out and not charged. A second board reported no_matching_jobs with its total of 170, and a made-up domain came back not_found at no charge.
It costs $0.002 per job returned and $0.002 per company resolved. Companies it can't find are free. Ready-made inputs: Which ATS does a company use? · Greenhouse board export · Posted salary ranges.
All four public endpoints (Greenhouse, Lever, Ashby and Workday), with their date fields and gotchas, are on one reference page: Greenhouse, Lever, Ashby and Workday jobs API.
Notes
-
Not covered yet: iCIMS, SuccessFactors and in-house careers sites. Those return
not_found, and you're not charged. (Workday was added on 22 Oct 2026. Its posted dates are approximate, because Workday's listing only says "Posted 3 Days Ago".) - Jobs without a published date are kept by default when you set
postedWithinDays, flaggedpostedAtUnknown: true. SetkeepJobsWithoutPostedDate: falseto drop them. - The tool drops recruiter contact details and strips emails and phone numbers from descriptions. Using job data is your responsibility under the terms and laws that apply to you.
Which ATS do your target companies use? Tell us in the comments.













