Job Board Scraper & Change Tracker avatar

Job Board Scraper & Change Tracker

Pricing

from $0.70 / 1,000 new job detecteds

Go to Apify Store
Job Board Scraper & Change Tracker

Job Board Scraper & Change Tracker

Find jobs at companies you follow and track new, updated, and removed postings across 38 hiring platforms. Filter by title, location, or remote status. Export results to CSV, Excel, or JSON.

Pricing

from $0.70 / 1,000 new job detecteds

Rating

0.0

(0)

Developer

Dakota Styck

Dakota Styck

Maintained by Community

Actor stats

0

Bookmarked

1

Total users

1

Monthly active users

16 days ago

Last modified

Security

Limited permissions

Share

Watch jobs at companies you care about

Use this job board scraper to find jobs at companies you follow and see which postings appear, change, or disappear. Filter by title, location, or remote status for your job search, hiring research, or job feed. Export results to CSV, Excel, or JSON.

Supported platforms include Workday, Greenhouse, Lever, Ashby, Workable, and SmartRecruiters, with 38 supported in total. You choose the company boards to check; platform support does not mean every employer is searched. See the full platform list and setup limits in the FAQ below.

Costs: No job-event charges for unchanged listings; run fees still apply. Preview has no job-event charges and does not start tracking. Showing current jobs on the first tracking run charges each matching posting as a new-job event.

Try the engineering-job example at Stripe and Databricks. It starts in preview so you can inspect matches before tracking.

Start in four steps

New Console runs start with a small Stripe/Compliance example. Replace both the company and job-title entries for your own search. Running the example returns current matches and charges each as a new-job event; run fees also apply. API calls and existing saved tasks do not acquire these example values.

  1. Choose employers. Add names or supported careers links under Companies or careers links. Start with 5–10 employers, or turn on Try an example: 3 companies to add Airbnb, Anthropic, and Spotify. Add or remove entries whenever you need.
  2. Choose your jobs. Example: titles engineer, developer; location Chicago. Leave fields blank to include everything. Words within a field match any; different fields must all match.
  3. Preview. Turn on Preview before tracking, run, then open Start here: run report. Check the resolved boards, match counts, sample jobs, and estimate. Preview saves no tracking baseline and charges no job events. Normal run costs still apply.
  4. Start tracking. Turn preview off. Enter a Search name, such as my-engineering-search, or save an Apify task. Choose your first-run behavior, run, then schedule that saved task for daily checks. Reuse the same search name and filters for continuing tracking.

When tracking starts:

ChoiceWhat you get
Show current matching jobs (default)Matching openings now, each charged as a new-job event. Later runs return changes. “New” on the first run does not mean posted today.
Start tracking from todayA saved starting point, with no initial job-event charges. Later runs return changes.

Both choices still incur normal run costs. A direct run without a search name or saved task is a one-time check and saves no reusable baseline.

Remote jobs only includes jobs the source marks remote. Unknown work arrangements are excluded; remote does not guarantee eligibility from every country. Locations use text matching, not a radius or geographic search.

Quick walkthrough: follow engineering jobs

  1. Open the linked example and choose Try to copy it into your Apify account. It selects the Stripe and Databricks careers links and the job-title words engineer or data.
  2. Run with Preview before tracking on. Open Start here: run report for board status, match counts, up to 20 sample jobs, and an estimate. The preview dataset is empty; sample jobs appear in the report.
  3. Turn preview off and run the saved task with Show current matching jobs. Open Matching jobs / changes for your results, or export them. Current matches are charged as new-job events.
  4. Run the same task again later, or schedule it for daily checks. Later runs report changes since its saved starting point; a successful check may find no changes. Scheduling alone does not send email.

Basic examples

Paste an example into the JSON input editor, run its preview, then change preview to false to track it. These examples use public company boards; availability and matches can change.

Job hunting: find engineering openings at three example employers.

{
"use_example_companies": true,
"title_keywords": ["engineer", "developer"],
"feed_id": "my-engineering-search",
"preview": true
}

Recruiter watchlist: track future sales-role changes at a chosen employer.

{
"companies": ["https://boards.greenhouse.io/airbnb"],
"title_keywords": ["sales"],
"feed_id": "sales-watchlist",
"first_run_mode": "baseline_only",
"preview": true
}

Job-feed builder: return a current list alongside change labels on each run.

{
"companies": ["https://jobs.lever.co/spotify"],
"title_keywords": ["engineer"],
"feed_id": "engineering-feed",
"emit_unchanged": true,
"preview": true
}

Unchanged rows have no job-event charge. New, updated, and removed postings can be charged. Results can be downloaded as CSV, Excel, or JSON; scheduling does not itself configure email delivery.

Read your results

Open Start here: run report after each run. It explains whether your starting point was saved, no changes were found, a board could not be checked, or output hit the charge limit. The Matching jobs / changes table shows Job, Company, Location, Work arrangement, Change, Observed at, and Job link. Technical details retains source IDs, hashes, and other integration fields. The full-data output link still returns all fields.

Preview displays at most 20 sample jobs but counts all observed matches. Its estimate covers a new search on successfully checked boards. It does not read an existing baseline or predict tomorrow's changes. The job-event subtotal uses prices supplied to the running actor when available; run-start/platform costs are additional. Missing prices are shown as unavailable, never guessed. Failed boards are excluded from the estimate.

Choosing boards and handling duplicates

Supported ATS-hosted careers links route directly to the right platform. Bare company names use the existing lookup order. If a custom company-domain link cannot be identified, use the matching field under Advanced: boards by platform. Unsupported or ambiguous URLs produce a corrective message rather than guessing another employer.

Repeated selections and verified equivalent spellings of the same board are fetched once. Existing saved board identities are retained when switching to an equivalent supported link, avoiding a new-job bill caused only by address formatting. Ambiguous historical aliases are reported for repair instead of merged automatically.

Separate postings remain separate, even when titles match. Cross-platform duplicate grouping is not included yet. In particular, multiple ADP career sites for one employer can repeat its requisitions: select one of those sites.

Technical reference

Existing API keys remain supported. feed_id is the form's Search name; first_run_mode accepts emit_all_as_new or baseline_only. Saved tasks supply their task ID when feed_id is blank. Give separate searches different feed IDs. Existing state_key inputs remain accepted as legacy feed IDs; migration seeds the new feed silently while retaining the legacy record.

The actor observes every job on a selected board, then evaluates search membership. A job that remains open but stops matching emits an unbilled match-exited row, never a closure. Editing the filters recalculates membership silently.

Output

Each dataset item is one change record. Read change_type first: new is a new posting matching your search, updated means a continuously matching posting changed, and removed means a previously matching posting disappeared from the board. match-entered and match-exited mean an existing posting crossed a stable search boundary while staying on the board. They are unbilled and include the current posting. Query edits recalculate membership silently. unchanged appears only when you request it and is never billed.

{
"change_type": "updated",
"change_reason": "content",
"job_key": "greenhouse:stripe:5416444",
"source": "greenhouse",
"board": "stripe",
"external_id": "5416444",
"title": "Data Analyst",
"company": "Stripe",
"locations": ["San Francisco, CA", "US"],
"department": "Data Science",
"team": null,
"employment_type": null,
"is_remote": null,
"url": "https://stripe.com/jobs/search?gh_jid=5416444",
"posted_at": "2026-06-02T12:58:57Z",
"updated_at": "2026-07-27T15:17:30Z",
"first_seen_at": "2026-08-01T09:00:00Z",
"last_seen_at": "2026-08-04T01:04:17Z",
"content_hash": "3f2a91c04be7d158"
}

The dataset downloads as JSON, CSV, Excel, or HTML.

A removed item has no current record - the posting is gone - so it carries source, board, external_id, job_key, first_seen_at, last_seen_at, and the last content hash. It also includes last-known title, company, locations, and URL when available. Older baselines may lack those details. Treat it as a deletion keyed by job_key; the retained link may no longer open.

Data fields

FieldDescription
change_typenew, updated, removed, match-entered, match-exited, or opt-in unchanged; only the first three use billable job events
change_reasonFor updated: content when the tracked-field hash moved, timestamp when only the vendor timestamp moved and you opted in, or content+timestamp when both moved
job_keyStable identity, source:board:external_id; use it for downstream upserts and deletions
sourceOne of the 38 source keys in src/sources/__init__.py:SOURCES: greenhouse, icims, jazzhr, factorial, paylocity, lever, ashby, avature, workable, recruitee, rippling, breezy, comeet, dover, jobvite, jobsoid, join, teamtailor, smartrecruiters, successfactors, taleo, bamboohr, personio, phenom, oracle, occupop, eightfold, manatal, polymer, pinpoint, zoho, workday, ukg, bullhorn, recruiterflow, gem, adp, or traffit
boardThe board token the job came from
titleJob title
companyCompany name, where the vendor publishes one (Greenhouse, Workable, Recruitee, Breezy, Teamtailor, SmartRecruiters; the rest do not)
locationsAll locations attached to the posting
department / teamOrg placement, where the source provides it
employment_typee.g. "Regular Full Time" (Lever), "FullTime" (Ashby), "Full-time" (Workable), "fulltime_permanent" (Recruitee); Greenhouse and Rippling publish none
is_remotetrue, false, or null when the source gives no signal
urlPublic posting URL
posted_atWhen the posting went live, ISO-8601 UTC
updated_atVendor's last-modified claim, when its public feed provides a stable one; otherwise null
first_seen_atWhen this actor first saw the job; survives updates
last_seen_atThis run's timestamp; on a removal, when the absence was observed rather than a vendor-supplied close time
content_hashHash of the tracked fields, for your own downstream checks; it excludes updated_at

A machine-readable receipt is written to the key-value store under RUN_SUMMARY. Check it before interpreting an empty or short dataset:

{
"outcome": "partial",
"first_run_mode": "baseline_only",
"first_run_boards": ["greenhouse:stripe"],
"silent_baseline_boards": ["greenhouse:stripe"],
"totals": {"new": 0, "updated": 2, "removed": 0, "unchanged": 0},
"detected": {"new": 0, "updated": 5, "removed": 84, "unchanged": 0},
"emitted": {"new": 0, "updated": 2, "removed": 0, "unchanged": 0},
"charged": {"new": 0, "updated": 2, "removed": 0, "unchanged": 0},
"withheld": {"new": 0, "updated": 3, "removed": 84, "unchanged": 0},
"billable_change_events": 2,
"failed_boards": ["lever:example"],
"unresolved_companies": ["unknown-company"],
"mass_removals_withheld": [{"board": "workday:example", "jobs": 84}],
"withheld_by_charge_limit": 3
}

outcome: "complete" means every resolved board was processed and no changes were withheld. partial means at least one input failed or did not resolve, the mass-removal guard held removals, the caller's charge limit stopped emission, or a push failed. detected counts change candidates plus removals held for confirmation. emitted counts accepted dataset rows; charged counts acknowledged job charges. withheld counts cap-limited rows and safeguarded removals. These counts also appear per board. On a push exception, publication_outcome_unknown and push_failed_boards identify the uncertain result, unconfirmed_after_push_failure counts candidates without confirmed publication, and state_committed is false. A retry can replay a row if the platform accepted it before returning an error. Legacy totals remains an alias for emitted; legacy per-board counts remains the candidate count before removal safeguards. billable_change_events is the acknowledged job-charge count and excludes the separate platform run-start event.

For tracking runs, read the receipt before interpreting a zero-row dataset: silent_baseline_boards together with state_committed: true means the requested starting point was saved without job-event charges; an empty withheld/failed section means the boards were simply quiet. Failed boards keep their old baseline and produce no removals. Mass-removal and charge-limit records are intentionally held out of both the dataset and the new baseline so they can be reconsidered or re-reported later.

Preview also uses RUN_SUMMARY, with mode: "preview", observed/matching counts, bounded samples, and a fresh-search estimate. It leaves the dataset empty and never commits tracking state.

How change detection works

Most of the difficulty is in deciding what counts as a change.

Every job carries a content hash. It covers the tracked fields: title, locations, department, team, employment type, remote flag, URL, posted date. A job is updated when the hash moves. Greenhouse also publishes an updated_at, and when that moves too you get change_reason: "content+timestamp", but on its own it never counts as a change unless you opt in via detect_timestamp_only.

That default comes from a measurement, not from caution. Greenhouse occasionally re-stamps nearly a whole board inside one minute with no tracked field changing at all. Under the opt-in, each of those events would bill you for hundreds of updates that never happened.

Only sources with a stable vendor modification timestamp populate updated_at. Greenhouse, Recruitee, Comeet, Eightfold, Dover, and Bullhorn currently provide one; the other 32 sources rely on the content hash for update detection. Creation, release, publication, and relative display dates are not treated as modification timestamps.

Two vendors also publish no description: Rippling and BambooHR return no body text at any request, and Breezy returns none either. On those boards a description-only edit cannot be detected, whatever hash_description is set to.

SmartRecruiters cannot report an empty board. Its API answers an unknown company id with an empty list rather than an error, which is indistinguishable from a real board that closed every job. Rather than risk reporting a whole board as removed because of one typo, a SmartRecruiters board returning zero jobs is treated as a failed fetch and your baseline is left untouched - so a genuine close-out of every job on such a board is never reported.

Only fingerprints are stored. A job id, a 16-character hash, two timestamps. Tracking 10,000 jobs costs well under a megabyte of state.

State lives in a named key-value store (state_store_name). Apify gives every run a fresh default key-value store, so a baseline written there would vanish and every run would look like a first run.

Adding a board doesn't reset the others. Baselines are tracked per board, so a newly added board is a first run for that board alone while every existing board keeps diffing normally. You aren't re-billed for your whole corpus because you added one company.

Changing what is hashed silently rebaselines existing boards. Flipping hash_description changes what the hash covers, so old and new hashes cannot be compared. Each existing board's next successful fetch records a new comparison baseline without billing its corpus as updates. Failed boards retain their prior history until they can be fetched successfully.

Overlapping runs of one feed use one writer. A second run waits briefly for the first to finish, then reads the committed baseline; if the wait expires, it fails with a retryable message. Separate feeds can run concurrently. A crash after a charged row is accepted but before its state write can still replay that row on retry.

Reliability

Change feeds have a specific failure mode: an outage that looks like news. Two guards address it.

A failed fetch is not an empty board. If a board returns an error, times out, or serves a non-JSON body, that board is skipped entirely: no records emitted, baseline preserved untouched. Without this, one transient 503 would report every job on that board as removed. Failed boards are listed in RUN_SUMMARY.failed_boards.

Mass removals are withheld pending corroboration. If more than removal_alarm_ratio (default 50%) of a board's tracked jobs disappear in a single run, the removals are held back and the baseline is kept, because a truncated-but-successful response looks exactly like a genuine mass close-out. Boards with fewer than 10 tracked jobs are exempt, where the ratio would be meaningless. Set suppress_mass_removals: false to let them through immediately.

Retries honor both integer seconds and HTTP-date Retry-After values in full. Without a valid header, they use exponential backoff with jitter, capped at 30 seconds. Each HTTP operation has a 300-second total budget covering requests and waits; request_timeout still bounds individual HTTP phases and max_retries bounds the retry count. On Apify, all boards and pages also share the run deadline minus a 30-second reserve for finishing. If a required wait cannot fit, the board fails with RetryBudgetExceeded and a message to retry in a later run; it appears in RUN_SUMMARY.failed_boards and retains its baseline. This does not automatically schedule another run.

Date waits use the longer interval computed from the local clock and a valid response Date, so a fast local clock does not shorten the server's requested wait. A slow local clock can cause a conservative deferral. A 404 isn't retried: it means the board token is wrong, and hammering a free public endpoint over a typo is rude.

How much does it cost?

Pay-per-event, two prices:

EventPrice
Run start$0.002 (charged once per run up to 1 GB of memory; this actor runs at 512 MB by default, so once)
Each new, updated, or removed record$0.001

match-entered, match-exited, and optional unchanged records are never charged.

You pay per change, so a large board that sits still costs you almost nothing. Measured on live Greenhouse boards of ~550 and ~800 jobs: 22-35 billable changes on a weekday, with weekends much quieter. That's a few cents either way. A daily poll over 10 boards with ~1,000 tracked jobs comes to roughly $1-2/month, and a quiet day costs $0.002, the run fee and nothing else. A snapshot API bills you for the same 500 rows every day whether or not anything moved.

The first run is the exception: with no baseline yet, emit_all_as_new bills every open job once as new. Choose first_run_mode: "baseline_only" to skip that and pay only for changes from run two onward.

Runs are HTTP-only with no browser, so compute is a small fraction of a browser-based scraper's.

Tips

  • Choose focused searches. Title and location filters reduce matching rows and billed change events while the actor still observes the full board to distinguish search exits from closures.
  • Leave include_description off unless you need the body text; it makes Greenhouse responses much larger.
  • Turn on hash_description only if you genuinely need description edits flagged, and know what it costs. Measured on Databricks' board, 2026-08-13: an employer added one legal boilerplate link to every posting at once, so 769 of 779 jobs would have billed as updated for a change that says nothing about the job. Ten were genuine edits, and six of those were already caught without it. Board-wide edits like that are normal, not rare: of sixteen Greenhouse boards scanned that day, ten showed one, and eight of those had fired within the previous ten days.
  • Use baseline_only on the first run if you only want deltas.
  • Keep max_concurrency modest. These are free public endpoints offered as a courtesy.

FAQ

Does this need an API key for any supported source? No. Every feed or public careers page used is unauthenticated and publicly available to job seekers.

Is this scraping? It reads public, unauthenticated job-board endpoints and server-rendered careers listings; there is no browser, CAPTCHA solving, or bypassing access controls. The payload format varies by source, including JSON, XML, and HTML.

Why is updated_at null on most of my jobs? Because only Greenhouse, Recruitee, Comeet, Eightfold, Dover, and Bullhorn currently provide a stable modification timestamp. On the other 32 sources, change detection works entirely off the content hash. This is a property of the public feeds, not a limitation of this actor.

Why did a job show as updated with no visible difference? Only possible if you enabled detect_timestamp_only: change_reason: "timestamp" means the vendor's updated_at advanced but nothing in the content hash moved. It's off by default because Greenhouse re-stamps nearly a whole board at once whenever the employer edits every posting - often just to add a legal link - and on the event measured 2026-08-13 that touched 769 of 779 jobs. If you do want description edits, enable hash_description; those arrive as change_reason: "content", and you should expect board-wide bursts along with the edits you actually care about.

Why did no removals appear when a board clearly emptied out? The mass-removal guard held them. Check RUN_SUMMARY.mass_removal_flagged. Set suppress_mass_removals: false if you'd rather have them immediately.

Which ATS platforms are supported? Thirty-eight: Greenhouse, iCIMS, JazzHR, Factorial, Paylocity, Lever, Ashby, Avature, Workable, Recruitee, Rippling, Breezy HR, Comeet, Dover, Jobvite, Jobsoid, JOIN, Teamtailor, SmartRecruiters, SAP SuccessFactors, Oracle Taleo, BambooHR, Personio, Phenom, Oracle Cloud HCM, Occupop, Eightfold, Manatal, Polymer, Pinpoint, Zoho Recruit, Workday, UKG Pro Recruiting, Bullhorn, Recruiterflow, Gem, ADP, and Traffit. Fourteen are also reachable by company name alone: Greenhouse, Lever, Ashby, Workable, Recruitee, Rippling, Breezy HR, Teamtailor, BambooHR, Manatal, Dover, Gem, Occupop, and iCIMS. The other 24 must be named explicitly because their board address is not a company slug or they cannot safely distinguish an unknown board from an empty or demo result. SmartRecruiters carries one further limit worth stating: an unknown company id returns an empty success rather than a 404, indistinguishable from a board that closed every job, so a SmartRecruiters board that reads as empty is treated as a failed fetch and your baseline is carried forward untouched - a genuine total close-out there is never reported.

Support

Found a bug or want another ATS supported? Open an issue on the Issues tab.

Development

See docs/BUILDING.md for dependency locks, Python 3.14 checks, schema validation, and the container/CI release workflow.