Blog / Web data by industry

Salary Data from Job Sites: Reading Ranges Without Bias

Salary data from job sites is usable if you treat it as a probably self-reported distribution, not a measurement. Read the percentile range, not a single average. Check the currency and pay period. Check how many reports sit behind the number. Then compare it with a source that doesn't rely on volunteers. This article walks through a recorded Glassdoor salaries response and lists the biases to correct for.

What does the recorded salaries response contain?

A call to GET /v1/glassdoor/salaries?job_title=software+engineer returns a list of employer rows and one national estimate block. Each employer row carries currency, pay_period, base_pay_mean, additional_pay_mean and a five-point total_pay distribution. The estimate adds location, confidence and salaries_count.

Here is the shape, trimmed from the recording:

{
  "success": true,
  "data": {
    "job_title": "software engineer",
    "salaries": [
      {
        "job_title": "Software Engineer",
        "employer": { "id": 9079, "name": "Google", "rating": 4.4 },
        "currency": "USD",
        "pay_period": "ANNUAL",
        "base_pay_mean": 184722.83,
        "additional_pay_mean": 126972.82,
        "total_pay": { "p10": 207909.08, "p50": 311695.65, "p90": 486503.67 }
      }
    ],
    "total_results": 104377,
    "estimate": {
      "currency": "USD",
      "pay_period": "ANNUAL",
      "location": "United States",
      "confidence": "CONFIDENT",
      "salaries_count": 727155,
      "total_pay": { "p10": 99464.84, "p50": 152101.59, "p90": 237195.82 }
    }
  }
}

The endpoint takes job_title (required), plus optional employer_id, page and limit. See the Glassdoor salaries endpoint for the full page, or the earlier Glassdoor API walkthrough for reviews and salaries together.

How wide are the ranges, and what do they say?

The recorded p90 is roughly 2.0 to 2.4 times the p10 in every row we have. A range that wide is the honest answer to "what does this job pay", and a single midpoint hides it. Compare spread and level separately.

Row p10 p50 p90 p90 / p10
Google, total pay 207,909.08 311,695.65 486,503.67 2.34
Microsoft, total pay 153,947.14 218,040.69 318,811.92 2.07
US estimate, total pay 99,464.84 152,101.59 237,195.82 2.38
US estimate, base pay 79,463.04 117,615.72 174,086.68 2.19

All values are USD and ANNUAL. Two readings follow. First, the gap between US total pay and base pay at the median is 34,485.87 (152,101.59 minus 117,615.72), so bonus and stock are a real share of what people report. Second, an employer row doesn't look like a slice of the national row. Google's p10 is above the national p75, and the response doesn't say how the two relate, so don't compare them directly.

One more detail worth checking in your own data: for both employers, base_pay_mean plus additional_pay_mean equals the total_pay.p50 exactly (184,722.83 + 126,972.82 = 311,695.65 for Google). The response doesn't say how the median is derived, so verify this on the rows you use before treating the p50 as an independent observation.

Which fields tell you how much to trust a number?

Trust depends on sample size, and the response spreads it across endpoints. The employer rows have no count, the estimate has salaries_count and a confidence label, and the company search has salary_count per employer. Pull the count before you quote a range.

Field Where Recorded value Use it for
estimate.salaries_count /v1/glassdoor/salaries 727,155 Size of the national pool for the title
estimate.confidence /v1/glassdoor/salaries CONFIDENT A label to filter on; we have no documented rule for it
total_results /v1/glassdoor/salaries 104,377 Employer rows for the title, not reports
salary_count /v1/glassdoor/companies-search 4,702 (Stripe), 38 (Stripe OLT) A company-level count; it sits on the company record, not on a title

The last row is the trap. salary_count appears on the company record, with no job title attached, so it looks company-wide: 4,702 at Stripe probably doesn't mean 4,702 software engineer salaries. The field isn't documented, so check it against your own pulls. Stripe OLT, with 38, shouldn't get a published percentile range at all. Look up an employer_id with the companies search endpoint, then pass it to salaries.

Also watch units. Every recorded row says USD and ANNUAL, but the fields exist because other values can occur. Reject any row whose currency or pay_period you didn't expect rather than converting it silently.

What biases does self-reported pay carry?

Self-reported pay is skewed by who reports, what they report and when. The response doesn't document the inputs behind any one number, so we can't say how much of it is user-submitted. What the response does give is a count of the salaries behind the national figure (salaries_count), so treat it as a distribution of individual reports, not a payroll census. Treat the list below as a checklist, not a footnote. None of it can be removed from the JSON; you correct for it by comparison.

The middle column says whether our two-employer recording shows anything. The rest are general risks of self-reported sources that we haven't measured here.

Bias Evidence in the recording What to do
Total-pay definitions Shown: additional_pay_mean is 126,972.82 for Google and 56,847.98 for Microsoft, so bonus and stock vary a lot by employer Compare base and total separately
Seniority blur Consistent with it: p90/p10 is 2.07 to 2.38 for one title Filter by employer and title, check the spread
Location mixing Shown: the estimate says United States, so a city premium is flattened Don't apply a national range to one city
Self-selection General risk; not testable from the JSON Compare with an employer-reported source
Rounding and recall General risk; not testable from the JSON Use ranges, not exact values
Employer mix General risk; the recording has only two employer rows, so it can't show who dominates Don't read the employer list as the market
Staleness General risk; the rows carry no report dates Re-pull on a schedule and diff the percentiles
Selective sharing General risk; nothing in the response says whether reports are verified Cross-check outliers before using them

The additional_pay_mean gap tests the definition problem. The wide p90/p10 ratio fits seniority blur: if one title were one job, you'd expect a narrower range, though the ratio alone doesn't prove it. The table doesn't prove any of the biases for this sample; it tells you where to look.

What should you compare it with?

Compare job-site pay with a survey that employers report to. The US Bureau of Labor Statistics builds its OEWS estimates from employer responses: a sample of about 1.1 million establishments collected over three years (BLS, Occupational Employment and Wage Statistics, 2025). That is a different method with its own limits, such as a May reference date and a lag, but it doesn't depend on volunteers.

The practical rule: use job-site data for the shape and the employer-level detail, and use a survey for the level. If your Glassdoor national p50 for a title sits far from the OEWS median for the closest occupation, suspect definitions first (total versus base pay) and mix second. For official methodology and tables, see the BLS OEWS program.

Can Indeed fill the gap?

Not from the recording we have. The Indeed jobs endpoint takes query, location, country, job_type and more, and returns postings. The recorded job objects carry id, title, company, location, description_html, attributes, date_published and apply_url. None of those is a structured salary field.

So treat Indeed as a way to count and describe postings, not to read pay. If a posting states a range, it would be in the text, and you'd have to extract it yourself and handle currency, period and wording. A posted range is also an employer's statement of intent, a different thing from what employees report earning. See Indeed Jobs API for the fields and job posting data for labour-market research for what postings can show.

How do you pull and sanity-check ranges in code?

Pull the rows, assert the units, compute the spread, and refuse to report ranges without a count. This script runs as written once API_KEY is set, and costs 1 credit per call.

import os
import requests

BASE = "https://api.perceptlayer.com"
HEADERS = {"X-API-Key": os.environ["API_KEY"]}

def salary_ranges(job_title, employer_id=None):
    params = {"job_title": job_title}
    if employer_id:
        params["employer_id"] = employer_id
    r = requests.get(f"{BASE}/v1/glassdoor/salaries", headers=HEADERS, params=params, timeout=30)
    r.raise_for_status()
    data = r.json()["data"]

    est = data.get("estimate") or {}
    print("estimate:", est.get("location"), est.get("confidence"), est.get("salaries_count"))

    for row in data["salaries"]:
        if row["currency"] != "USD" or row["pay_period"] != "ANNUAL":
            print("skip unexpected units:", row["employer"]["name"])
            continue
        pay = row["total_pay"]
        print(
            row["employer"]["name"],
            round(pay["p10"]), round(pay["p50"]), round(pay["p90"]),
            "spread", round(pay["p90"] / pay["p10"], 2),
        )

salary_ranges("software engineer")

Add a second call to /v1/glassdoor/companies-search for salary_count on any employer you intend to publish. A run over 100 job titles, one page each, costs 100 × 1 credit = 100 credits, well inside the 1,000 free credits a month. Walking all 10,438 pages for one title would cost 10,438 credits, which is rarely worth it; the first page plus the estimate block is usually enough. Authentication is covered in the authentication docs, and credits explains billing. For what credits cost, see pricing.

Data is collected from publicly available sources and returned as found at the time of the call. Re-run the same query later and the percentiles can move.

To try it on your own job titles, get a free API key and run the salaries call in the playground on the Glassdoor API page. More jobs data lives under Jobs APIs.

APIs in this post

Frequently asked questions

How accurate is salary data from job sites?

It is accurate as a description of what was reported, and unverified as a description of what everyone earns. Percentile ranges are more reliable than means. Check the report count, the currency and pay period, and compare the level with an employer-reported survey such as the BLS OEWS before relying on it.

Does the Glassdoor salaries endpoint return a sample size?

Not on the employer rows. The national estimate block returns salaries_count (727,155 in our recording), and /v1/glassdoor/companies-search returns a company-level salary_count. The employer row for a title has no count, so don't publish thin ranges without checking the company-level figure.

Can I get salary data from Indeed?

The recorded response from /v1/indeed/jobs has no structured salary field. Its fields are the job id, title, company, location, description_html, attributes, date_published and apply_url. Any pay stated in a posting would have to be extracted from the text yourself, and it reflects what the employer offers.

How much does it cost to pull salary ranges?

Each call to /v1/glassdoor/salaries costs 1 credit, and failed calls cost nothing. Looking up 100 job titles at one page each is 100 × 1 credit = 100 credits. Every account gets 1,000 free credits a month after verifying an email address, with no credit card.

  1. Job Posting Data for Labour-Market Research

    Which Indeed posting fields support trend analysis, where postings mislead versus hires, and a weekly collection plan that costs 3,000 credits a month.

  2. Glassdoor API: Company Reviews and Salaries as JSON

    Find a Glassdoor employer ID, then pull reviews and salary percentiles as JSON. Real responses, parameters and 1 credit per call.

  3. Software Review Data Analysis for Competitive Research

    What Capterra, Trustpilot and Google Play reviews can and can't tell you about a competitor, which fields to keep, the sampling bias, and a worked rating tally.

Start with 1,000 free credits

No password to set. We email you a sign-in link, and your key works on every API.

No credit card. 1,000 free credits a month.