---
title: "Salary Data from Job Sites: Reading Ranges Without Bias"
description: "Glassdoor's salaries endpoint returns p10 to p90 total pay, a currency and a national count. How to read those ranges and the biases of self-reported pay."
url: https://perceptlayer.com/blog/salary-data-job-sites-bias
---

[Blog](https://perceptlayer.com/blog) / [Web data by industry](https://perceptlayer.com/blog/category/web-data)

# Salary Data from Job Sites: Reading Ranges Without Bias

6 Oct 2026 Perceptlayer 7 min read

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.

## Key takeaways

-   The `/v1/glassdoor/salaries` response gives per-employer `total_pay` at p10, p25, p50, p75 and p90, plus `currency` and `pay_period`. It has no sample size on employer rows.
-   Sample size appears in two other places: `estimate.salaries_count` (727,155 in our recording) and `salary_count` on each company from `/v1/glassdoor/companies-search` (4,702 for Stripe, 38 for Stripe OLT).
-   In the recording, Google's p10 total pay (207,909.08) sits above the US-wide p75 (192,868.14). The response doesn't say how employer rows and the national estimate relate, so don't mix them.
-   The recorded Indeed jobs response has no structured pay field, so don't plan a salary pipeline on it without checking the fields first.
-   Each salary call costs 1 credit, and the free tier has 1,000 credits a month.

## 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:

```json
{
  "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](https://perceptlayer.com/apis/glassdoor/salaries) for the full page, or the earlier [Glassdoor API walkthrough](https://perceptlayer.com/blog/glassdoor-salary-data-api) 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](https://perceptlayer.com/apis/glassdoor/companies-search), 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](https://www.bls.gov/oes/oes_emp.htm)). 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](https://www.bls.gov/oes/).

## Can Indeed fill the gap?

Not from the recording we have. The [Indeed jobs endpoint](https://perceptlayer.com/apis/indeed/jobs) 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](https://perceptlayer.com/blog/indeed-jobs-api-json) for the fields and [job posting data for labour-market research](https://perceptlayer.com/blog/job-posting-data-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.

```python
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](https://docs.perceptlayer.com/authentication), and [credits](https://docs.perceptlayer.com/credits) explains billing. For what credits cost, see [pricing](https://perceptlayer.com/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](https://perceptlayer.com/signup) and run the salaries call in the playground on the [Glassdoor API page](https://perceptlayer.com/apis/glassdoor). More jobs data lives under [Jobs APIs](https://perceptlayer.com/verticals/jobs).

## APIs in this post

-   [Glassdoor **1 credit per call** · 6 endpoints](https://perceptlayer.com/apis/glassdoor)
-   [Indeed **1 credit per call** · 1 endpoint](https://perceptlayer.com/apis/indeed)

## 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.

salary data glassdoor indeed compensation jobs

## More from the blog

1.  [Web data by industry](https://perceptlayer.com/blog/category/web-data) 21 Oct 2026 Perceptlayer 8 min read
    
    ## [Job Posting Data for Labour-Market Research](https://perceptlayer.com/blog/job-posting-data-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.  [API guides](https://perceptlayer.com/blog/category/api-guides) 14 Aug 2026 Perceptlayer 8 min read
    
    ## [Glassdoor API: Company Reviews and Salaries as JSON](https://perceptlayer.com/blog/glassdoor-salary-data-api)
    
    Find a Glassdoor employer ID, then pull reviews and salary percentiles as JSON. Real responses, parameters and 1 credit per call.
    
3.  [Web data by industry](https://perceptlayer.com/blog/category/web-data) 30 Sept 2026 Perceptlayer 7 min read
    
    ## [Software Review Data Analysis for Competitive Research](https://perceptlayer.com/blog/software-review-data-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.
