# Glassdoor Jobs Scraper - Pay and Ratings (`usestring/glassdoor-jobs`) Actor

Scrape Glassdoor job listings by keyword and location. Each row carries the title, company, employer rating, pay as numbers with its period and currency next to the range Glassdoor displays, easy-apply and sponsored flags, a description snippet, days since posting and the apply URL.

- **URL**: https://apify.com/usestring/glassdoor-jobs.md
- **Developed by:** [String](https://apify.com/usestring) (community)
- **Categories:** Jobs, Business
- **Features:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.25 / 1,000 results

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

This Actor collects job listings from Glassdoor search. You give it job titles or keywords and one
location; it returns each listing with the employer's Glassdoor rating, the pay range as numbers
rather than a display string, how many days ago the job went up and a link to apply.

### What does the Glassdoor Jobs Scraper do?

Glassdoor's job search is a Next.js App Router page, so its state arrives as React flight chunks
pushed onto `self.__next_f` in pieces. The Actor concatenates those chunks back into the stream the
page was serialised from and reads `searchResultsData.jobListings` out of it, which is the same
object the site renders from.

One field is not in that payload. Glassdoor keeps pay as p10 and p90 integers and builds the
displayed range in the browser from a translation template, so the string a human sees exists only
in the markup. The Actor picks it up from the card's own `id="job-salary-<listingId>"` element and
joins it back by listing ID, never by card order, so no listing can inherit its neighbour's pay.

- Pay arrives twice on purpose: `salaryMin` and `salaryMax` as numbers, and `salaryEstimate` as the
  string Glassdoor shows.
- `salarySource` keeps Glassdoor's own label, so an employer-stated range and a Glassdoor estimate
  stay distinguishable instead of being flattened together.
- `employerRating` comes from the same record as the job, so pay and reputation land on one row.
- Free-text locations work. The Actor resolves `"New York, NY"` through Glassdoor's own location
  autocomplete before searching, so you never have to know Glassdoor's internal location IDs.
- `sponsored` marks paid placements, which is what you filter on when a market count has to be
  honest.

### What data does it extract?

| Field | Type | Description |
| --- | --- | --- |
| `jobId` | string | Glassdoor's listing ID. Stable, so use it as the de-duplication key |
| `title` | string | Job title as listed |
| `company` | string | Employer named on the search row |
| `employerId` | number | Glassdoor's employer ID, the same across that employer's listings |
| `employerRating` | number | Overall Glassdoor rating for the employer, out of 5 |
| `salaryEstimate` | string | The range as Glassdoor prints it on the card, e.g. `"$136K - $184K"` |
| `salaryMin` | number | Low end of the range as a number, e.g. `136000` |
| `salaryMax` | number | High end of the range as a number |
| `salaryPeriod` | string | What the pay is quoted over, e.g. `"ANNUAL"` |
| `salaryCurrency` | string | Currency of `salaryMin` and `salaryMax`, e.g. `"USD"` |
| `salarySource` | string | Where the figure came from, e.g. `"EMPLOYER_PROVIDED"` |
| `location` | string | Where Glassdoor places the job |
| `ageInDays` | number | Whole days since posting, exactly as Glassdoor states it |
| `postedAt` | string | ISO 8601 timestamp derived from `ageInDays`. Accurate to the day |
| `easyApply` | boolean | Whether the listing takes Glassdoor's one-click apply |
| `sponsored` | boolean | Whether the listing is a paid placement |
| `descriptionSnippet` | string | The first description fragment on the card, not the full text |
| `jobUrl` | string | Direct link to the Glassdoor posting |
| `query` | string | The search term that produced the row |
| `searchLocation` | string | The location Glassdoor resolved your input to |
| `sourceUrl` | string | The exact search URL the row came from |
| `collectedAt` | string | ISO 8601 timestamp of the run |

Anything Glassdoor did not supply is `null`. Nothing is filled in by inference.

### Why scrape Glassdoor?

- **Compensation benchmarking that skips the cleanup.** `salaryMin`, `salaryMax`, `salaryPeriod` and
  `salaryCurrency` go straight into a spreadsheet or a model. No regex over `"$136K - $184K"`.
- **Employer-brand work.** `employerRating` next to what a company is currently hiring for shows
  which teams are growing at companies whose staff rate them badly, which is where candidates are
  reachable.
- **Separating advertised pay from estimated pay.** Filter on `salarySource` to build a dataset of
  ranges employers actually committed to.
- **Competitor hiring signals by city.** One location per run, several roles per run, on a schedule.
- **Honest market counts.** Drop rows where `sponsored` is true before you count openings.

### How to use it

1. Open the Actor on Apify Store and click **Try for free**.
2. Enter your **Search terms**, one per line, up to 50: `"software engineer"`,
   `"product manager"`, `"nurse practitioner"`.
3. Set **Location** to a city, state or country, for example `"New York, NY"` or
   `"United Kingdom"`. It defaults to `"United States"`. Every term in the run uses this one
   location.
4. Click **Start**. The run resolves the location first, then runs the searches in parallel.
5. Export from the **Dataset** tab as JSON, CSV or Excel, or read it through the API.
6. For a time series, save the input as a task, schedule it, and join runs on `jobId`.

### Input

| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `queries` | string\[] | required | Job titles or keywords, 1 to 50. Duplicates collapse to one fetch |
| `location` | string | `"United States"` | City, state or country. One location per run |
| `maxItems` | integer | 1000 | Cap on dataset rows, 1 to 50000. Free plans stop earlier, see below |
| `concurrency` | integer | 5 | Searches fetched in parallel, 1 to 10 |

```json
{
  "queries": ["software engineer", "product manager"],
  "location": "New York, NY",
  "maxItems": 500,
  "concurrency": 5
}
```

Resolving the location costs one extra request at the start of the run. If Glassdoor cannot match
what you typed, the run fails immediately instead of quietly searching the whole country.

### Output

One row per listing. This is the shape, with illustrative values rather than a captured run.

```json
{
  "jobId": "1009472283810",
  "title": "Senior Data Engineer",
  "company": "Halden Health",
  "employerId": 918274,
  "employerRating": 3.9,
  "salaryEstimate": "$136K - $184K",
  "salaryMin": 136000,
  "salaryMax": 184000,
  "salaryPeriod": "ANNUAL",
  "salaryCurrency": "USD",
  "salarySource": "EMPLOYER_PROVIDED",
  "location": "New York, NY",
  "ageInDays": 6,
  "postedAt": "2026-08-14T09:12:44.301Z",
  "easyApply": false,
  "sponsored": false,
  "descriptionSnippet": "You will own the ingestion pipelines behind our clinical reporting stack...",
  "jobUrl": "https://www.glassdoor.com/job-listing/senior-data-engineer-halden-health-JV_IC1132348_KO0,20.htm",
  "query": "data engineer",
  "searchLocation": "New York, NY",
  "sourceUrl": "https://www.glassdoor.com/Job/jobs.htm?sc.keyword=data+engineer&locT=C&locId=1132348",
  "collectedAt": "2026-08-20T09:31:02.118Z"
}
```

### Reliability

Rows come from the search page's own data payload, so a value is either what Glassdoor holds or
`null`. No model interprets the HTML, which is what keeps a job title from being paraphrased and a
rating from being rounded.

Glassdoor dates a listing only as a whole-day age. `ageInDays` is carried through as the source's
own value and `postedAt` is derived from it, so the timestamp is honest to the day rather than
presenting an invented hour.

A search that cannot be read is recorded in the run's `SUMMARY` under `failures` with its URL and
error, and the remaining searches still return. A run where every search failed exits with an error.
A location Glassdoor cannot resolve fails the run outright, because continuing would silently widen
every query to nationwide results and you would not see it in the data.

### Limitations

One results page per search, around 22 listings, and no pagination input. Ten terms return roughly
220 listings. Split a broad query into narrower ones to widen coverage.

One location per run. Company reviews, interview reports, salary-report pages and full job
descriptions are out of scope; `descriptionSnippet` is the card fragment, and `jobUrl` leads to the
rest.

### How much does it cost?

Pay per event: you are charged per result row written to the dataset, plus Apify's platform usage
for the run. The current per-result price is on this Actor's Store page under Pricing.

The location lookup at the start of a run writes no rows and costs you nothing. Neither does a
search that fails.

Runs started from an Apify **free plan** stop at 250 requests and 250 results, and the run says so
in its status message. Any paid plan runs the full input and whatever `maxItems` you set. The limit
exists because this Actor fetches through our own anti-bot infrastructure, which Apify does not
cover for free-plan runs. It binds on requests as well as rows so that a long input list cannot
spend those fetches on rows the run will never return.

### Using it with the Apify API

```javascript
import { ApifyClient } from 'apify-client';

const client = new ApifyClient({ token: 'YOUR_APIFY_TOKEN' });

const run = await client.actor('usestring/glassdoor-jobs').call({
    queries: ['software engineer', 'product manager'],
    location: 'New York, NY',
    maxItems: 500
});

const { items } = await client.dataset(run.defaultDatasetId).listItems();
const stated = items.filter((job) => job.salarySource === 'EMPLOYER_PROVIDED' && job.salaryMax);
const midpoints = stated.map((job) => (job.salaryMin + job.salaryMax) / 2);
console.log(`${stated.length} listings with employer-stated pay`, midpoints);
```

The Python client takes the same input, as does the REST API. The `SUMMARY` record with the item
count and any failed searches sits in the run's default key-value store.

### Related actors

- [Indeed Job Listings Scraper with Salaries](https://apify.com/usestring/indeed-jobs)
- [ZipRecruiter Jobs Scraper](https://apify.com/usestring/ziprecruiter-jobs)
- [LinkedIn Public Profile Scraper (No Login)](https://apify.com/usestring/linkedin-profiles)
- [Wellfound Startup Jobs Scraper](https://apify.com/usestring/wellfound-jobs)
- [Workday Job Board Scraper](https://apify.com/usestring/workday-jobs)

Running the Indeed scraper over the same titles gives you employer-stated salary strings to check
these numeric ranges against.

### FAQ

**Do I need a Glassdoor account or API key?**
No. Only public search pages are read, with no login, cookie or key.

**Is the salary the employer's or Glassdoor's estimate?**
Both occur in the data and `salarySource` tells you which. Employer-stated ranges carry
`"EMPLOYER_PROVIDED"`. The label is passed through unchanged rather than flattened away, so you can
filter to one kind before you benchmark anything.

**How many listings does one search return, and can I page further?**
Around 22, which is one Glassdoor results page. There is no pagination input. For more coverage,
run more specific terms, or the same term against several locations in separate runs.

**Can I search more than one location in a run?**
No. All of a run's `queries` share one `location`, because the search URL is built from Glassdoor's
resolved location ID. Start one run per city.

**How current is `postedAt`?**
It is derived from `ageInDays`, which Glassdoor reports in whole days, so it is accurate to the day
of posting and not to the hour. `collectedAt` records when the run read the page.

**What happens when a search fails or Glassdoor blocks?**
Fetches go through our own anti-bot infrastructure. A search that still cannot be read lands in
`failures` in the run's `SUMMARY` and the rest of the run continues. If all of them fail, the run
errors out so a schedule notices.

### Feedback

If a field is wrong, or you need something Glassdoor shows that is not in the table above, open an
issue from this Actor's Store page. Include the search term and location so we can reproduce it.

# Actor input Schema

## `queries` (type: `array`):

Job titles or keywords.

## `location` (type: `string`):

City, state or country to search in.

## `maxItems` (type: `integer`):

Global cap on dataset items. Runs started from an Apify free plan stop at 250 requests and 250 results; any paid plan runs the full amount.

## `concurrency` (type: `integer`):

Targets fetched in parallel.

## Actor input object example

```json
{
  "queries": [
    "software engineer"
  ],
  "location": "United States",
  "maxItems": 1000,
  "concurrency": 5
}
```

# Actor output Schema

## `results` (type: `string`):

Scrape Glassdoor job listings by keyword and location. Each row carries the title, company, employer rating, pay as numbers with its period and currency next to the range Glassdoor displays, easy-apply and sponsored flags, a description snippet, days since posting and the apply URL.

## `summary` (type: `string`):

Item count, failure count and every target that failed, with its error.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "queries": [
        "software engineer"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("usestring/glassdoor-jobs").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = { "queries": ["software engineer"] }

# Run the Actor and wait for it to finish
run = client.actor("usestring/glassdoor-jobs").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "queries": [
    "software engineer"
  ]
}' |
apify call usestring/glassdoor-jobs --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,usestring/glassdoor-jobs"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/7qu9KFQXCnwUuTpgx/builds/h9unp8Q5frFa8qHI7/openapi.json
