Greenhouse, Lever & Ashby Jobs Scraper | Salaries & Changes
Pricing
from $1.00 / 1,000 job or change rows
Greenhouse, Lever & Ashby Jobs Scraper | Salaries & Changes
Collect public jobs from company Greenhouse, Lever and Ashby boards. Get titles, locations, descriptions and published pay, or track new, changed and no-longer-listed jobs. Supply board identifiers; unknown pay and remote status stay null.
Pricing
from $1.00 / 1,000 job or change rows
Rating
0.0
(0)
Developer
Felix GoldWin
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
2 days ago
Last modified
Categories
Share
Greenhouse, Lever & Ashby jobs, salaries and changes
Add a company's board identifier. Get its public jobs in one consistent JSON table. Collect job titles, locations, descriptions, source links and any structured pay the employer publishes through the official Greenhouse, Lever or Ashby API.
Use it for hiring research, company watchlists and published-salary comparisons. Choose the mode that matches your question:
| Your question | Choose | What you get |
|---|---|---|
| What jobs are listed now? | snapshot | The current jobs retrieved, up to your limits. |
| What changed since my last check? | incremental | A first-run baseline, then new, changed, reappeared or no-longer-listed events. Requires complete fetches and a persistent namespace. |
Start with the examples/small-snapshot.json and the Console steps. Then use the examples/incremental.json to track one board over time.
Coverage is limited to the company boards you supply. Missing structured salary or remote evidence stays null; the scraper does not estimate it from descriptions. This is an MVP with tested examples and documented limits. It does not discover every company or provide full-market coverage. This independent tool is not affiliated with Greenhouse, Lever, or Ashby.
What data is extracted?
| Field | Meaning |
|---|---|
id, sourceJobId | Stable board-scoped ID and source posting ID. Greenhouse posting IDs are distinct from internal job IDs. |
provider, board, region, company | Source identity. Company is a supplied label or the board identifier; provenance identifies that fallback. |
title, location, sourceUrl | Published title, primary location, and posting URL. Additional source locations remain in provenance. |
remote | true, false, or null, based only on supported structured source fields. |
description | Published source description. HTML or encoded HTML is preserved as data. |
publishedPay | Structured published ranges/components, source currency and period, plus raw provenance; null when unavailable. |
publishedAt, sourceUpdatedAt | Source timestamps when exposed by the adapter. These are distinct from observation times. |
contentHash, provenance | Deterministic normalized-content hash and the source fields/endpoints used. |
eventType, eventId | Snapshot or incremental event type and deterministic delivery key. |
firstSeen, lastSeen, observedAt | Local observation history; not evidence of the original publication date. |
No source URLs embedded in job records are fetched. Descriptions and raw source fields are untrusted text; downstream consumers must escape them before displaying HTML.
Run on Apify
- Open the Actor's Input tab and supply a company's board identifier with its provider. Use an identifier such as
lever, not a full URL; setregion: "eu"for the small Lever example. - Start with
mode: "snapshot"and a smallmaxResults, such as 10. Leave Greenhouse pay enrichment off until you have checked the base output. - Review the Console price and set the run's maximum charge. At the base rates below, 10 delivered rows plus a start cost $0.015. Allow enough for the whole output bundle; an insufficient row budget produces an incomplete report and no new rows.
- Run the Actor, open its default dataset, and inspect REPORT in the default key-value store. Check both source completeness and delivery status before using the results.
- For change tracking, use
mode: "incremental", a fresh persistentstateNamespace, andsingleWriter: true. Externally serialize every run using that namespace. Recover an unfinished observation with the same input and observation ID before starting a new one.
Run the scraper locally
Requirements: Node.js 22 or later, npm, and the official Apify CLI available as apify. The lockfile pins dependencies. Public job API requests do not need credentials or proxies.
- In this project directory, run
npm ciandapify --help. - Create
storage/key_value_stores/default/INPUT.jsonwith an example below. Create its parent directory if needed. - Run the Actor with the official local command:
apify run --user-agent apify-codex-plugin/apify-actor-development
- Inspect
storage/datasets/default/for rows andstorage/key_value_stores/default/REPORT.jsonfor completeness and state diagnostics. Local storage is not uploaded to Apify Console.
Do not purge or delete the named state store when continuing an incremental watch. This project does not require apify push, cloud builds, paid runs, purchased proxies, or newly created credentials for local operation.
Snapshot example
{"boards": [{ "provider": "lever", "board": "lever", "region": "eu", "company": "Lever" }],"mode": "snapshot","maxResults": 10,"maxResultsPerBoard": 10,"pageSize": 3,"maxConcurrency": 1}
Snapshot mode emits the valid jobs retrieved, including partial results, with eventType: "snapshot". It does not update incremental state. Its first/last-seen values describe this snapshot only. Always inspect the report when a result or page limit is used.
Incremental example
{"boards": [{ "provider": "lever", "board": "lever", "region": "eu", "company": "Lever" }],"mode": "incremental","stateNamespace": "lever-eu-watch-v1","singleWriter": true,"observationId": "watch-001","maxResults": 1000,"maxResultsPerBoard": 1000}
Use a new observation ID, such as watch-002, for the next fresh observation. Reuse watch-001 with exactly the same input to replay its saved result. An ID is generated if omitted, but explicit IDs make interruption recovery easier.
Input options and limits
The .actor/input_schema.json defines the Console form; runtime validation also rejects unknown fields and invalid combinations.
| Option | Default | Accepted values / behavior |
|---|---|---|
boards | Required | 1–50 objects. provider: greenhouse, lever, or ashby; board: 1–100 letters/digits/underscores/hyphens, starting with a letter/digit. URLs are rejected. |
Board region, company | global; identifier fallback | eu is supported only for Lever. Company label is optional nonempty text up to 200 characters. Duplicate boards are rejected. Keep identifier spelling/case consistent between runs. |
mode | snapshot | snapshot or incremental. |
stateNamespace | None | Required for incremental: 3–50 lowercase letters/digits/hyphens, starting with a letter/digit. Store name is jobwatch-{stateNamespace}. |
singleWriter | false | Must be true for incremental; acknowledges the concurrency restriction below. |
observationId | Generated | 1–100 letters/digits/underscores/dots/hyphens, starting with a letter/digit. Immutable replay key. |
maxResults | 1,000 | 1–10,000 output rows globally. A board's events are not committed if they exceed the remaining budget. |
maxResultsPerBoard | 1,000 | 1–10,000 jobs per board, also bounded by maxResults. Truncation prevents state advancement. |
maxConcurrency | 2 | 1–5 board workers and a shared HTTP concurrency budget, including pay enrichment. |
pageSize, maxPages | 100; 100 | Lever page size 1–100; page count 1–1,000. Hitting the page cap without proving completion is incomplete. |
timeoutMs | 15,000 | 100–60,000 milliseconds per request, including DNS and response body. |
maxRetries | 2 | 0–5 retries after the initial attempt for selected transient failures. |
minIntervalMs | 300 | 100–10,000 milliseconds between starts to the same API host. |
maxResponseBytes | 20,971,520 | 1,024–52,428,800 bytes per response. Oversized responses fail closed. |
maxRetryWaitMs | 30,000 | 0–60,000 milliseconds for retry waits. A longer Retry-After fails the request instead of retrying early. |
greenhousePayDetails | false | Optional individual public job requests for structured Greenhouse pay. |
maxPayDetailRequests | 20 | 0–1,000 detail requests per Greenhouse board. Incomplete requested enrichment prevents incremental advancement. |
titleContains | None | Nonempty case-insensitive literal title filter, up to 200 characters. Any filter suppresses all incremental advancement. |
There are no user-configurable API hosts, request URLs, proxy settings, or application-submission methods. The HTTP transport allowlists official HTTPS API paths, refuses redirects, checks every DNS answer against public-address rules, and pins a checked address to the connection.
The timeout applies separately to each HTTP attempt. Queueing, host spacing and retry backoff can make the total run take longer than timeoutMs.
Incremental state and event semantics
State is independent for each provider/region/board within the namespace. A board omitted from a run is untouched. The first accepted complete unfiltered observation emits baseline, even when jobs were published long ago. An empty complete baseline is valid; later observed IDs are new.
| Event | Meaning |
|---|---|
baseline | Present in the first accepted observation. |
new | First observed after a baseline, not necessarily newly published by the employer. |
changed | The normalized content hash changed. |
reappeared | A previously no-longer-listed source ID returned. |
no_longer_listed | Missing in two complete successful unfiltered observations of the same board. It does not mean filled, hired, or permanently closed. |
Unchanged jobs update lastSeen without output rows. firstSeen survives changes and reappearances. Content hashes sort object keys and exclude observation metadata; source-published timestamps and selected raw provenance remain part of the content. Array ordering is preserved, so the hash measures normalized content, not a semantic text comparison.
Failures, malformed records, duplicate IDs, truncation, filters, and incomplete requested pay enrichment leave the entire board state unchanged, including its missing counters. Partial observations between two complete missing observations add no confirmation. A return resets the missing count. A suspicious disappearance of at least five active jobs and more than 50% of that board holds the entire observation indefinitely for operator investigation. The MVP does not provide a mass-hold override; after verification, a new namespace can establish a fresh baseline.
Company label, Greenhouse pay-enrichment choice, and normalization version form a persistent profile. Changing that profile requires a new namespace to avoid reporting configuration changes as source changes.
Replay and interruption recovery
Before making provider requests or creating a new observation attempt, the Actor checks for remaining row budget. If none remains, it stops without fetching or changing incremental state; any existing pending recovery remains intact. Otherwise, an immutable OBS-{hash} record preserves the exact fetched observation. A canonical EVENTS-{hash} bundle then stores all planned rows, the report and pending state updates before any state commit. The Actor checks again that the remaining row budget can cover the entire bundle before committing state or appending output. If it cannot, the full bundle remains pending and no new state is committed in that attempt. Recover with the same observation ID and input and a sufficient run budget; the saved observation is not fetched again.
A PENDING marker identifies the current delivery attempt. State commits occur after budget preflight and before dataset publication. The Actor verifies the SDK's accepted row count; only a full confirmed append writes a matching DELIVERED-{hash} receipt and clears pending state. Unexpected truncation or an uncertain append retains the full bundle for recovery, even when state already advanced. An older successful delivery receipt cannot acknowledge a later incomplete replay. If interruption happened before OBS was saved, recovery can fetch again because no board transitions were committed. Do not manually clear PENDING to bypass an unresolved interruption.
Completed replays return the original saved bundle without refetching or rewinding newer state. Recovery of a partially committed bundle applies only its outstanding state updates. The board engine also retains a replay ledger for its latest 100 accepted observations.
Dataset delivery is at least once. A crash or append retry can create duplicate rows; consumers must deduplicate by eventId. Recovery and explicit replay deliver the entire bundle into the current run's dataset, including any rows already delivered in an earlier attempt; those newly delivered rows are charged again. The canonical EVENTS bundle is the recovery record, not a transaction across key-value storage, datasets and billing. Replaying a failed or partial fetch repeats its saved result; use a new observation ID to request fresh data after that completed observation.
Concurrency and storage restrictions
Only one writer may use a namespace at a time. Local runs use a cooperative exclusive lock under storage/jobwatch-locks/ (or the configured local storage root). A crash can leave a stale lock: verify the prior process has stopped and inspect the lock before removing it. Locks are never automatically stolen.
Cross-machine/distributed locking is not implemented. singleWriter: true is an acknowledgment, not a distributed lock. Cloud use must externally serialize all runs sharing a namespace; overlapping runs are unsupported.
OBS and EVENTS records are retained permanently, and state retains historical job IDs. They contain descriptions and provenance and can grow substantially. There is no automatic retention, compaction, or storage-cost control in this MVP. Plan retention before sustained use; deleting records weakens historical replay guarantees.
Provider scope and published-pay caveats
Greenhouse: the public Job Board API list endpoint uses content=true. Structured pay needs per-job pay_transparency=true; the optional enrichment costs additional HTTP requests. Cent values are converted to currency units while preserving each raw range. The documented pay object has no period, so period remains null. Descriptions retain source HTML entities. No remote classification is inferred from a location or description.
Lever: the official Postings API supports global and EU hosts with offset pagination. Descriptions combine opening/body, lists, and closing content without duplicating combined sections. Optional salaryRange supplies currency and interval. Remote/on-site values are explicit; hybrid or unspecified workplace values remain null in the boolean remote field, with source values retained. A short page proves traversal ended; reaching a cap exactly is conservatively incomplete. Offset pagination is not an atomic snapshot and can race with provider edits.
Ashby: the public posting endpoint requests compensation and excludes isListed: false direct-link postings. publishedAt means last published. All compensation tiers and summary components are preserved; sourcePath distinguishes overlapping summaries from tier details. Never sum those components as independent pay ranges. Equity remains its published compensation type and is never relabeled cash or salary. Missing currency, period, amounts, and remote evidence remain unknown.
The adapters request public job-board data only, not private applicant endpoints or internal job data. Published descriptions can themselves contain names or contact text; they are retained as part of the public posting. No applicant records or private credentials are collected.
Output and diagnostics
The default dataset contains snapshot rows or incremental event rows. The default key-value store contains REPORT, including each board's complete, unfiltered, advanced, replayed, counts, and diagnostics. Completeness describes the fetch; an otherwise complete board can still be held or not committed. Always inspect advanced and diagnostics in incremental mode. A run that finishes with zero rows is not evidence that every board succeeded or has no jobs.
plannedOutputCount is the bundle size; outputCount is the SDK-confirmed number accepted in this run. delivery.status is complete, budget_insufficient, truncated, or uncertain. A budget hold reports zero accepted rows. When no row budget exists before an observation, delivery.reason is no_row_budget, the unfetched bundle size and observedAt are null, and pendingObservationId identifies any unchanged pending recovery. Unexpected truncation reports the confirmed total, while per-board returnedRows is null because a chunked append does not prove which rows were accepted. An uncertain append reports outputCount: null, saves the diagnostic report when possible, and fails the run. Incomplete delivery sets the report's overall status to incomplete and retains pending incremental recovery.
These are abbreviated synthetic examples, not live results:
[{"id": "lever:global:example:posting-1","sourceJobId": "posting-1","eventType": "baseline","title": "Engineer","remote": null,"publishedPay": null},{"id": "ashby:global:example:posting-2","sourceJobId": "posting-2","eventType": "changed","title": "Designer","remote": true,"publishedPay": {"sourceField": "compensation","ranges": [{"kind": "Salary","min": 90000,"max": 120000,"currency": "USD","period": "1 YEAR","sourcePath": "compensation.compensationTiers[0].components[0]"}]}}]
Use JSON to retain nested compensation/provenance. Apify dataset exports can provide formats such as JSON, HTML, CSV, or Excel when platform export functionality is used; this repository's local runs write local JSON records and do not upload an export.
Public board references and verification
The following primary-source references establish candidate identifiers. They are examples to inspect, not a guarantee that a company keeps using the same provider or that every live acceptance test passed:
| Provider | Board / region | Primary-source evidence |
|---|---|---|
| Greenhouse | anthropic / global | Anthropic open roles link to its hosted board. |
| Greenhouse | cloudflare / global | Official Cloudflare hosted posting; its board redirects to Cloudflare careers. |
| Lever | ro / global | Ro careers links to Ro's hosted listings. |
| Lever | lever / eu | Lever's official API documentation links to the EU example board. |
| Ashby | ashby / global | Ashby's API documentation gives its own board as the name example; hosted board. |
| Ashby | ramp / global | Ramp's hosted board; Ramp applicant privacy notice identifies Ashby. |
Run npm test for synthetic tests and npm run test:coverage for measured coverage. Tests cover adapters, pagination, malformed/partial failures, pay nulls, input/network validation, closure safety, replay, and concurrency behavior. Consult the delivery's test report for actual results, live checks, runtime versions, and remaining gaps. Synthetic tests do not prove future provider behavior, atomic remote snapshots, or cloud operational readiness.
Pricing and operating limits
The Actor uses Apify's automatic pay-per-event billing at these base rates:
| Event | Base price | What is charged |
|---|---|---|
apify-actor-start | $0.005 per start at up to 1 GB RAM | Each run start, including runs that return no rows or encounter an error. |
apify-default-dataset-item | $0.001 per delivered row ($1 per 1,000) | Each snapshot job or incremental event saved to the run's default dataset. |
Repeat snapshots, partial snapshot results, and explicit replay/recovery rows are charged when delivered. This is per-row pricing, not a unique-job subscription. Unchanged incremental observations emit no row events, but the start charge still applies. The Console pricing table is authoritative for the active price. The code never manually charges these synthetic events.
Runs default to 256 MB memory, a 60-second timeout and a $0.05 maximum charge. The allowed memory range is 256 MB to 1 GB. The tested small example fits these limits. Larger boards, many boards, retries or Greenhouse pay enrichment can take longer; review the timeout, input limits and maximum charge in the Console or API before running them. The input row cap and maximum run charge are separate controls.
Public API requests use no purchased proxies. Sustained-load costs and long-term storage have not been benchmarked. Greenhouse enrichment increases request volume; immutable observations, pending state plans and raw provenance increase storage volume. Externally serialize incremental runs and plan retention before sustained use.
Hosted validation on October 15, 2026 (build 0.1.3) verified that six delivered rows matched six native row-billing events. A $0.006 run cap held the six-row bundle without state advancement; recovery delivered and billed all six rows with zero provider requests and cleared pending recovery. These checks cover the small test board, not sustained-load or broad-board performance.