ATS Job Monitor — New Postings Only (Greenhouse, Lever, Ashby) avatar

ATS Job Monitor — New Postings Only (Greenhouse, Lever, Ashby)

Pricing

from $5.00 / 1,000 new job detecteds

Go to Apify Store
ATS Job Monitor — New Postings Only (Greenhouse, Lever, Ashby)

ATS Job Monitor — New Postings Only (Greenhouse, Lever, Ashby)

Yesterday's job list is noise. Get only the roles that weren't there last run — Greenhouse, Lever and Ashby, with signed webhooks and per-signal pricing.

Pricing

from $5.00 / 1,000 new job detecteds

Rating

0.0

(0)

Developer

Kevin Baldassari

Kevin Baldassari

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

9 days ago

Last modified

Share

Greenhouse, Lever & Ashby Job Scraper — Only New Postings

Yesterday's job list is noise. This Actor watches your target companies' job boards and reports only the roles that weren't there on the last run.

"Only new jobs" is easy to promise and hard to get right. This one is built around the parts that go wrong — and it ships a health check that tells you when something upstream breaks, instead of leaving you to notice your alerts went quiet.

What "reliably" means here, concretely:

  • A board glitch doesn't become fifty fake alerts. If a company's board briefly returns a partial or empty list — a deploy, an ATS migration, a rate limiter that still answers 200 — a naive differ forgets those jobs and re-reports every one of them as new on the next run. This Actor requires a posting to be absent across several consecutive runs before forgetting it.
  • State actually survives between scheduled runs. Tracking data lives in a persistent named store, not in per-run storage that is recreated every time. Get this wrong and the Actor silently re-baselines on every run and reports everything as new, forever.
  • A failed fetch never corrupts the diff. If one company can't be reached, its state is left untouched rather than advanced on data that never arrived.
  • You are never charged for a signal you didn't receive. If a spend limit is reached mid-run, the undelivered postings are withheld from the state too, so the next run reports them again instead of losing them.
  • You can predict the bill before you buy. Real measured posting volumes are published below, not left for you to discover on your first invoice.
  • You can tell when it stops working. Job board APIs change without warning. A built-in health check verifies all three providers still return the fields this Actor depends on, and fails loudly if not — schedule it and you find out before your alerts silently go empty.
  • Webhooks can be verified. Every POST can be signed, so your endpoint can prove it came from this Actor and hasn't been replayed.

What it does

  • Tracks any company across Greenhouse, Lever and Ashby job boards — tell it the ATS or let it detect it automatically.
  • Detects only what's new. It remembers every job it has already seen, so a run over unchanged data returns zero new postings instead of the same list again.
  • Shows you signal from run one. A 7-day lookback means your first run already surfaces the roles posted this week, rather than an empty table.
  • Delivers the signal to a dataset row and, optionally, a webhook POST fired once per new matching job.

Who it's for

  • Sales / BD teams — get pinged when a target account posts a role that signals a buying trigger (a "DevOps Engineer" opening at a prospect means infrastructure budget).
  • Recruiters / talent teams — track when competitors open roles in specific departments, and benchmark their hiring velocity.
  • Investors / market researchers — monitor portfolio or watchlist companies' hiring as a growth or slowdown proxy.

⚠️ Read this first: how the first run works

The Actor reports what's new since it started tracking a company. On the very first run there is nothing to compare against, so it records everything currently open as the baseline. What it reports on that first run is your choice:

firstRunLookbackDaysFirst run reportsCost of that run
7 (prefilled)roles the ATS says were posted in the last 7 dayssmall — a few dozen roles
0nothing; pure baselinefree
— with alertOnFirstRunevery open role, however oldexpensive — 500+ roles at a big company

The default input uses a 7-day lookback, so your very first run already shows real hiring signals instead of an empty table. Set it to 0 if you want a strictly free baseline and are happy to wait for the next run.

Either way, from the second run onward you only ever get genuinely new postings. The lookback applies to a company's first run only.

Things worth knowing:

  1. You must put the Actor on a schedule. A single run gives you a snapshot. The product is the ongoing feed.
  2. Every company gets its own first run. Add a company to your list six months from now and the same logic applies to it alone — your existing companies are unaffected.
  3. Avoid alertOnFirstRun unless you want a full snapshot. It charges for every open role, including ones posted years ago. The lookback exists to give you the useful part at a fraction of the cost.

How it works

  1. Add your companies. Each needs its ATS slug — the company identifier in its job board URL. Set ats, or leave it as auto to detect it.
  2. Put it on a schedule. Save your input as a Task, then Task → Schedules → Create schedule. Daily is a good default; hourly if you're in a competitive sales cycle. The Actor does not schedule itself.
  3. Get notified only on new matches. New roles appear in the dataset with isNewThisRun: true, and each fires a webhook if you set a webhookUrl.

Finding a company's slug

It's the identifier in the job board URL:

Board URLSlugATS
boards.greenhouse.io/stripestripegreenhouse
jobs.lever.co/palantirpalantirlever
jobs.ashbyhq.com/ramprampashby

Many companies host the board on their own domain (stripe.com/jobs). The slug is usually still the company name in lowercase — set ats: "auto" and let the Actor find it. If a slug is wrong, that company is skipped with a log message and the rest of your list still runs.


Input

{
"companies": [
{ "name": "Stripe", "slug": "stripe", "ats": "greenhouse" },
{ "name": "Palantir", "slug": "palantir", "ats": "lever" },
{ "name": "Ramp", "slug": "ramp", "ats": "ashby" },
{ "name": "Vanta", "slug": "vanta", "ats": "auto" }
],
"keywords": ["engineer", "devops", "sales"],
"locations": ["Berlin", "Remote"],
"firstRunLookbackDays": 7,
"webhookUrl": "https://hooks.example.com/new-job"
}

Only companies is required.

FieldTypeDefaultWhat it does
companiesarrayCompanies to track. Each needs slug; name and ats are optional.
keywordsstring[]noneAlert only on job titles containing one of these.
departmentsstring[]noneAlert only on these departments.
locationsstring[]noneAlert only on these locations.
webhookUrlstringnonePOST once per new matching job.
webhookSecretstring (secret)noneSigns every webhook so your endpoint can verify it. See below.
healthCheckbooleanfalseIgnore the company list and check the three ATS APIs for breakage instead. Free.
firstRunLookbackDaysinteger0 (form prefills 7)On a company's first run only, also report roles the ATS says were posted within this many days. Max 90.
alertOnFirstRunbooleanfalseReport every open role on a company's first run, however old. Expensive — see above. Overrides the lookback.
onlyPushNewJobsbooleanfalsePush only new jobs to the dataset instead of all open roles.
resetStatebooleanfalseClear tracking state and re-baseline.
stateStoreNamestringhiring-signal-stateNamed key-value store holding the "already seen" state.
maxConcurrencyinteger5Companies fetched in parallel.
requestTimeoutSecsinteger60Timeout for one ATS request.
maxRequestRetriesinteger3Retries before giving up on a company.

How filters behave

All three filters are case-insensitive substring matches. A job must satisfy every filter you set, and any one term within a filter. So keywords: ["engineer","designer"] + locations: ["berlin"] means (engineer OR designer) AND berlin.

Filters control alerts and billing, not the dataset. By default every open role still lands in the dataset so you keep the full picture; jobs that are new but filtered out are flagged isNewThisRun: true and cost you nothing. Set onlyPushNewJobs: true if you want a leaner dataset.


Output

One dataset row per job:

{
"company": "Ramp",
"ats": "ashby",
"jobId": "34413f8d-26bf-4bbc-8ade-eb309a0e2245",
"title": "Security Engineer, Cloud",
"department": "Engineering",
"location": "New York, NY (HQ)",
"remote": true,
"employmentType": "Full-time",
"salary": "$211.4K – $290.6K • Offers Equity",
"url": "https://jobs.ashbyhq.com/ramp/34413f8d-26bf-4bbc-8ade-eb309a0e2245",
"publishedAt": "2026-08-04T14:22:10.000Z",
"firstSeenAt": "2026-08-05T09:00:00.000Z",
"isNewThisRun": true,
"matchedKeywords": ["engineer"]
}

Two dates, and the difference matters:

  • publishedAt — when the company published the role, straight from the ATS. Use this to judge how fresh a posting is.
  • firstSeenAt — when this Actor first saw it. On an ongoing schedule the two are close; on a first run publishedAt can be much older.

matchedKeywords is null when no keyword filter is set.

The dataset has two ready-made views in the Console: New jobs this run and All tracked jobs.

What each ATS actually publishes

Optional fields are null when the source board doesn't provide them — this varies by platform, and it's worth knowing before you build a filter around one:

FieldGreenhouseLeverAshby
department
location
publishedAt
remoteinferred from location text✅ from workplace type✅ real flag
employmentTypesometimes
salaryrarelyrarely✅ when disclosed

remote on Greenhouse is a best-effort read of the location string, so a remote-friendly role advertised as "New York" reads as false.

Webhook payload

One POST per new matching job — never batched:

{
"event": "new_job_posting",
"company": "Ramp",
"title": "Security Engineer, Cloud",
"url": "https://jobs.ashbyhq.com/ramp/34413f8d-26bf-4bbc-8ade-eb309a0e2245",
"detectedAt": "2026-08-05T09:00:00.000Z"
}

A webhook endpoint that is down or returns an error is retried, then logged and skipped — it never fails the run or costs you the rest of your signals.

Verifying that a webhook really came from us

Set webhookSecret and every POST carries two extra headers:

x-hiring-signal-timestamp: 1786041600
x-hiring-signal-signature: sha256=9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08

The signature is HMAC-SHA256 of <timestamp>.<raw request body>, hex-encoded. The timestamp is signed together with the body, so a captured request can't be replayed later.

Node.js

import { createHmac, timingSafeEqual } from 'node:crypto';
function isValid(rawBody, headers, secret) {
const timestamp = headers['x-hiring-signal-timestamp'];
const received = headers['x-hiring-signal-signature'];
// Reject anything older than 5 minutes.
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
const expected = 'sha256=' +
createHmac('sha256', secret).update(`${timestamp}.${rawBody}`).digest('hex');
const a = Buffer.from(expected);
const b = Buffer.from(received ?? '');
return a.length === b.length && timingSafeEqual(a, b);
}

Python

import hmac, hashlib, time
def is_valid(raw_body: bytes, headers, secret: str) -> bool:
timestamp = headers.get("x-hiring-signal-timestamp", "")
received = headers.get("x-hiring-signal-signature", "")
if abs(time.time() - float(timestamp)) > 300:
return False
expected = "sha256=" + hmac.new(
secret.encode(), f"{timestamp}.".encode() + raw_body, hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, received)

Verify against the raw body bytes, before any JSON parsing — re-serialising changes the bytes and the signature won't match.


Knowing when it breaks

Public job board APIs change without notice. When that happens the usual symptom isn't an error, it's silence: alerts quietly stop arriving and you assume nobody is hiring.

Run the Actor with healthCheck: true and it ignores your company list and checks the providers themselves:

  • can a known-good board still be reached on each of Greenhouse, Lever and Ashby?
  • does every job still carry an id, title and URL?
  • are ids still unique?
  • are publishedAt, location and department still being populated?
  • does a non-existent company still return a clean 404?

It writes one row per provider and fails the run if anything critical broke, so Apify's run notifications reach you. It touches no tracking state and is never charged.

Schedule it daily alongside your real runs if you depend on this Actor for anything that matters.


Pricing

$0.02 per new job detected and delivered. Not per run, not per API call, not per dataset row.

  • Only new jobs that pass your filters are charged. Filters lower your bill.
  • Rows that aren't new are free. A run where nothing changed costs you nothing.
  • A pure baseline run is free — with firstRunLookbackDays: 0, a company's first run reports nothing and charges nothing.
  • With the prefilled 7-day lookback, a first run costs a little because it reports real recent postings. Measured on the exact prefilled input (Dropbox, Palantir, Linear + the engineer/sales keyword filter): 364 open roles scanned, 13 posted in the last 7 days, 4 matched the filter — so 4 events, $0.08. Remove the keyword filter and the same run charges 13 events, $0.26.
  • You can cap total spend per run in the Actor's settings.

What that actually costs you

Most pay-per-event pricing leaves you guessing at the bill, so here is real data. Measured across 19 live boards in August 2026, a median company publishes 43 new roles per 30 days (quartiles: 29 / 43 / 111). Small companies post 2–5 a month; the largest post 180–260.

Your setupAlerts / monthCost / month
10 companies, no filters~430~$9
20 companies, no filters~860~$17
20 companies, with filters~250~$5
50 companies, no filters~2,150~$43

If you track large, fast-hiring companies, use filters. That's what they're for, and it's the difference between $43 and $5.

One-off first-run cost. With the 7-day lookback, adding companies costs roughly a quarter of the monthly figures above, once — and your filters apply to it just as they do to everything else. Ten companies unfiltered ≈ 110 recent roles ≈ $2.20, and you see a week of hiring activity immediately. Compare that with alertOnFirstRun, which for the same ten companies would report ~1,700 mostly stale roles at ~$34.


Things worth knowing

Put each independent watchlist in its own state store. Two schedules tracking the same company with the same stateStoreName share one baseline — whichever runs first consumes the signal, and the other sees nothing new. If you want a daily sales alert and a weekly research report over overlapping companies, give one of them a different stateStoreName.

A job that briefly disappears and comes back is not re-alerted. Boards glitch, and companies migrate mid-quarter. A posting must be missing from several consecutive runs before it's forgotten — otherwise a blip would arrive as a burst of false "new job" alerts, on your bill.

A genuinely re-opened role does count as new. If a company closes a role and re-posts it weeks later, that's a fresh hiring signal and you'll be told.

A broken slug doesn't break the batch. It's logged and skipped, and every other company still runs. Check the run log or the RUN_SUMMARY record for the reason.

A company with no open roles is fine. It's tracked normally and reports zero jobs.

Some companies run boards on two ATS platforms. ClickHouse, for example, serves the same roles on both Greenhouse and Ashby. With auto, the platform that actually has open roles wins. Set ats explicitly to force one.

resetState: true and changing stateStoreName both start a fresh baseline — free, but you lose the tracking history and that run reports nothing.


Limits

  • Greenhouse, Lever and Ashby only. No Workday, SmartRecruiters or LinkedIn.
  • No historical backfill: it reports what's new since you started tracking, not since the company was founded.
  • Webhook delivery only — no email.
  • Public job boards only. Nothing private, nothing behind a login.

FAQ

Why did my first run report nothing? You ran it with firstRunLookbackDays: 0. That's the strict setting: the first run for a company only records a baseline, so you aren't alerted (or charged) for roles that have been open for months. Set the lookback to 7 to see the last week's postings immediately, or just run it again tomorrow.

Is the lookback the same as historical backfill? No. It reaches back a bounded number of days on a company's first run only, so you're not starting from an empty screen. It will not reconstruct a company's entire posting history, and it never applies to later runs.

Why do publishedAt and firstSeenAt differ? publishedAt is the company's own date for the role; firstSeenAt is when this Actor first saw it. On a first run with a lookback they can be days apart — that's expected. On an ongoing daily schedule they'll usually be within a day of each other.

Why not just call the free ATS APIs myself? You can — for one company, once. What this removes is the rest: normalizing three different JSON schemas into one row shape, keeping per-company state across scheduled runs so you know what changed, and the edge cases you only find in production. Boards that answer 200 with an empty array. Companies that migrate ATS mid-quarter and leave a stale empty board behind. Companies that run two ATS at once. Titles with stray whitespace that break naive deduplication. Transient upstream blips that a naive differ reports as fifty brand-new jobs.

How is this different from other ATS scrapers on the Store? Several now advertise a diff or delta mode, so "only new jobs" alone is no longer the distinction. The difference is in the failure cases listed at the top of this page — absence tolerance, state that genuinely persists across scheduled runs, never charging for an undelivered signal — plus a built-in health check, signed webhooks, and published volume figures so you can predict your bill. If you're comparing options, the useful test is to run each for a few days and see which one reports zero on an unchanged board and doesn't spam you when a board hiccups.

Is the health check charged? No. It reports no jobs, so there is nothing to charge for. Schedule it as often as you like.

Do I have to use the webhook secret? No, it's optional. Set it if your endpoint is publicly reachable and you want to reject anything that didn't come from this Actor — which, if you're triggering internal automation off these alerts, you should.

How do I keep the cost down? Use filters. You're charged only for new jobs that match them, so a keyword or department filter is a direct discount. Tracking a few very large companies unfiltered is the expensive case.

How often should I schedule it? Daily suits most use cases. Hourly makes sense for competitive sales triggers where being first matters. The frequency doesn't change your bill — you're charged per new posting, not per run, so an hourly schedule over a quiet week still costs nothing.

Can I get alerted only about a specific department or location? Yes — departments and locations, alongside keywords. They combine with AND.

What happens if my webhook endpoint is down? It's retried, then logged and skipped. The run still succeeds and the jobs are still in the dataset, so you can recover anything you missed.

Do I need to check the dataset if I use webhooks? No, but it's useful. The webhook carries the essentials (company, title, URL, timestamp); the dataset row adds department, location, remote, employment type and salary.


Keywords: ATS scraper, Greenhouse API, Lever jobs, Ashby jobs, hiring signals, new job alerts, sales intelligence, recruiting intelligence, competitor hiring tracker, job posting webhook.