Jobs Feed API - Deduplicated LinkedIn, Indeed & ATS Jobs
Pricing
from $1.00 / 1,000 unique jobs
Jobs Feed API - Deduplicated LinkedIn, Indeed & ATS Jobs
Deduplicated job postings from Greenhouse, Lever, Ashby, LinkedIn (public) and Indeed in one normalized schema, with a delta mode that returns only new, changed and removed jobs between runs - guest pages only, no login.
Pricing
from $1.00 / 1,000 unique jobs
Rating
0.0
(0)
Developer
Sergey Faraday
Maintained by CommunityActor stats
1
Bookmarked
2
Total users
1
Monthly active users
17 days ago
Last modified
Categories
Share
Jobs Feed API - deduplicated LinkedIn, Indeed and ATS jobs (no login)
Not a scraper - a jobs feed. One call, all sources (Greenhouse, Lever, Ashby, Indeed, Glassdoor, LinkedIn public), one normalized schema, deduplicated across sources - and a delta mode that emits and bills only new, changed and (opt-in) removed jobs between runs. This Actor sells state, not rows: schedule it and every morning only what changed is waiting for you.
Status: v0.6 - ATS sources plus three guest sources, ranked by strength of published basis: Indeed (opt-in, read in a real browser - it degrades, never fails the run, when challenged; descriptions from Indeed are snippets), Glassdoor (opt-in, read in a real browser - first search page per query and location inside your
datePostedwindow; narrow the window or add a location for completeness) and LinkedIn public (guest job pages, no login, no accounts - addlinkedin-publicto Sources). Indeed, Glassdoor and LinkedIn run through the built-in residential proxy - no proxy setup needed (bring your own in Advanced to cut costs on heavy workloads). By default LinkedIn runs in list mode: full posting pages are fetched only when you turn ondetailFetch(each page billed asjob-detail-fetch).
Unique jobs across LinkedIn, Indeed & ATS boards
Unique job postings from LinkedIn (public), Indeed and ATS boards (Greenhouse, Lever, Ashby) arrive in one normalized schema, deduplicated across sources. The same job on Greenhouse, LinkedIn and Indeed is one row with seenOn[] - not three charges. Re-runs bill only what changed.
Duplicates are never separate rows. Two listings merge only when they share the same canonical apply URL or ATS requisition (exact), or - in standard mode - when they belong to the same company and have near-identical title and description (strong) - "near-identical description" also covers a listing that contains the other one plus appended boilerplate (the LinkedIn guest page vs the ATS text), and it never holds for two listings in different cities or countries. Look-alikes that do not clear that bar are kept as separate rows and annotated possibleDuplicateOf. Two distinct positions on the same board (multi-location postings) are never merged. LinkedIn and Indeed re-list one posting as a card per city - those cards fold into the ATS row (seenOn lists every card, location.all every city). Glassdoor cards carry only a short snippet, so a Glassdoor card merges with the matching Greenhouse/Lever/Ashby row when title and snippet match closely; otherwise it is kept as its own row and annotated possibleDuplicateOf - expect fewer automatic merges than from LinkedIn, whose cards carry the full text. strict mode merges exact matches only.
Deduplicated job feed - pay per unique job
You pay per unique job, never per row scanned: unique-job for a job new to your delta scope, job-update when a tracked field changes, job-removed (opt-in) when it disappears. Unchanged jobs cost nothing.
What 30 days of monitoring costs
Scenario: watch 3 queries across 50 companies (≈ 500 live postings, ~3 % new and ~2 % changed per day) every day for a month.
| Re-scraping stack (per-row Actors) | Jobs Feed API in delta mode | |
|---|---|---|
| Rows you pay for | 500 rows × 30 days = 15,000 | day 1: 500 new · days 2-30: ~15 new + ~10 updates per day |
| Price per row | $0.0004 - $0.005 (typical LinkedIn/Indeed job scrapers on the Store, Free plan, 2026-08-18¹) | unique-job $0.0014 · job-update $0.0012 · job-removed $0.0020 (Free plan; Bronze/Silver/Gold discounts) |
| Month | $6 - $75: $6 at the cheapest listed price ($0.0004), $10.50 at the median listed price ($0.0007), $75 at the priciest ($0.005) | ≈ $1.7 (day 1 $0.70, then ≈ $0.03/day): 3.5x below the cheapest listing, 6x below the median, 44x below the priciest |
| Plus | your own dedup, your own diff, re-processing 15,000 rows | rows arrive already unique and already classified new / updated / removed |
¹ Store listing prices of the five most-used LinkedIn job scrapers and four Indeed scrapers on the day of writing, apify-default-dataset-item Free-plan tier: $0.0004, $0.0005, $0.0007, $0.002, $0.005 (LinkedIn) and $0.00005 - $0.006 (Indeed). Real delta pair on this Actor (run FAC4Fbr7KcJUOKfay → xhgLzFfBp7WYy2S0D, Discord Greenhouse + LinkedIn, 2026-08-18): day 1 = 50 rows = $0.045, day 2 = 0 rows = $0. The left column uses the five LinkedIn listing prices sampled in this footnote; their median is $0.0007. The table is a model, not a measurement: churn figures are the modeling assumption, not a promise; your REPORT.deltaStats shows the real ones.
One-off bulk is a different case. On "LinkedIn bulk with full details" (thousands of rows, every posting page fetched, once) a per-row LinkedIn scraper is cheaper than this Actor - deliberately. Our case is uniques + state with selective details: score postings from the list first, fetch full pages only for the ones that already look like matches (detailFetch: selective), and pay per state change from day 2 on.
Fair billing
Rows are billed only after they are delivered and committed. A run that fails before its first committed row bills nothing. If a failure occurs later, only rows already delivered and committed remain billed. Failed sources never add charges. Within retained delta state, an unchanged committed job is never billed twice. A crash may under-charge, never intentionally over-charge.
Three row events, mutually exclusive per row: unique-job (a job seen for the first time in your delta scope), job-update (a known job whose title, salary, description, location or employment type changed) and, only when you opt in with trackRemovals, job-removed (a tracked job that disappeared from every source it was seen on). Unchanged jobs cost nothing. Actor start costs nothing. One service event exists: job-detail-fetch, one per full posting page fetched from a guest source - and only when you turned detailFetch on; the default list mode never fires it.
Two lines worth comparing before you pick a scraper:
- Rows your filters rejected are never billed. With
matchProfile.minMatchScore(orearlyApplicantsOnly) set, filtered rows never reach the dataset and never reach your bill - the report'smatchFilteredcounts them. Some per-row scrapers collect and bill every row their crawl touched, whether or not it matched your filters. - No per-run minimum. A run that finds 3 new jobs bills 3 events.
Greenhouse, Lever & Ashby in one API
Greenhouse, Lever and Ashby boards are the default sources and need no keys: press Start with just a query. With no companies, the ATS sources scan the catalog of verified Greenhouse / Lever / Ashby boards bundled with this build (v2026.08.3: 704 boards, ~23K postings; rebuilt weekly from public discovery) - largest boards first, within maxScannedJobs - and keep the postings that match your queries. The catalog is built from public discovery only: the companies you pass in your own runs never enter it. Today's catalog is biased toward large tech boards (catalog in the run report shows the version and how many boards were scanned); set catalogMode: "off" to require explicit companies.
{"companies": [{ "domain": "discord.com" },{ "domain": "ramp.com", "ashbyBoard": "ramp" },{ "domain": "acme.com", "greenhouseBoard": "acme", "leverSite": "acme" }],"queries": ["backend engineer", "data engineer"],"maxItems": 500}
companies- one entry per company. Onlydomainis required; the Actor discovers Greenhouse / Lever / Ashby boards from the company site and, as a fallback, by probing the domain label. PassgreenhouseBoard/leverSite/ashbyBoard(the public board slugs; legacy namegreenhouseTokenstill accepted) to skip discovery - they always win.queries- a keyword filter for career boards: a posting is kept when every word of at least one query appears in its title, or (ATS boards, which return the description at list time) when the query appears as a phrase in the description. Case-insensitive, deterministic; undermaxItems, title matches are emitted first. Not a ranked full-text search. Leave empty to get every posting of the listed companies.
Only new and changed jobs - delta mode
Delta mode makes every later run return only what changed: run 1 emits every job as new, run 2 with the same input emits nothing (unless a job changed) and charges nothing. It needs two storages you own, so that billing state is yours and two runs can never write it at once:
- Key-value store - create one (Console → Storage → Key-value stores → Create, e.g.
jobs-feed-state) and pick it in Delta state store. - Request queue - create an empty one (Storage → Request queues → Create, e.g.
jobs-feed-lock) and pick it in Delta lock queue. One queue per store - never share it. - Open the Actor, fill the input - your queries and/or companies, sources - and pick the store in Delta state store and the queue in Delta lock queue. Optional: Track removals on to receive
removedrows. - Save as Task (top right) → the task's Schedule tab → the hour you want → Save. Schedule it hourly or daily; the choice only changes how often the delta is computed.
That is the whole setup. Tomorrow's dataset contains only what changed - wire it into anything that reads a dataset (n8n / Make / Zapier "Apify → run task → get dataset items"). The run report (REPORT in the run's key-value store) carries meta.origin (SCHEDULER / API / WEB) and deltaStats: { new, updated, known, coalesced, evicted, rebaselined }.
Details worth knowing:
- Four distinct dates per row (
postedAt,earliestPostedAt,firstSeenAt,lastSeenAt), anddatePostedis applied at the source where supported, post-filtered before billing where not. - Delta state is bound to your exact input scope (queries, companies, sources, dedup mode). Changing it stops the run with
DELTA_SCOPE_MISMATCHbefore scraping ($0). Set If the delta store was built for other queries/companies to rebase to intentionally start a new delta history (the first run of the new scope bills as new). - If a second run starts while one is running, it fails before scraping with
DELTA_LOCK_HELD($0). - A job that disappears and comes back after 45 days is a new occurrence (
repostOflinks to the old one) and bills once more. - Delta guarantee is exact within retained state: 50 MB of delta core per generation (≈ 35-40K occurrences at the measured ~1.3 KB each; hard cap 200,000). Beyond that the oldest occurrences are evicted (
deltaStats.evicted) and could re-emit if seen again. Removal summaries have their own 60 MB budget and are evicted first, never the occurrences. - Emission priority is new jobs first, then updates, then removals: under
maxItemsor your spending limit, updates and removals may wait for the next run; they are not lost.
Closed positions - trackRemovals (opt-in)
Set trackRemovals: true (delta mode only) and a job that disappears from every source it was seen on gets one deltaStatus: "removed" row: the last known summary (title, company, apply URL, location, salary, employment type, seenOn; no description), removedAt, daysOpen (time-to-fill proxy: last seen minus first seen) and removalConfidence. Billed once as job-removed.
A removal is disappearance from our observation, never proven closure, and it is judged honestly:
- The missing counter advances only on conclusive scans: the source was healthy and the board or search that originally surfaced the job was enumerated completely (no failed fetch, no
maxScannedJobstruncation, no pagination cap). Failed or truncated scans neither advance nor reset it.removalConfirmRuns(default 2) consecutive conclusive misses are required. removalConfidence: "high"- every source that carried the job is an ATS board (full board reads)."best-effort"- LinkedIn/Indeed, whose search enumeration is capped; in practice their removals are rare, and the first run warnsREMOVALS_BEST_EFFORT_SOURCEwhen such sources are selected. Reliable removals are an ATS-board feature. Glassdoor never contributes removal evidence (windowed first-page reads cannot prove a job disappeared) - and because a removal needs every source that carried the job to have fully enumerated, any job Glassdoor also surfaces stops being eligible for removal reporting while Glassdoor is in your scope, even if its primary source is an ATS board. Turn Glassdoor off in a removals-focused task.- A removed job seen again later is a new occurrence with
repostOfpointing at the closed one (a reopened position), billed asunique-job- not silently revived. - The first run of a fresh state emits no removals; a removal is emitted at most once per occurrence.
- Removal rows keep a compact summary stored separately from the delta core (
snapshotAvailable: true); under heavy scopes that summary may be evicted and the row degrades to title, apply URL andseenOn(snapshotAvailable: false).
trackRemovals is off by default while its false-removal rate is measured on our canary; the default will flip on with 14 days of notice.
Switching from another scraper - seedFromDataset (free)
"I have months of data from another scraper - will your first run re-bill me for jobs I already have?" No. Pick a dataset you already own in Seed delta state from a dataset (your own jobs-feed rows, or a LinkedIn / Indeed job-scraper export - format auto-detected, or set Seed format) on the first run of a fresh delta store: the state is seeded from it - no rows, no events - and that same run emits only the jobs that are actually new to you. Rules: delta mode on, empty state (SEED_STATE_NOT_EMPTY otherwise, $0), ≤ 50,000 items (SEED_TOO_LARGE), rows must parse (≥ 5 % unparseable ⇒ SEED_FORMAT_MISMATCH, $0). Fields your export did not carry (often the description in list exports) are baselined silently on the first live observation - a field appearing for the first time is not a change; only fields present in the seed and different live produce job-update. Duplicates in your export (their data has no cross-source dedup) coalesce on the first live run at no charge. Seeded jobs older than 45 days are not treated as reposts. CSV/URL seeding is not in v1 (only datasets on your account).
Several locations and freshness windows in one run
Several locations in one run: pass queries as objects with their own location and freshness window - "queries": [{ "q": "backend engineer", "location": "Dubai", "datePosted": "7d" }]. The top-level location and datePosted set the default for every query that does not override them.
locationanddatePostedare scope filters ("Dubai", "last 24 hours"): LinkedIn/Indeed searches are narrowed server-side by the platform's own geo and freshness parameters; ATS postings are matched by their location text and posted date.datePosted(24h/3d/7d/14d/30dor days as a number) is also the cost control - without it every run re-scans the full history of every board.- Changing filters changes the delta scope (a
DELTA_SCOPE_MISMATCHguard protects your state); withtrackRemovals, a job that merely leaves the window is never mistaken for a removal.
For job seekers - match profile, early-applicant signal
Job seekers get a match-scored daily feed: set a match profile of your resume keywords and every morning only the new jobs scoring above your threshold arrive - typically $2-4 a month.
The same engine, pointed at your search: a delta feed sends you the 20 new matching jobs every morning, and at these prices that is a consumer subscription, not a data budget - a daily run over your target companies typically bills $2-4 a month (day 1 seeds your scope as unique-jobs, every day after that only what appeared, changed or closed).
Set a match profile and let the Actor score every job against your resume keywords:
{"queries": ["backend engineer"],"matchProfile": {"keywords": [{ "keyword": "TypeScript", "aliases": ["TS"], "weight": 2 },{ "keyword": "Kubernetes", "aliases": ["k8s"] },{ "keyword": "GraphQL" }],"minMatchScore": 60}}
- Every row gets
matchScore(0-100: matched weights over total weights),matchedKeywords,unmatchedKeywordsandmatchBasis. Matching is whole-word and case-insensitive; an alias hit counts as its keyword. matchBasisis the honesty marker:title+snippetmeans the score was computed in list mode and can miss skills that only appear in the description;title+descriptionmeans the full text was scored.- Rows that don't match your profile are never emitted and never billed -
matchFilteredtells you how many were dropped. SetminMatchScore: nullto annotate without filtering. detailFetch: "selective"(withdetailFetchMinScore) makes the expensive part self-targeting: postings are scored provisionally from the list, only the ones that already look like matches get their full page fetched (billedjob-detail-fetch), andminMatchScoreis applied to the final score computed on the full text. Plainly: a fetched page is billed even when the final score then filters the row - you paid to find out. Provisional scoring can miss description-only skills; the threshold trades cost for recall.earlyApplicantsOnly: truekeeps only jobs still showing LinkedIn's "Be among the first N applicants" (the N is on every row asapplicantsHint);easyApplyFlagmarks in-platform applications where visible. Together withjob-updateandjob-removedthis is the freshness-signal line: apply first, re-engage on change, stop when closed.
A ready n8n flow - schedule → delta run with your match profile → jobs with matchScore ≥ 80 to a Telegram/email digest - is in templates/n8n/jobseeker-digest.json.
No login, no cookies - guest pages only
LinkedIn, Indeed and Glassdoor are read exactly as a logged-out visitor reads them: guest pages only, no accounts, no cookies, no session reuse, nothing to configure and nothing of yours to hand over. This is a locked design decision, not a default - there is no input that turns it on.
What that costs you, plainly: fields that exist only behind a login are permanently out of this Actor. Applicant counts beyond the public "Be among the first N applicants" hint, recruiter contacts, skill-match percentages and any other logged-in-only field will never appear in a row, on any plan. If your use case needs them, this is not the tool.
Sources
Greenhouse, Lever and Ashby are read through their public board APIs. Indeed and Glassdoor are read through a real browser with an ordinary browser fingerprint (guest pages, no login - the sites serve them as they serve any visitor); LinkedIn through its public guest job pages. Every run reports what each source actually returned (sourceHealth) and where limits were hit (capsHit). You never wonder why you got fewer rows.
Live source health: gist.github.com/sergey-faraday/d432228fab307fbf56a093b861165b65 - 7-day success rate, latency, errors and last incident per source, measured by our own canary tasks (aggregated counts only; never your runs or your queries) and refreshed daily. Bad weeks are shown as they are.
| Source | How | Notes |
|---|---|---|
| Greenhouse / Lever / Ashby | public board APIs | full descriptions, structured salary where the ATS has it; discovered from the company domain or given as board slugs |
| Indeed (opt-in) | guest search page, read in a real browser | built-in residential proxy; reads the guest result page per query (newest first, ~45 postings); page 2 and beyond are login-gated on Indeed and are never fetched (no accounts, ever); snippet-only descriptions; easyApplyFlag where visible; explicit vs estimated salary flagged (salary.estimated) |
| Glassdoor (opt-in) | guest search page, first page only, read in a real browser | reads up to 30 jobs per query and location from inside your datePosted window (30 days when unset, and capped at 30); Glassdoor's pagination and detail pages are closed to crawlers and are never requested; snippet-only descriptions; easyApplyFlag; salary as a p10-p90 range with salary.estimated when not employer-provided; contributes no removal signal |
| LinkedIn (public) | guest job search (+ guest job page with detailFetch) | no login, no cookies; built-in residential proxy; list mode by default - descriptions and applicantsHint come from posting pages fetched under detailFetch (billed job-detail-fetch); the external apply link is not exposed on guest pages, so applyUrl is the LinkedIn job page; the same job on the company's ATS board is merged into one row (seenOn lists both) |
Browser sources need memory and a moment: run with at least 2,048 MB when Indeed or Glassdoor is selected (the run stops before scraping with GUEST_NEEDS_MEMORY otherwise), and the first guest run on a cold worker adds about 25-30 seconds of startup. ATS-only runs never start the browser.
Each guest source has an operator kill switch (JOBS_FEED_DISABLE_LINKEDIN_PUBLIC, JOBS_FEED_DISABLE_INDEED, JOBS_FEED_DISABLE_GLASSDOOR, set in Console, not in your input). A source that is switched off shows as disabled in sourceHealth and costs nothing.
proxyConfiguration (in Advanced) is optional: guest sources use the built-in Apify residential proxy by default and ATS boards go direct. Pass your own groups or proxyUrls to override it, or { "useApifyProxy": false } with no URLs to force direct connections.
Use with n8n
Two ready n8n templates ship in templates/n8n/: daily-jobs-feed.json routes new rows to Slack, updated to a Sheet and removed to a CRM webhook; jobseeker-digest.json runs a delta run with your match profile on a schedule and sends jobs with matchScore ≥ 80 to a Telegram/email digest. Both use the standard "Apify → run task → get dataset items" nodes, so Make and Zapier work the same way.
Use with Claude and AI agents (MCP)
Claude and any MCP client can call this Actor through Apify's MCP server (apify/actors-mcp-server): the call-actor tool with the Actor id sergeyfaraday/jobs-feed-api and the input below. Typed input, output, dataset and key-value-store schemas ship with the build, and the run report is a single JSON record (REPORT) in the run's key-value store, so an agent can read the result without parsing prose.
{"queries": [{ "q": "backend engineer", "location": "Berlin", "datePosted": "7d" }],"sources": ["greenhouse", "lever", "ashby", "linkedin-public"],"maxItems": 50}
Current limits
- Bulk on a single board: for a one-off pull of thousands of rows from one board, a scraper priced per 1,000 raw rows costs less than this Actor. Our unit is a unique job plus its state over time, not a row.
- Support: issues answered within 24 hours on business days. That is our commitment, and other developers on the Store answer faster.
- No logged-in LinkedIn depth: applicant counts, skill matches and recruiter fields that exist only behind a login are permanently out (see No login, no cookies).
- Corpus size: our reach is the bundled catalog (704 verified boards) plus the companies you pass plus guest searches. Aggregators that index six-figure numbers of career sites cover more companies than we do; our answer is the unit of billing and the state between runs, not breadth.
- Track record: this is a new listing. No review history yet - the live source-health gist above is the only public record of how it behaves, and it is published in full, bad weeks included.
LinkedIn jobs API pricing - what a row costs
A unique job costs $0.0014, a change to a known job $0.0012 and a closed position $0.0020 on the Free plan - you pay per event, never per row scanned; duplicates and unchanged jobs are free. Bronze / Silver / Gold plans get lower rates:
| Event | Fires when | Free plan |
|---|---|---|
unique-job | a job is delivered for the first time in your delta scope | $0.0014 |
job-update | a tracked field of a known job changed (title, salary, description, location, employment type) | $0.0012 |
job-removed | opt-in trackRemovals: a tracked job disappeared from every source it was seen on | $0.0020 |
job-detail-fetch | one full posting page fetched from a guest source, only with detailFetch on | $0.0020 |
Unchanged jobs and Actor start cost nothing. The console prefill caps a first click at 200 rows (≈ $0.28 at the Free-plan price); the API default is 1000. CSV and Excel export are verified against a live run on every release, starting with 0.6.0 (tools/deploy/csv-export-smoke.js).
Issues: please open a GitHub issue; response within 24 hours on business days. This Actor is not affiliated with Greenhouse, Lever, Ashby, LinkedIn, Indeed or Glassdoor.
Output
Dataset: one row per unique job occurrence (deltaStatus: new | updated), see the Jobs view. Choose Output view = LinkedIn-compatible field names to get jobUrl, companyName, location, postedAt, descriptionText instead.
Every posting is normalized: title, company (name + registrable domain), apply URL, description, location (city/region/country + all locations for multi-location postings), work mode (remote/hybrid/onsite), salary (min/max/currency/period; parsed from the pay-transparency text when the ATS has no structured field), employment type, posted date, department, and seenOn[] with every source that carried it.
With a matchProfile, every row also carries matchScore, matchedKeywords, unmatchedKeywords and matchBasis; guest-source rows carry applicantsHint / easyApplyFlag where the signals are public.
Run report (REPORT in the run's key-value store): sourceHealth[] per source (ok | degraded | failed | disabled, listed/parsed counts, errors), duplicatesReport, deltaStats, matchFiltered, costTelemetry (capsHit, underchargedRows, detailFetchesCharged), batchManifests[], feedVersion, identityVersion, contentHashVersion, generation. sourceHealth[].notes[] (Glassdoor) lists non-error observations - GLASSDOOR_SLICE_TRUNCATED (a window held more than 30 jobs; narrow datePosted or location) and GLASSDOOR_LOCATION_UNMAPPED (searched the United States and filtered by the card location).
Limits and errors
Every run is bounded by caps you set: maxItems up to 50,000 new and updated rows, maxScannedJobs up to 100,000, and any cap that is hit is named in capsHit.
maxItems ≤ 50,000 (new + updated), maxScannedJobs ≤ 100,000, maxDetailFetches ≤ 50,000, ≤ 20 queries, ≤ 500 companies. When a cap is hit it is listed in capsHit. maxDetailFetches caps posting-page fetches when detailFetch is on (each successful fetch is billed job-detail-fetch; the cap also respects your maximum total charge). In the default list mode LinkedIn cards have no description and cannot be text-merged with the ATS listing of the same job - expect LinkedIn-only rows next to their ATS twins unless you enable detailFetch. Errors that stop a run before scraping (INPUT_INVALID, DELTA_LOCK_HELD, DELTA_LOCK_QUEUE_INVALID, DELTA_SCOPE_MISMATCH, DELTA_STORE_UNREACHABLE, ALL_SOURCES_FAILED) always bill $0. onSourceError: fail aborts before any row is written when any source fails ($0); the default skip continues with the other sources and reports the failure in sourceHealth.