FreshHire — Verified ATS Job Changes API
Pricing
from $2.00 / 1,000 ats board monitoreds
FreshHire — Verified ATS Job Changes API
Monitor Greenhouse, Lever and Ashby company career pages for new openings, material updates, reopened jobs and confirmed closures. Includes a normalized ATS job scraper snapshot mode.
Pricing
from $2.00 / 1,000 ats board monitoreds
Rating
0.0
(0)
Developer
Cesar Joaquin Lares Illescas
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
2 days ago
Last modified
Categories
Share
FreshHire — ATS Job Scraper & Job Change Monitoring
Track hiring activity directly from company job boards. FreshHire collects Greenhouse Jobs, Lever Jobs and Ashby Jobs in a consistent format, then monitors those boards for new openings, meaningful updates, confirmed closures and reopened positions.
Use Snapshot mode to get current vacancies or Changes mode to receive only the changes detected since earlier scans. Persistent monitoring saves you from comparing repeated job lists yourself. Each change includes stable identifiers and observation times; updates identify the changed fields and their previous values.
FreshHire gives recruiting platforms, job aggregators, labor market researchers and automation developers a shared interface to the Greenhouse Jobs API, Lever and Ashby public job boards. Use it to:
- Feed new job alerts and recruiting workflows with newly observed openings.
- Keep job directories current with updates, closures and reopened listings.
- Follow hiring activity at selected companies or departments.
- Supply structured job data to APIs, dashboards and AI agents.
FreshHire retrieves public ATS data directly. No ATS login, browser setup, proxy subscription or paid enrichment API is required.
Quick start
- Open the Actor's Input tab and paste one of the JSON examples below.
- Select 256 or 512 MB of memory and set a maximum cost per run in Apify's run options.
- Run the Actor. View or export records from its default dataset.
- Check
RUN_SUMMARYin the run's default Key-Value Store, especially when the dataset is empty or the run is partial.
Changes mode charges for successful monitoring scans even when no changes are found. The first baseline also incurs scan fees. See pricing before scheduling recurring runs.
Snapshot: get current Greenhouse jobs
{"mode": "snapshot","boards": [{"source": "greenhouse", "boardId": "greenhouse", "company": "Greenhouse"}],"includeDescriptions": false,"maxResults": 100}
This returns up to 100 currently listed jobs with changeType: "snapshot". Snapshot mode does not maintain monitoring history: its firstSeenAt and lastSeenAt describe observations in that run.
Changes: monitor Ashby jobs across runs
{"mode": "changes","monitorId": "my-job-monitor-v1","stateStoreName": "freshhire-my-job-monitor-v1","boards": [{"source": "ashby", "boardId": "Ashby", "company": "Ashby"}],"includeDescriptions": false,"includeDetails": false,"maxResults": 100}
Run this input once to establish a baseline, then again with the same monitorId, stateStoreName and board settings. The first complete scan records current jobs and emits no change rows. Later runs compare against that stored history. A complete scan with no changes also returns an empty dataset; both runs still incur startup and successful board-scan fees.
Choose an unused state-store name for your first monitor so FreshHire can create it under its storage permissions. Keep that store between runs. Different monitor IDs keep independent histories, including when they share a store. A new monitor ID starts a new baseline.
The example board identifiers are public boards used in FreshHire's integration checks. Their jobs and availability can change. Substitute the boards you want to monitor.
Pricing
FreshHire uses Pay Per Event with these prices in USD:
| Charge | Price | When it applies |
|---|---|---|
Actor start (apify-actor-start) | $0.00005 per run | Automatic startup charge at the documented 256–512 MB allocation, including runs that return no records or fail. |
Dataset result (apify-default-dataset-item) | $0.001 per delivered result | Each record written to the default dataset: a snapshot job or a change event. |
Board scanned (board-scanned) | $0.002 per successfully monitored ATS board | Changes mode only, after a complete successful scan is saved to monitoring state. |
Monitoring is charged for checking boards, even when no changes are detected. Baseline initialization, unchanged boards and valid empty boards qualify for the board-scan charge. Every new scheduled run can incur these charges again.
Failed, skipped or incomplete boards do not incur a board-scan fee. Snapshot mode has no board-scan fee. Diagnostic errors and RUN_SUMMARY are not paid dataset records. Startup charges still apply when a run produces no output.
For a normal run at the documented memory allocation:
- Snapshot:
$0.00005 + ($0.001 × delivered results). - Changes:
$0.00005 + ($0.002 × successful complete board scans) + ($0.001 × delivered changes).
| Example | Event fees |
|---|---|
| Snapshot delivering 100 jobs | $0.10005 |
| First baseline for one board, no change rows | $0.00205 |
| One unchanged board, no change rows | $0.00205 |
| One board with three delivered changes | $0.00505 |
| Three unchanged boards, no change rows | $0.00605 |
These examples assume sufficient budget, one startup event and no crash-related replay. Apify charges startup and dataset events automatically; FreshHire charges only the custom board-scan event. If a run is allocated more than 1 GB, Apify can charge multiple startup units, so use 256–512 MB for the per-run startup price above. See Apify's event-pricing rules and the Actor's Pricing tab for the applicable rates.
Spending limits and pending results
Set the maximum cost per run in Apify Console or the run API options. It is a per-run limit, not a monthly monitoring budget. maxResults separately caps delivered rows; it does not cap board scans or their fees.
FreshHire checks affordability before starting a new paid scan or delivering another result. A low budget can leave boards uninitialized, skip scans or leave eligible changes waiting for delivery. Check spendingLimitReached, partial, boardsSkipped and pendingChanges in RUN_SUMMARY.
In Changes mode, complete scan state and eligible events are saved before the custom board charge. Undelivered committed changes remain pending for later runs. Reuse the same monitor and boards with sufficient budget and result capacity to deliver them. Pending changes are delivered before a fresh scan of their board; if some remain, that scan is skipped. Snapshot results have no persistent delivery queue: rerun a snapshot to retrieve results omitted by its limits.
Output filters do not reduce monitoring fees. All boards in boards remain configured for scanning, including when sources, companies or keywords exclude their results. Remove a board from boards to stop monitoring it, after consuming its pending results.
Billing and recovery limitations
Checkpoints suppress repeated custom charges during retries of the same Apify run. If a charge's outcome is uncertain, FreshHire stops further paid work rather than risking a repeated charge. This can leave a saved scan unbilled; it does not erase monitoring history or pending changes.
Dataset delivery is at least once. A crash after a dataset write but before acknowledgement can cause the same event to be delivered and automatically charged again on recovery. Deduplicate downstream by eventId; deduplication does not reverse a dataset fee. Exactly-once delivery or charging after a crash is not guaranteed.
SDK counts and platform counters can become visible at different times. Use the run's platform event counts when reconciling charges; RUN_SUMMARY.billing is operational accounting, not a settled customer invoice. Abrupt platform termination can prevent a final summary from being written.
Supported ATS boards
Each boards entry needs a source and either a boardId or a supported hosted-board url. company is an optional label; otherwise it defaults to the board identifier.
| Provider | Example board entry |
|---|---|
| Greenhouse Jobs API | {"source":"greenhouse","boardId":"greenhouse","company":"Greenhouse"} |
| Lever Jobs, global | {"source":"lever","boardId":"leverdemo","region":"global"} |
| Lever Jobs, Europe | {"source":"lever","boardId":"YOUR_BOARD","region":"eu"} — replace YOUR_BOARD with your EU board identifier. |
| Ashby Jobs | {"source":"ashby","boardId":"Ashby","company":"Ashby"} |
For example, use {"source":"ashby","url":"https://jobs.ashbyhq.com/Ashby"} instead of its boardId entry. Supported hosted domains include boards.greenhouse.io, job-boards.greenhouse.io, jobs.lever.co, jobs.eu.lever.co and jobs.ashbyhq.com.
Supply the ATS board root, not an individual job URL or custom company careers page. Arbitrary fetch URLs, credentials, query strings, fragments and custom ports are rejected. Identical boards are checked once; conflicting company labels for a duplicate board are rejected. Lever's global and EU regions are separate identities.
How Job Change Monitoring works
Each board creates its baseline independently on its first complete successful scan. RUN_SUMMARY.boardsInitialized identifies those boards. Existing vacancies are recorded as the baseline, not emitted as new jobs.
| Change type | Meaning |
|---|---|
new | A provider job ID newly observed in this monitor's retained history. It does not prove that the vacancy was just published. |
updated | A material field changed, such as title, department, location, workplace type, employment type, compensation, URLs or an enabled description. |
closed | A previously open job was absent from two consecutive complete successful scans. It disappeared from the public board; this does not prove that the role was filled. |
reopened | The same provider ID returned after a confirmed closure, while its closed history was still retained. |
Observation timestamps, source-date-only changes, reordered fields/lists, whitespace and irrelevant description HTML formatting do not generate updates. changedFields and previousValues explain material updates. Description changes are tracked only with includeDescriptions: true.
Failed or incomplete scans cannot confirm closures. A failed or skipped scan resets the absence streak, so another two complete successful scans without the job are needed. A valid empty board is a successful scan and follows the same closure rule.
Keep company labels, includeDescriptions and includeDetails stable within a monitor. Incompatible setting changes are rejected; create a new monitor ID when deliberately starting a different baseline. Full board snapshots are stored independently of output filters. Changing filters cannot close jobs, but it does not replay historical changes that were previously filtered out. Already-pending events keep their original eligibility.
Filters and useful settings
| Input | Default | Behavior |
|---|---|---|
mode | snapshot | snapshot for current jobs; changes for persistent monitoring. |
monitorId | Required for Changes | Stable name for an independent monitoring history. |
stateStoreName | freshhire-state | Named Key-Value Store retained across monitoring runs. |
titleKeywords | [] | Case-insensitive substring matches in the title. |
locationKeywords | [] | Case-insensitive substring matches in the primary location field. |
workplaceTypes | [] | Exact values: remote, hybrid, onsite, unknown. |
sources / companies | [] | Output filters for ATS providers or company labels. Company matching is case-insensitive and exact. |
maxResults | 1000 | Dataset row cap, from 1 to 10,000. Does not make a board scan incomplete. |
includeDescriptions | false | Include plain text descriptions and monitor their changes. |
includeDetails | false | Fetch Greenhouse details, including available publication dates and structured pay ranges. Adds a request per posting. |
Keywords use OR within a list, AND between populated filter groups. For example, titleKeywords: ["engineer", "developer"] together with locationKeywords: ["London"] matches either title keyword plus London in the primary location. Empty lists accept all values. Unknown workplace types are not guessed from location text.
Output and run summary
Records include provider and company identifiers, titles, available locations, workplace and employment types, source dates, observation dates, available compensation and job/apply URLs. Unknown or unavailable values remain null or unknown. Additional provider fields and provenance can be present.
This is an illustrative change record, not a live vacancy:
{"source": "ashby","company": "Example Company","boardId": "example","jobId": "54b0f045-3763-4823-9fdc-c2d14b797920","stableId": "ashby:example:54b0f045-3763-4823-9fdc-c2d14b797920","eventId": "c9f7a9a16c09ae732056d0b02ba954780149a32085c5160427faf10397d8af03","title": "Backend Engineer","department": "Engineering","location": "Remote - United States","workplaceType": "remote","employmentType": "full_time","sourcePublishedAt": "2026-10-20T09:00:00Z","sourceUpdatedAt": null,"firstSeenAt": "2026-10-21T12:00:00Z","lastSeenAt": "2026-10-22T12:00:00Z","salaryMin": 140000,"salaryMax": 175000,"salaryCurrency": "USD","salaryInterval": "year","jobUrl": "https://jobs.ashbyhq.com/example/54b0f045-3763-4823-9fdc-c2d14b797920","applyUrl": "https://jobs.ashbyhq.com/example/54b0f045-3763-4823-9fdc-c2d14b797920/application","changeType": "updated","detectedAt": "2026-10-22T12:00:00Z","changedFields": ["salaryMax"],"previousValues": {"salaryMax": 170000},"provenance": {"sourcePublishedAt": {"field": "publishedAt","endpoint": "list","confidence": "source","meaning": "last_publication","status": "valid"}}}
stableId identifies a job across scans using its source, board and provider ID; Lever also includes its region. eventId identifies a particular change for deduplication and is absent from snapshot records. Similar titles at different companies remain distinct.
Read RUN_SUMMARY alongside the dataset. It reports requested/successful boards, failedBoards, boardsSkipped, boardsInitialized, fetched/delivered job counts, pending changes, duration, warnings and partial/incomplete flags. newJobs, updatedJobs, closedJobs and reopenedJobs count delivered rows; detectedChanges counts newly detected events before filtering. Its billing section and boardResults describe custom scan-charge outcomes and spending-limit handling.
Source dates and compensation
| Value | Meaning |
|---|---|
firstSeenAt | First successful observation in this monitor's retained history; in Snapshot mode, the current run's observation. Never substituted for source publication. |
lastSeenAt | Last successful scan containing the job. A closure retains its previous observation time. |
detectedAt | When FreshHire detected the snapshot/event. Delayed delivery preserves the event's original detection time. |
Greenhouse first_published | Preserved as sourcePublishedAt when available. Documented on the detail endpoint; enable includeDetails for those requests. |
Greenhouse updated_at | Source update time from list/detail responses, normalized as sourceUpdatedAt. |
| Lever dates | Publication/update timestamps are not guaranteed and remain null. Optional numeric createdAt is normalized to sourceCreatedAt, not treated as a publication date. |
Ashby publishedAt | Last publication time, which may differ from first publication; preserved as sourcePublishedAt with provenance. |
Dates with timezone information are normalized to UTC; missing, invalid or timezone-less dates remain null. Salary values retain their source currency and interval, with no FX conversion, annualization or salary inference from prose. Greenhouse documented cents may be converted to major currency units. Multiple or ambiguous pay tiers remain structured ranges instead of being merged across currencies or intervals.
Scheduling and API access
Save a Changes input as an Apify task. Run it once for the baseline, then add that task to a schedule with the same monitor and store. For example, 0 */6 * * * runs every six hours. Choose a timezone explicitly and a run timeout shorter than the schedule interval. See Apify's scheduling instructions.
Use one active run per monitor. Avoid overlapping schedules or manual runs against the same monitor. Different monitor IDs can operate independently. Each scheduled execution has its own spending limit and can incur startup and monitoring fees.
The Actor's API tab provides authenticated examples for starting runs and retrieving datasets. You can also invoke a saved task, then read its run's default dataset and RUN_SUMMARY using Apify's API clients. Keep your Apify token in an environment variable or secret store. No separate FreshHire server or external database is needed. Send delivered events to your own alerting or automation workflow.
Troubleshooting and limitations
| Symptom | What to check |
|---|---|
| Empty Changes dataset | boardsInitialized, detectedChanges, filters, pendingChanges and partial. A baseline or unchanged scan can return no rows and still incur monitoring fees. |
| 404 / board not found | Confirm the public board identifier and Lever region. A failed request is not an empty board. |
| 403 or authentication challenge | Verify source access and permissions. FreshHire does not bypass access controls. |
| 429 / timeouts | Increase request pacing or reduce concurrency. Retries and excessive delays are bounded. |
| Pending changes or spending limit | Retain the same monitor/store/boards and provide sufficient budget and maxResults for delivery. |
| State conflict / lock busy | Wait for the existing run to finish. An uncertain cloud write can leave a lease until its one-hour expiry; do not forcibly clear a live lease. |
| Description/detail settings changed | Restore the prior settings or choose a new monitor ID for a new baseline. |
| State-size limit | Review board size and description settings. A new monitor without descriptions can reduce state size; incomplete scans cannot confirm closures. |
Polling cannot detect jobs that appear and disappear between scans. A closure confirms absence from the public board, not that a role was filled. Providers may silently omit postings; validation and two-scan confirmation reduce transient false closures but cannot establish completeness beyond the provider's response. A replacement provider ID appears as a new job and eventual closure of the old ID.
Reopening recognition is limited to retained history: closed jobs default to 180 days and 10,000 records per board. Old closed records can be pruned with a warning; a later reappearance then becomes new. Source API changes require ongoing adapter maintenance.
Independent Cloud runs have verified snapshots, persistent baselines, unchanged monitoring, lock contention, event counts and graceful budget handling on the documented boards. Controlled cloud crash recovery and forced cloud lifecycle transitions have not been verified; local tests cover those transitions. These checks do not guarantee exactly-once delivery, customer invoice settlement or capacity for every board size.
Responsible use
Use public job information only where permitted by source terms and applicable law. Choose reasonable polling intervals and respect access restrictions. Public availability does not grant unrestricted redistribution rights. FreshHire does not collect candidate records, submit applications, bypass authentication or CAPTCHAs, or enrich listings with unrelated personal data.
Technical details
FreshHire uses Python 3.12+, the official Apify Python SDK and asynchronous HTTP requests. Each source has an independent adapter. Requests use timeouts, bounded retries, rate-limit handling and configurable pacing. No browser automation or LLM processing is used.
| Provider | Public API used |
|---|---|
| Greenhouse | https://boards-api.greenhouse.io/v1/boards/{boardId}/jobs; optional job details. See Greenhouse Job Board API. |
| Lever | https://api.lever.co/v0/postings/{boardId}?mode=json, or api.eu.lever.co for EU boards. See Lever Postings API. |
| Ashby | https://api.ashbyhq.com/posting-api/job-board/{boardId}?includeCompensation=true. Postings marked isListed=false are excluded. See Ashby Public Job Posting API. |
Request concurrency defaults to 3 (range 1–5), with at least 0.5 seconds between requests to a provider host by default. HTTP timeouts default to 30 seconds and transient retries to 3. Source responses have a 20 MiB size limit. maxJobsPerBoard defaults to 20,000, with a configurable maximum of 50,000; exceeding it makes the scan incomplete.
Monitoring uses versioned board records in a named Key-Value Store, protected by a request-queue lease in Cloud and a file lock locally. These stores do not provide transactional, fenced compare-and-swap writes. A board state has an 8 MiB safety bound; pending events are bounded to twice maxJobsPerBoard. Excessive state fails explicitly. Retired monitors and removed boards are not automatically deleted; retain pending data before deciding to archive or delete their storage. See Apify storage concepts and Actor permissions.
Local development
Store users can run FreshHire directly in Apify; these commands are for developers with the source files. Requires Python 3.12+ and Node.js for the CLI:
python -m venv .venv.\.venv\Scripts\Activate.ps1python -m pip install -r requirements.txt -r requirements-dev.txtnpx --yes apify-cli@1.10.0 validate-schemanpx --yes apify-cli@1.10.0 run --entrypoint src --input-file examples/ashby.jsonpython -m pytest -q
On Linux/macOS, activate with source .venv/bin/activate. The Python entry point is python -m src; the Dockerfile uses apify/actor-python:3.12. Local named storage must survive between monitoring runs. Keep local and Cloud stores separate and run one local writer per monitor. Internal validation reports and maintenance procedures are in the source checkout's docs directory; customer-facing behavior and recovery limits are described above.
Local Docker execution
From a source checkout in PowerShell:
docker build -t freshhire:local .$freshHireRoot = (Get-Location).Path$freshHireStorage = Join-Path $freshHireRoot "storage/docker-manual"New-Item -ItemType Directory -Force -Path $freshHireStorage | Out-Nulldocker run --rm --memory 256m --memory-swap 256m `--env APIFY_IS_AT_HOME=false `--env APIFY_LOCAL_STORAGE_DIR=/usr/src/app/storage `--env CRAWLEE_STORAGE_DIR=/usr/src/app/storage `--mount "type=bind,source=$freshHireRoot/examples/lever.json,target=/usr/src/app/INPUT.json,readonly" `--mount "type=bind,source=$freshHireStorage,target=/usr/src/app/storage" `freshhire:local
This keeps local output in storage/docker-manual. For monitoring, replace examples/lever.json with examples/local-monitor-smoke.json and repeat with the same storage mount, monitor ID and store name. Named monitoring state survives container removal only when that mount is retained. Inspect the local dataset and RUN_SUMMARY; do not run simultaneous containers against the same monitoring storage.