Build a live salary dataset from job postings across 6 boards
Last updated
Salary market research from job postings starts with structured pay fields, and the Datapika job board scraper returns four on every row that states pay: salary_min, salary_max, salary_currency and salary_interval, plus a salary_source flag that says whether the board supplied the figure or it was parsed from the description. Switch on annual normalization and hourly rates are multiplied by 2,080 and monthly pay by 12, so contract and salaried roles share one column. One run sweeps LinkedIn, Indeed, Glassdoor, ZipRecruiter, Naukri and Bayt, deduplicates across boards, and costs $0.005 per job, so a 1,000-row study is about $5.
Which salary fields does a scraped job posting include?
Five pay fields sit on each row under the same names, whichever board produced it. salary_min and salary_max hold the numeric range as the posting states it, salary_currency holds the currency code such as USD or GBP, and salary_interval records the original pay period as yearly, monthly or hourly. The fifth field, salary_source, is the one to read first: direct_data means the board exposed the figure as structured data, while description means the number was extracted from the posting text. Structured values are the safer input for a median; parsed values catch postings that only mention pay in a paragraph, but they deserve a spot check before they enter a benchmark. Around the pay fields sit the segmentation columns that explain variance: job_type, is_remote, location, job_level on LinkedIn, and company_num_employees, company_revenue, company_industry and company_rating where the board publishes an employer profile. The sample Indeed row in the listing shows the shape: 120,000 to 185,000 USD yearly from direct_data, with a 3.9 employer rating and 21,432 reviews on the same row.
- salary_min and salary_max: numeric bounds exactly as the posting states them, null when no pay is published
- salary_currency: the currency code, so UK, Indian and Gulf rows never blend into USD averages by accident
- salary_interval: yearly, monthly or hourly as published, which tells you which rows need converting
- salary_source: direct_data from the board or description parsed from text, your first quality filter
- Segmentation on the same row: job_type, is_remote, job_level, employer size, revenue, industry and rating
- Descriptions ship as Markdown or HTML, so you can re-parse pay language yourself when a posting is ambiguous
How do you normalize hourly, monthly and yearly salaries into one column?
Postings quote pay in whatever unit the employer typed, and an hourly contract sitting next to an annual salaried range breaks any median you compute. The scraper has one input for this, enforceAnnualSalary. When it is on, hourly figures are multiplied by 2,080 (40 hours across 52 weeks) and monthly figures by 12, so a 45 per hour contract lands at 93,600 and a 7,500 per month role at 90,000, in the same column as a posted 95,000 salary. Leave it off when hourly and salaried markets should be studied separately, for example nursing or trades roles where hourly is the native unit; salary_interval then tells your own code which rows to convert. Normalization never converts currency. A run with countryIndeed set to uk returns GBP rows and a Naukri sweep returns Indian postings, so group by salary_currency before ranking anything, or apply your own exchange rates at analysis time. Keep salary_source in view as well: a description-parsed hourly number multiplied by 2,080 amplifies any parsing error by the same factor.
- enforceAnnualSalary: hourly x 2,080, monthly x 12, yearly left unchanged
- 45 per hour becomes 93,600 a year; 7,500 per month becomes 90,000
- Off by default, so hourly-native markets keep their original unit unless you ask
- Currency is never converted: group by salary_currency or apply your own rates
- Parsed hourly values are the rows to spot check, because the 2,080 multiplier scales any error
Which job boards publish salary data, and what does each one add?
Salary coverage depends on the board and on the employer, so the practical rule is to sweep every board that serves your market and let deduplication merge the overlaps; per-board detail pages are listed at /scrape. Indeed is the most reliable source and carries the richest employer profile, including employee count, revenue band and company country, with countryIndeed choosing the national site. Glassdoor uses the same countryIndeed setting for country targeting and is fetched through a real browser, so its rows arrive after Indeed's. LinkedIn adds job_level for seniority cuts but rate-limits at roughly 100 results per IP, so keep residential proxies on and enable linkedinFetchDescription when you need description-parsed pay. ZipRecruiter covers the US and Canada only. Naukri returns experience_range, skills and company_rating, which makes it the board to use for pay-by-experience curves in India, while Bayt covers the Middle East and accepts only the search term, with no hours or job type filters. Google Jobs and BDJobs stay selectable but currently return no rows.
- Indeed: most reliable board, employer size, revenue and country on the row, countryIndeed picks the national site
- Glassdoor: same countryIndeed setting as Indeed, browser-fetched, rows land in 1 to 3 minutes
- LinkedIn: job_level for seniority, about 100 results per IP, residential proxy recommended
- ZipRecruiter: US and Canada postings only
- Naukri: experience_range, skills and company_rating for India pay curves
- Bayt: Middle East coverage, search term only, no hoursOld or jobType filters
What does a salary benchmarking workflow look like with scraped postings?
A repeatable benchmark is a saved input plus a notebook. Start with searchTerms holding up to 5 related titles, so one run covers the whole job family and every row carries matched_search_term to show which query found it. Set location, pick the boards for your market, raise maxResults toward its cap of 100 per board per term, and set hoursOld to 168 so the sample reflects the current week. Deduplication across boards happens before rows are written, so a job listed on three boards contributes one observation. Export CSV or Excel from the Output tab, or pull the dataset by API into pandas. In the notebook, drop rows where salary_min is null, split by salary_currency, then compute the median, p25 and p75 of the midpoint by title, location and company_employees_label. A single run tops out at 4,000 rows (100 results x 8 boards x 5 terms), so paginate with offset or split by city when you need more. Schedule the saved input weekly and append each run keyed on id and scraped_at for a time series.
- Up to 5 searchTerms per run, each row tagged with matched_search_term
- maxResults up to 100 per board per term; hoursOld 168 keeps the sample to the current week
- One observation per job: cross-board duplicates are merged before rows land
- Export CSV, Excel or JSON, or fetch the rows over the Apify API
- Compute median, p25 and p75 of the midpoint by title, location and employer size
- Append weekly runs keyed on id and scraped_at for a trend line
What filter and coverage limits affect a salary sample?
Two board-side restrictions shape sample design. Indeed cannot combine hoursOld with jobType, isRemote or easyApply, and LinkedIn cannot combine hoursOld with easyApply, so a recency-filtered pull of remote-only Indeed roles takes two steps: pull by hours, then filter is_remote in your own code. Bayt ignores everything except the search term. Expect fewer rows than maxResults when a board has fewer matches or duplicates were removed; that is coverage working, not a failure. Speed also differs by board: Indeed and LinkedIn rows land 5 to 20 seconds after start, while Glassdoor, ZipRecruiter, Bayt and Naukri go through a real browser and take 1 to 3 minutes, streaming into the dataset as each board finishes. For the sample itself, treat a posted range as an advertised band, not a paid salary: it skews toward roles where pay transparency laws or competitive markets force disclosure. Reporting the share of rows with any salary alongside the median keeps the benchmark honest, and salary_source lets you publish a structured-only figure next to the broader one.
- Indeed: hoursOld cannot pair with jobType, isRemote or easyApply; filter those columns after the pull
- LinkedIn: hoursOld cannot pair with easyApply
- Bayt: search term only, so apply recency and type filters in your notebook
- Indeed and LinkedIn rows arrive in 5 to 20 seconds; browser-fetched boards take 1 to 3 minutes
- Report the share of rows with published pay next to every median
| Field | Where it is populated | What it decides |
|---|---|---|
| salary_min, salary_max | Any of the 6 active boards when the posting states pay, or when a figure can be parsed from the description; null otherwise | Midpoint, median and p25/p75 per title; drop null rows before aggregating |
| salary_currency | Every row with pay; follows the board's market (countryIndeed for Indeed and Glassdoor, India on Naukri, US and Canada on ZipRecruiter) | Group by currency first; never rank across currencies without your own rates |
| salary_interval | Every row with pay: yearly, monthly or hourly | Whether to enable enforceAnnualSalary or analyze hourly markets separately |
| enforceAnnualSalary (input) | Applies to every board in the run: hourly x 2,080, monthly x 12 | One annual column for cross-posting comparison |
| salary_source | Every row with pay: direct_data or description | Weight structured values; spot check parsed ones before publishing |
| company_num_employees, company_revenue | Employer profile boards, Indeed in particular, with labels such as 1001-5000 and $1B+ | Segment pay by employer size and revenue band |
| company_rating, company_reviews_count | Indeed employer profiles (the sample row carries 3.9 and 21,432 reviews) and Naukri | Test whether higher-rated employers advertise higher bands |
| job_level | Pay by seniority tier | |
| experience_range, skills | Naukri | Pay by years of experience and skill demand in India |
| is_remote, location, job_type | All boards where the posting states them; Bayt accepts the search term only | Remote premium or discount, city comparisons, contract versus full time |
| date_posted, scraped_at, hoursOld (input) | All boards; hoursOld is not applied on Bayt | Recency window and the keys for a time series |
How to do it
- 1.Open the Datapika job board scraper on Apify, enter up to 5 searchTerms for the job family, set location and countryIndeed, and select the boards that serve your market.
- 2.Set hoursOld to 168, raise maxResults to 100, and switch on enforceAnnualSalary if you want one annual column; leave it off to study hourly markets in their native unit.
- 3.Run it, watch Indeed and LinkedIn rows stream in within 5 to 20 seconds, then export CSV or Excel or pull the dataset by API.
- 4.In your notebook, drop rows without salary_min, split by salary_currency, filter on salary_source, and compute medians and percentiles by title, location and employer size; schedule the same input weekly for a trend line.
Questions, answered
Does Glassdoor have an official API?
Not one you can apply for today. Glassdoor closed public access to its developer API years ago (Zuplo on dev.to dates it to 2021, JobsPipe to 2022), and the help page that once held the API docs now describes Glassdoor Customer Insights, a subscription product for HR teams rather than a developer job-data API. Indeed retired its Publisher API too, and its remaining partner APIs push jobs in rather than read them out. Reading the public search pages is the practical route to salary rows.
How do I pull salary data from several job boards in one run?
Run one Datapika sweep with your target titles in searchTerms and the boards for your market in sites. Each returned row carries salary_min, salary_max, salary_currency, salary_interval and salary_source where pay is published, plus employer size, revenue and rating for segmentation. Duplicates are merged across boards before rows are written, so the same posting never counts twice in your median.
Can I compare an hourly contract rate with an annual salary in the same dataset?
Yes. Enable enforceAnnualSalary and every hourly figure is multiplied by 2,080 and every monthly figure by 12, so all three intervals land in one annual column. The original salary_interval still tells you which rows were converted. Currency is not converted, so split by salary_currency before comparing a London row with a New York row.
What does salary_source mean, and should I filter on it?
salary_source is direct_data when the board exposed the pay range as structured data and description when the number was parsed from the posting text. For a published benchmark, compute one figure on direct_data rows only and a second on all rows with pay; if they diverge, spot check the parsed rows. Parsed hourly values matter most, since annual normalization multiplies any mistake by 2,080.
How much does a 1,000-row salary dataset cost?
$5 per 1,000 rows, billed only for jobs that actually land in your dataset, so a run that finds nothing costs nothing beyond the platform start fee. A default run of 6 boards at 20 results each returns at most 120 rows. The scraper has 2,471 users, 381 of them active in the last 30 days, 27,458 runs and a 5.0 rating from 3 reviews (Apify, August 29, 2026).
Which countries can I benchmark salaries in?
Indeed and Glassdoor take a countryIndeed code covering usa, uk, canada, australia, germany, france, india, singapore, uae and most other markets. LinkedIn is global, ZipRecruiter covers the US and Canada, Naukri covers India with experience ranges and skills, and Bayt covers the Middle East. Run one country per sweep so salary_currency stays uniform, then combine the exports with your own exchange rates.
Keep reading
Scrape guides
Scrape Glassdoor jobs with salary bands, no API key
Scrape guides
Naukri job scraper: skills, experience, INR salary fields
Comparisons
Indeed API alternatives for job data (2026)
Use cases
Jobs posted in the last 24 hours: filter, verify, schedule
Use cases
Competitor hiring tracker: weekly scans of named companies