# Changelog of 🔥 Glassdoor jobs scraper (`bebity/glassdoor-jobs-scraper`) Actor

- **URL**: https://apify.com/bebity/glassdoor-jobs-scraper/changelog.md
- **Full Actor documentation**: https://apify.com/bebity/glassdoor-jobs-scraper.md

## Changelog

All notable changes to `glassdoor-scraper` are documented here.
Format: [Keep a Changelog](https://keepachangelog.com/). Versioning: [SemVer](https://semver.org/).

### \[0.3.5] — 2026-09-09

**Pay-per-event is live, and it gets cheaper on higher Apify plans.**

The monthly rental is gone. From **22 September 2026** you pay only for the jobs you actually get,
and platform usage is included rather than billed on top of a subscription.

#### Changed

- **No more monthly fee.** The old model was $25/month plus your own platform usage. There is now
  no subscription and no separate usage bill — if you collect fewer than roughly 60,000 jobs a
  month, you pay less than before, most people by a wide margin.
- **A job costs $0.40 per 1,000**, description included. The salary benchmark is now **$1.00 per
  1,000** instead of $1.50, matching company insights, so a run with everything switched on costs
  **$2.40 per 1,000** rather than $2.90.
- **Starting a run costs $0.00005** instead of $0.005 — Apify's own default, which also waives the
  first five seconds of every run's compute.

#### Added

- **Automatic discounts on higher Apify plans.** Applied from your subscription tier, nothing to
  ask for:

  | Per 1,000 jobs | Free | Bronze | Silver | Gold and above |
  |---|---|---|---|---|
  | Jobs | $0.40 | $0.35 | $0.30 | $0.28 |
  | ➕ Company insights | $1.00 | $0.80 | $0.60 | $0.50 |
  | ➕ Salary benchmark | $1.00 | $0.80 | $0.60 | $0.50 |
  | **Everything on** | $2.40 | $1.95 | $1.50 | $1.28 |

#### Notes

- Nothing changed in the data a run returns, in the input, or in how the scraper works. Your
  existing runs and integrations keep working unchanged.
- You are still never charged for a failed row, for a duplicate Glassdoor repeats across its pages,
  or for an add-on that found nothing.

### \[0.3.4] — 2026-09-09

**Search by city and by company works again.**

Glassdoor moved the two lookups that turn a city name or a company name into an id. The scraper was
still calling the old addresses, which now return a "page not found", so any run that searched by
**keyword + location** or **by company name** failed straight away with `location-unresolved` or
`employer-unresolved`. Searching by pasted URL, by job ID, or by keyword alone was unaffected.

Nothing was lost on Glassdoor's side — the lookups were consolidated, not removed, and they return
the same ids as before. The scraper now calls the new addresses.

#### Fixed

- **Keyword + location searches run again.** Verified live on glassdoor.com and glassdoor.fr.
- **Company-name searches run again.** Verified live.
- **Every country site is covered by the same fix** — checked on .com, .fr, .co.uk, .de and .ca.
- **The location the run locks onto is now named the way your country site names it** — "New York,
  NY (US)" on .com, "Paris, 75 (France)" on .fr, "Berlin (Deutschland)" on .de. That line is your
  first check that Glassdoor picked the place you meant.

### \[0.3.3] — 2026-09-09

**Four output fixes found by benchmarking this scraper against its competitors.**

#### Fixed

- **`job_types` reads like a list of job types again.** It was shipping Glassdoor's internal
  translation keys — `search-jobs.job-type-options.contract` — on every row. It now carries the same
  clean values the previous Glassdoor scraper did: `FULL_TIME`, `PART_TIME`, `CONTRACT`,
  `INTERNSHIP`, `TEMPORARY`. A type we have not seen before comes through in the same shape rather
  than being dropped, and `job_type_labels` still carries Glassdoor's own display text.
- **A rate-limited request is now reported as `rate-limited`.** Every throttled request was landing
  in your failed rows as `parse-error`, which said the data could not be read when in fact it was
  never fetched. Blocked and not-found requests were mislabelled the same way and are now
  `waf-blocked` and `not-found`.
- **`company_city` is filled.** It was declared and left empty on every row; it now carries the
  employer's city, taken from its headquarters.
- **We no longer claim a "500–700 results" limit.** How far one search reaches varies — we have
  measured 543 and 1,008 on the same keyword over different connections — so the run now warns you
  when your request looks larger than one search can deliver, and tells you at the end **exactly**
  how many it reached instead of quoting a figure that can be wrong.

#### Notes

- `job_location.unknown` keeps its name. It looks like a placeholder and is not: it is the field
  name the previous scraper used for the displayable location ("New York, NY"), and renaming it
  would break existing integrations. `city`, `state` and `country` hold the parsed parts.

### \[0.3.2] — 2026-09-09

**A run that gets moved to another server no longer starts over — or charges you twice.**

Apify sometimes moves a running Actor to a different server. Until now this scraper lost everything
it had done and began again from the first page, re-delivering jobs you already had and billing for
them a second time. It now picks up where it left off.

#### Fixed

- **Migrations no longer restart the run.** The scraper keeps a private note of which jobs it has
  already delivered, and skips them when it resumes. You see one line in the log — *"Picking up
  where the run left off — N jobs already delivered, and you will not be charged for them again"* —
  and the run continues instead of repeating itself.
- **Nothing is billed twice.** Jobs delivered before a move are never re-delivered and never
  re-charged, in every search mode, including a run built from a list of job IDs.
- **`maxItems` stays honest across a move.** A run asked for 500 jobs delivers 500 in total, not 500
  more after the move.

#### Notes

- The note the scraper keeps holds nothing but job IDs you already have in your results, plus three
  counters. No links, no queries, no session data. It is deleted when the run ends.
- Nothing changed in the data a run returns, in the input, or in prices.

### \[0.3.1] — 2026-09-08

**New pricing: $0.40 per 1,000 jobs, full description included. Most runs get cheaper.**

We benchmarked the eight busiest Glassdoor scrapers on Apify against the same search, then repriced
against what we found. The headline number drops from $1.00 to **$0.40 per 1,000 jobs** — the same
price as the category leader — and the full job description, which used to cost extra, is now part
of the price of a job. Two optional add-ons carry the depth nobody else sells.

#### Changed

- **A job now costs $0.0004 — $0.40 per 1,000, down from $1.00.** One price, whatever the run.
- **The full job description is included.** It used to make a job cost $0.003 instead of $0.001.
  Every competing scraper ships the description in its base price, so ours no longer charges for it.
  *Fetch full job descriptions* still exists, but it now changes only the **speed** of a run: on, we
  make one request per job; off, one per thirty. Same price either way.
- **What a run costs now**, start fee included: 100 jobs **$0.05** (was $0.11) · 1,000 jobs **$0.41**
  (was $1.01) · 1,000 with everything on **$2.91** (was $3.01) · 10,000 with everything on **$29.01**
  (was $30.01).

#### Added

- **Two optional add-ons**, each billed on top of a job and **only on rows where they found
  something**:
  - **Company insights** (+$1.00 per 1,000) — the employer's own description and mission, its CEO,
    its competitors, its awards, links to its reviews and salaries pages, and how many reviews,
    salaries and interviews Glassdoor holds. About 40% of listings have no employer page; those rows
    cost you nothing extra.
  - **Salary benchmark** (+$1.50 per 1,000) — what the market pays for that role in that place, from
    the 10th to the 90th percentile for base, additional and total pay, with Glassdoor's confidence
    rating. No other Glassdoor scraper on the Store sells this.
- **Duplicates are never billed.** Glassdoor repeats about 10% of listings across pages; they are
  dropped before billing, so a run of 1,000 is 1,000 distinct jobs. (This was already true — it is
  stated here because it is worth knowing when comparing per-job prices.)

#### Notes

- The billed events changed name. `job-basic` and `job-detailed` are replaced by `job`,
  `company-insights` and `salary-benchmark`, so old invoices and new ones list different lines.
- Nothing changed in the data a run returns. Same fields, same names, same shape as 0.3.0.

### \[0.3.0] — 2026-09-08

**One flat row. The `v2` object is gone, and nothing you already read has moved.**

Rows carried the same data twice: the field names from the previous Glassdoor scraper at the top
level, and a `v2` object repeating them under modern names. You had to know which half to read, and
the two halves disagreed about how to say the same thing. There is now a single set of fields.

#### Changed

- **The `v2` object no longer exists.** Everything it carried is now a normal field on the row,
  named the same way the rest of the row is named. Twenty-one of its entries were simply duplicates
  of a field you already had, and were dropped rather than renamed.
- **Every legacy field is untouched.** All 41 of them — same names, same types, same values,
  including the ones deliberately left empty. A test now asserts the complete list on every row, so
  this cannot drift by accident.
- **New fields you did not have before**, all promoted out of `v2`: `company_rating` (the employer's
  Glassdoor score, which had no legacy field at all), `company_ratings` with its ten detailed
  scores, `company_sector`, `company_type`, `company_id`, `company_profile_url`, `job_easy_apply`,
  `job_expired`, `job_source`, `job_seo_url`, `job_age_in_days`, `job_type_labels`,
  `job_description_snippet`, `company_details` and `job_salary_benchmark`.
- **`job_location` and `job_salary` gained keys instead of gaining neighbours.** `job_location` now
  carries `id`, `type` (city / state / country / metro) and `country_id`; `job_salary` carries
  `median` and `source`. Their existing keys are unchanged.
- **`company_description` is filled** when *Fetch company details* is on. It was always empty
  before — the one legacy field Glassdoor can now answer.

Three fields are deliberately kept in both forms, because the two renderings serve different
purposes and dropping either would break someone:

| field | keeps | gains |
|---|---|---|
| employment type | `job_types`, Glassdoor's raw key | `job_type_labels`, e.g. `["Full-time"]` |
| posting age | `job_publication_age`, e.g. `"14 days ago"` | `job_age_in_days`, e.g. `14` |
| job link | `job_url`, the tracking link | `job_seo_url`, the readable one |

#### Added

- **Five fields Glassdoor was already serving, at no extra cost.** All of them come from the search
  response, so they arrive in both modes without a single extra request.
  - `job_location.state` — the state or region, e.g. *New York State*. Absent on a country-wide job.
  - `job_location.latitude` / `job_location.longitude` — coordinates. Their precision follows the
    job's location **scope**, not its address: a country-wide French listing answers `46 / 2`, the
    centroid of France. `job_location.type` tells you which scope applies.
  - `job_occupation` and `job_occupation_id` — Glassdoor's normalised occupation and its stable
    numeric key. This is **not** a duplicate of `job_normalized_title`, which is localised: on a
    live glassdoor.fr run the two differed on 11 rows out of 12 — *ingénieur front end (h/f)*
    against *front end engineer*. It is what lets one role be grouped across country sites.

Five more were probed and deliberately left out, so nobody re-opens them without new evidence:
`map.postalCode` exists but is null on every job tested, `header.urgencySignal` (the only candidate
for the always-empty `job_candidate_numbers`) likewise, `payPercentile25` / `payPercentile75` return
0 on listings — a job only ever carries the low, median and high — and `divisionEmployerName` is
null throughout. Seniority, language, shift and job-poster fields are rejected by the API outright.

#### Views

- **Four views replace the two.** **Jobs** is the complete one, logo first. **Companies** reads the
  same rows through the employer's columns — ratings, size, sector, CEO, review counts. **Salaries**
  puts what the job pays next to what the market pays for the role. **Failed items** is unchanged.
- **The `v1_compat` view is removed.** With one set of fields there is nothing left to switch
  between. If you linked to `?view=v1_compat`, use `?view=overview`; the columns you were reading
  are all there.

#### Fixed

- `company_rating` and the overall score in `company_ratings` are both kept on purpose. They look
  like the same number and usually are, but they come from different places in Glassdoor and
  disagree on about a quarter of rows — measured on live data, `1` against `2.6` on one job.

### \[0.2.8] — 2026-09-08

**What the market pays, next to what the job pays.**

#### Added

- **"Add salary benchmark"** — a new option that answers the question a salary range on its own
  cannot: *is this job paid well?* Every job gains Glassdoor's own market estimate for the same role
  in the same place — **base pay, additional pay** (bonus, commission, profit sharing) **and total
  pay**, each from the **10th to the 90th percentile**, with the currency, the pay period and
  Glassdoor's confidence rating. A New York software engineer benchmarks at $83,013 / $125,658 /
  $190,213 base, $43,124 median on top, and $168,783 median all in.

  The additional-pay percentiles are worth calling out on their own: almost nobody publishes the
  bonus side of a role, and it is often a quarter of the package.

  It joins on data every row already carries — Glassdoor's normalised job title and the structured
  location — so there is nothing extra to configure. And it is asked **once per role and place, not
  once per job**: 30 real jobs held 11 distinct roles, so a live run resolved 18 roles in **2
  batched calls**. Off by default.

- ⚠️ **The benchmark is always ANNUAL**, and the row says so in `payPeriod`. Glassdoor annualises
  these estimates whatever the trade — a New York server comes back at $43,520, not at an hourly
  rate — so a job advertised at "$50–100 per hour" is **not** below a $138,057 benchmark. The two
  figures are in different units until you convert, and shipping the period alongside the numbers
  is what keeps that from becoming a silent error.

#### Fixed

- **Company details survive a hiccup.** Shipped in 0.2.7, the employer pass gave up on a batch after
  a single failure — and a batch covers up to 30 companies, one of which can carry sixty listings.
  A live run lost a dossier to a transient proxy error exactly that way. Both enrichment passes now
  retry, like the location lookup already did. Nothing is recorded until a response parses, so a
  retry cannot double-count.

#### Notes

`industry` is a required argument on Glassdoor's side and changes nothing: five different industry
ids returned the same figures to the cent. It is sent to satisfy the schema and never presented as
a filter.

The currency symbol is derived from the ISO code rather than taken from Glassdoor, which returns it
as an HTML entity (`&euro;` instead of `€`). That also collapsed two copies of the same table into
one, shared with `job_salary.currency_symbol`.

### \[0.2.7] — 2026-09-08

**Two ratings we were not shipping, and the company behind the job.**

#### Fixed

- **We were promising 8 detailed ratings and Glassdoor serves 10.** `businessOutlook` (the share of
  reviewers positive about where the company is heading) and `diversityAndInclusion` were being
  requested by nobody, at exactly the same fill rate as the eight we already shipped. They cost no
  extra request and are on every row now, in both modes. The README, the schema and the Jobs view
  said eight; they now say ten, because that is what arrives.

#### Added

- **"Fetch company details"** — a new option that adds what Glassdoor knows about the *employer*,
  which no job query returns at any price:

  - the company's own **description** and **mission**;
  - its **CEO** — name, title, photo;
  - its **competitors**, with logos;
  - its **awards** — Fortune's 100 Best Companies to Work For, LinkedIn Top Companies…;
  - **how much Glassdoor holds** on it: review, salary, interview, photo and benefit counts;
  - direct links to its jobs, reviews and salaries pages.

  It is asked **once per company, not once per job**, and up to 30 companies share a single request.
  That matters because employers repeat: 700 real jobs held only 192 distinct employers. A run of
  1,000 jobs therefore costs about **10 extra requests** — against the 1,000 that fetching
  descriptions costs. Off by default, since it does add a little time to each page.

  A company with no Glassdoor profile, or a batch that fails, leaves the row exactly as it was:
  company details are an enrichment, never a condition for a job to ship.

### \[0.2.6] — 2026-09-08

**Six fields that were sitting in the API untouched — including one that was blank since v1.**

#### Added

- **`job_remote` / `v2.remote`** — whether the role is work-from-home. This field has existed in the
  output since the very first version and was **always null**: Glassdoor does expose it, under
  `remoteWorkTypes`, and nobody had asked for it. It comes back on 30% of an ordinary search and
  100% of a remote-filtered one. A job Glassdoor says nothing about stays empty rather than being
  reported as on-site — absence is not proof.

- **`v2.seoUrl`** — the clean, readable listing URL Glassdoor itself links to, instead of only the
  tracking redirect.

- **`v2.company.profileUrl`** — the employer's Glassdoor page. The API has no field for it, so it is
  built from the employer id and was checked against live pages before shipping.

- **`v2.expired`** — whether the listing is still live.

- **`v2.jobSource`** — where Glassdoor imported the listing from, e.g. "Indeed Job Search Platform".

- **`v2.descriptionSnippet`** — a ~160-character opening excerpt of the description (124-175 across
  the sample), served by the search itself. It is what makes a run without descriptions actually
  triageable: you can tell what a job is about before deciding to pay for the full text. It is an
  excerpt, not a summary, and only sometimes ends in an ellipsis — hence the name. It shows up as
  the **Description preview** column. Comes in two flavours, like the full description does:
  `descriptionSnippet` is plain text, `descriptionSnippetHtml` keeps the `<b>` Glassdoor wraps
  around your searched keyword.

All six arrive with the search response: no extra request, no extra cost, both modes.
**Readable job link**, **Remote**, **Expired**, **Listing source** and **Company profile** are now
columns in the Jobs view, and the Legacy (v1) view gained **`job_remote`** and **`job_posted_date`**,
which had nothing to show until now.

#### Notes

Two candidates were probed and deliberately dropped: `payPercentile25` / `payPercentile75` return
0 on every job tested, and `organic` reports `true` even on sponsored listings. Shipping either
would have meant shipping a field that is always wrong.

### \[0.2.5] — 2026-09-07

**The company profile and all 8 ratings now come with every job, description or not.**

#### Changed

- **Company profiles no longer need "Fetch full details".** Glassdoor serves the entire employer
  dossier on the search response itself — we were fetching it a second time, once per job, for
  nothing. Verified field by field against the old detail call on 20 jobs: **120 comparisons, 120
  identical, zero differences.** So size, industry, sector, revenue, year founded, headquarters,
  website, company type, employer id, the apply link, employment type, posting date and **all 8
  detailed ratings** are now on every row, in both modes, at no extra cost or time.
- **The option is now "Fetch full job descriptions"**, because the description is the only thing it
  still buys. Everything it used to gate comes free.
- A 1,000-job run with descriptions goes from about **1,034 requests to about 34**.
- Measured before shipping: 54 pages of the wider query — **zero Cloudflare blocks**, no latency
  drift over 30 consecutive pages, pagination reaching just as far, +180 ms per page of 30 jobs and
  1.6 KB more per job (against the 6–17 KB detail call it replaces).

#### Fixed

- **`company_size` and `company_revenue` shipped the literal string `"Unknown"`.** Glassdoor sends
  `"Unknown"` / `"Unknown / Non-Applicable"` rather than omitting an unknown value; those now come
  back empty, like every other missing field. Affects roughly one employer in five.
- `company_short_name` returned the full company name. It now uses Glassdoor's actual short name
  ("NVIDIA" rather than "NVIDIA Corporation").
- `job_location.country` now prefers Glassdoor's own country name, so it resolves for **any**
  country instead of only the 20 the country selector offers — including in the job-ID mode, which
  never goes through a search and would otherwise have been the one mode left without a country.

### \[0.2.4] — 2026-09-07

**Currency and country, in every mode.**

#### Fixed

- **Salaries came back without a currency when full details were off.** A lite row showed
  `min` and `max` but `currency: null`, so a €45,000 job and a $45,000 one were indistinguishable
  in the same dataset. Glassdoor serves the currency on the search response itself — we simply
  weren't asking for it. `currency`, `currency_symbol` and `estimated` (employer-stated vs
  Glassdoor estimate) are now filled in **both** modes, at no extra cost or time.
- **`job_location.country` was always empty.** It is now resolved for all 20 supported country
  sites, in both modes.
- **A nationwide or region-wide job was reported as a city.** `city` took whatever came before the
  first comma, so a country-scoped listing became `city: "united states"` and a Brazilian
  region-wide one became `city: "trabalho remoto"` — literally *remote work*. `city` is now filled
  only when the job really is city-scoped, and left empty otherwise.
- Currency symbols now cover all 20 supported markets (₹, S$, R$, HK$, CHF, NZ$, MX$, AR$…),
  instead of five.

#### Added

- **`v2.country`** — the country name, resolved from Glassdoor's own country id.
- **`v2.locationType`** — what `location` refers to: `C` city, `S` state or region, `N` country,
  `M` metro area. Lets you tell a city listing from a nationwide one without parsing strings.
- **`v2.locationId` / `v2.countryId`** — Glassdoor's own identifiers, for joining runs together.
- **Country** is now a column in the *Jobs* dataset view.

### \[0.2.3] — 2026-09-07

**What a search can actually return, said honestly — and quicker runs.**

#### Fixed

- **Runs announced 2,000 results and delivered about 500.** Glassdoor advertises a huge total —
  93,361 on one search we probed — but stops serving new results after roughly **500–700 jobs**,
  then simply offers no further page. Paginating to exhaustion returned 547 unique jobs at 30 per
  page, 554 at 50 and 543 at 100: it is a limit on *results*, not on pages, so a larger page size
  does not reach further. The old *"Glassdoor returns at most 2,000 results, so we'll fetch 2,000"*
  line came from an early estimate that was never paginated to the end, and it promised four times
  what any run could deliver. The run log, the **Maximum jobs** field and the README now state the
  measured figure.
  Two consequences worth knowing:
  - The warning also fires on **smaller** searches. A search reporting 900 jobs dries up at ~550
    just like one reporting 93,000, and used to say nothing at all while quietly returning half of
    what you asked for.
  - **Nothing changed about how much we collect** — only about what we tell you. The internal
    ceiling is untouched, because a filtered search does reach further and lowering it would have
    thrown away real results.
    To go beyond it, split one search into several narrower ones — by posted date, salary band,
    location, industry or job function — and combine the datasets, deduplicating on `job_id`. Each
    narrower search gets its own allowance and the results barely overlap: on a live test one
    filtered variation returned 739 jobs, **699 of which never appeared** in the unfiltered run.

- **The Docker image only kept building thanks to a cache.** The build installed `pnpm@latest`,
  an unpinned version. pnpm moved to 12.x, whose new install engine cannot reconcile the
  `node_modules` shipped in the Apify base image, and every fresh build failed. Existing builds
  survived on a layer cache, so nothing looked wrong — one cache eviction and the Actor would have
  become unbuildable with no code change to blame. pnpm is now pinned to the version that produced
  the lockfile. This is the same class of failure that broke the previous Glassdoor scraper.

#### Changed

- **Runs are about 20% faster, and cost a fifth as much to operate.** Measured over 11 live runs
  sweeping concurrency and memory against the same search:

  | | before | now |
  |---|---|---|
  | concurrent requests | 10 | **25** |
  | memory | 4 GB | **1 GB** |
  | seconds per job | 0.219 | **0.164** |

  The scraper is limited by the network, not the machine: CPU sits at 9–13% and memory peaks around
  175 MB whatever it is given, so 4 GB was twelve times what a run uses. 25 requests at a time is a
  measured peak and not a floor — **50 was slower than 25** — and no run was blocked or failed at
  any level tested. **Your price is unchanged**, since billing is per delivered job; runs simply
  finish sooner.

#### Internal

- The Actor manifest carried a name and description from the rebuild that never matched the
  published Actor. Restored from the platform, which still held the originals. The name mattered
  beyond the wording: it did not match the deployed Actor, so a plain deploy created a new private
  Actor instead of updating production.

### \[0.2.2] — 2026-09-04

#### Added

- **🚫 Exclude employers.** Paste the companies you never want in your results, one per line. A job
  is left out when its company name contains what you type, so `Amazon` also removes
  *Amazon.com Services LLC* — while `Apple` leaves *Applebee's* alone, because the match is on whole
  words. Excluded jobs never reach your dataset and never trigger a details request, and the run
  keeps paginating until it reaches your maximum: on a live test, excluding two employers dropped 15
  jobs and still delivered the 20 requested. The count is reported at the end of the run.
  (Glassdoor has no exclusion filter of its own — its filter bar offers Easy Apply, Remote, Company
  rating, Date posted and Salary range — so this is applied as results arrive.)

### \[0.2.1] — 2026-09-03

#### Fixed

- **Saved runs from the previous scraper that set an employment type were being refused.** Since
  v0.1.6 the field accepts several values at once, and Apify checks the input before the actor
  starts — so a task carrying the old single value (`"fulltime"`) was rejected outright with
  *"Field input.jobType must be array"*, without ever reaching the compatibility layer. The old
  field now accepts both the old single value and a list, so those tasks run again; the picker you
  see in the Console moved to **Type of employment** and is unchanged. Nothing to redo on your side.
- **Hourly jobs reported a yearly figure.** Glassdoor returns the pay twice — once in the period it
  advertises, once converted to a year — and the full-details path read the converted one while
  labelling it "hourly", so a $65/hour job came back as *hourly, 135,200*. Both paths now read the
  figure that matches the stated period. Annual jobs were never affected, which is why this survived
  earlier checks; 2 of 10 rows in a real run were wrong.
- **Runs by job ID came back without a job title.** That mode does no search, and the details request
  never asked for the title or location — so rows arrived with an empty title, no location, no
  posting age and no company rating. All four are now filled.

### \[0.2.0] — 2026-09-02

**Pay-per-event billing, and the cost work that makes it viable.**

Apify retires the rental pricing model on 2026-10-01; Actors left on it are migrated to
pay-per-usage, where the developer earns nothing. This release moves the Actor to pay-per-event
and cuts the platform cost it now has to absorb.

#### Pricing

- Billing is now **per job**, replacing the $25/month subscription:
  `job-basic` $0.001 ($1 per 1,000) · `job-detailed` $0.003 ($3 per 1,000) ·
  `apify-actor-start` $0.005 per run.
- Charges fire **once per delivered job**, after the row is pushed. Previously the list was billed
  once per page, which made it impossible to price the two modes differently.
- `failed: true` rows are still never charged. `apify-default-dataset-item` is deliberately not
  declared — it would bill them.
- `safeCharge()` now refuses platform-managed events outright: charging `apify-actor-start`
  from code fails the run.
- README gains a Pricing section with worked cost examples.

#### Cost

- **Split the proxy pools.** The Cloudflare-gated location/employer lookups keep residential IPs;
  the bulk `/graph` crawl moves to the cheaper datacenter pool. Measured over 8 cities and
  1,912 jobs: `/graph` is unaffected by the pool, while lookups score 8/8 on residential against
  4/8 on datacenter — so neither pool alone is right. Cost per 1,000 enriched jobs falls from
  $0.100 to $0.013.
- **Memory bounds set to 256–2048 MB** (was unbounded, defaulting to 4,096 MB). Peak measured use
  is 138 MB at every scale tested, and Apify's own production data put 4 GB runs at $3.19 per
  1,000 results against $0.114 at 1 GB. Set the platform default to 512 MB — it also keeps
  `apify-actor-start` at a single charge per run.
- The default proxy input no longer pins an account-specific group.

### \[0.1.12] — 2026-09-01

#### Fixed

- **Apply links were missing their beginning.** Glassdoor returns the apply link as a path
  (`/partner/jobListing.htm?…`), never a full address, and we shipped it as-is — so `v2.applyUrl`
  and `job_apply_url` couldn't be opened or clicked without knowing to prepend the site yourself.
  Both now carry the complete URL (verified: it resolves straight to the job listing). `job_url` was
  already complete, so rows used to contradict themselves.
- All three links now follow the **Glassdoor country site you picked** — a glassdoor.fr run no longer
  hands you glassdoor.com links. The query string is kept intact on purpose: it looks like tracking,
  but it is what makes the partner apply link work.

#### Added

- **A link to the job listing on every row** (`v2.jobUrl`), built from the job id alone — so it is
  there in the fast mode too, where the previous scraper had no link at all. The legacy `job_url`
  field now falls back to it instead of staying empty.
- **Readable employment types** (`v2.jobTypes`): `Full-time`, `Part-time`, `Contract` — the previous
  scraper exposed Glassdoor's raw internal keys (`search-jobs.job-type-options.fulltime`), which it
  still does under the legacy field, untouched.

#### Changed

- **The Jobs table in the Output tab is now the complete picture.** It leads with the logo, title,
  company, rating and location, then the **job link and apply link**, employment type, the full
  salary range, the description, and the whole company profile with its 8 ratings — 39 columns,
  ordered most-useful-first. It mixes the clean new fields with the legacy ones that have no modern
  equivalent, so nothing is only reachable by switching views. The always-empty benefits column was
  removed from it (Glassdoor returned no benefits on any of 13 sampled listings); the field itself
  stays on the row for backward compatibility.

### \[0.1.11] — 2026-09-01

#### Changed

- **The run log now names the location Glassdoor actually matched**, instead of echoing what you
  typed. The lookup returns a single best match, so an ambiguous name is decided for you —
  *Cambridge* means Massachusetts and not England, *Springfield* means Missouri, *Paris* means
  France. The log used to say `Location locked in: cambridge`; it now says
  `Location locked in: Cambridge, MA (US)`, so a wrong pick is obvious on the first run. Add the
  state or country to your search (`Cambridge, UK`) — or paste a Glassdoor search URL — to force the
  one you mean.

### \[0.1.10] — 2026-09-01

#### Fixed

- **🔴 Location search silently fell back to a nationwide search.** Resolving a location (and a
  company name) went through one single request with no retry, and the Apify residential proxy
  rejects a tunnel 5–25% of the time. Any such blip was swallowed: the run widened to the whole
  country, kept going, and billed you for jobs you never asked for. A real run answered
  `location: "new york"` with **11,228 US-wide jobs**. The lookup now retries on a fresh exit IP
  (measured **20/20** successful resolutions, versus 6/8 before), and when a location genuinely
  cannot be matched the run **stops with a clear message and charges nothing** instead of quietly
  scraping the wrong thing. Searching by company name gets the same treatment — it used to crash the
  whole run on the same blip.
- **🎯 Title match is now Off by default.** Introduced in v0.1.9 as *Strict*, it was calibrated
  against results polluted by the location bug above — so it was compensating for the wrong problem.
  Re-measured on live data with a working location (*Web developer* + *New York*, 3 pages): Strict
  keeps **5 of 150 real results (3%)**, discarding *Full Stack Engineer*, *Lead Frontend Engineer*
  and *Software Engineer, Frontend* — exactly the jobs a buyer wants. Off restores the previous
  scraper's behaviour, which never filtered. Strict and Loose remain available as opt-ins.
- **66% more jobs per search page.** Each result page now requests 50 jobs (what the previous
  scraper and Glassdoor's own site request) instead of 30. Glassdoor stops paginating at roughly the
  same page number whatever the page size, so the smaller pages were throwing away a third of the
  reachable depth: 3 pages returned 90 unique jobs, now 149. Same number of requests.
- Local development no longer falls through to a stub session when `AUTH_SERVICE_URL` /
  `AUTH_SERVICE_KEY` are set as documented — both those names and `COOKIE_SERVICE_*` are accepted.

### \[0.1.9] — 2026-07-23

#### Added

- **🎯 Title match — results now stay on topic.** Glassdoor matches a keyword one *word* at a time
  and, once the genuinely-matching jobs run out, keeps padding the results with loosely related
  roles. A search for *AI engineer* drifted into anything containing "engineer" — attorneys,
  machinists, project schedulers. The new **Title match** setting fixes that:

  - **Strict** (default) — a job is kept only if its title contains **every** word of your keyword.
  - **Loose** — the title must contain **at least one** of your words.
  - **Off** — keep everything Glassdoor returns, the previous behaviour.

  Replayed against a real customer run, Strict took the dataset from **46% to 100% on-topic**.
  Skipped jobs are always reported at the end of the run, and are **never charged**.

#### Fixed

- **Pasted search URLs for a state, country or metro area returned nothing.** Only city URLs were
  understood; every other scope was silently skipped or sent with the wrong location type, which
  Glassdoor answers with zero results. All four scopes now work, and a keyword-only search URL is
  accepted as a nationwide search.
- **An unresolvable location no longer corrupts your keyword.** When a location couldn't be matched,
  the actor used to append it to your keyword and search nationwide — which quietly diluted the
  results (measured: 100% → 39% on-topic). It now widens the scope without ever rewriting your query.

### [0.1.8] — 2026-07-20

#### Changed

- **Full details are now on by default.** A fresh run fetches the complete **job description**, the
  **apply link** and the full **company profile** (size, industry, revenue, year founded, HQ, and 8
  detailed ratings) out of the box — matching the previous Glassdoor Jobs Scraper, which always did.
  Prefer speed and a lower cost? Turn **Fetch full details** off for the lite mode — you still get
  the salary range, company rating and location. Full details make one extra request per job, so a
  details-on run is slower and costs more than lite.

### [0.1.7] — 2026-07-20

#### Changed

- **A cleaner, friendlier run log.** The live log now reads as a tidy progress story — a branded
  intro, your search milestones (location, company, jobs found), a running "N of M jobs so far…"
  counter, and a friendly wrap-up — instead of the scraping engine's internal chatter. Technical
  diagnostics (retries, internal stats, the data endpoint) are no longer shown by default; they stay
  available in debug mode. No change to the data you get — just a nicer window into the run.

### [0.1.6] — 2026-07-20

#### Added

- **Multi-select filters.** Employment type, seniority, industry and job function now let you pick
  **several values at once** (e.g. Full-time *and* Contract, or Engineering *and* IT) — results match
  any of them. Previously each was limited to a single choice.
- **Company logos on every job** — shown in the results table even in the fast (list-only) mode, with
  no extra request or cost.
- **Cleaner Output tab.** A logo-first results table and a proper Actor output schema, so runs display
  their jobs immediately in Apify Console instead of showing an empty or "no output schema" state.

#### Fixed

- **Location search now targets the city you enter.** It was silently falling back to a nationwide
  search on every run — the location lookup was being blocked before it could resolve. Searching
  "New York" now returns New York–area jobs. (Same fix restores **search by company name**.)
- **Runs now return the full number of jobs you ask for.** In full-details mode, asking for 100 could
  return ~88: Glassdoor repeats around 10% of its listings across result pages, and those repeats were
  eating into the count. Duplicate listings are now skipped and the search keeps going until it reaches
  your target — so 100 means 100 (and no duplicate rows in fast mode either).

### [0.1.5] — 2026-07-19

#### Fixed (backward compatibility)

- **The previous scraper's "Domain" filter now works again.** Saved tasks that set a Domain
  (Engineering, Finance, Legal, IT, Consulting, HR, …) had it silently ignored — the old scraper's
  category ids are dead on Glassdoor's side today. It's now mapped to the current, working
  job-function taxonomy, so those runs narrow their results as intended. Every target id was
  verified live (e.g. Domain = Legal on a "manager" search → 264 legal roles, versus ~795k
  unfiltered).
- The **"All"** option on the Domain and Industry dropdowns no longer slips through as a literal
  filter value — it now correctly means "no filter".

### [0.1.4] — 2026-07-18

#### Added / Fixed (filter completeness audit)

- **Industry** now offers all **25 Glassdoor sectors** (was 4) — Healthcare, Legal, Manufacturing,
  Retail, Government, Education, Real Estate, Insurance, Telecommunications, and more. The filter
  targets Glassdoor's numeric sector id; the full taxonomy was enumerated from live data and every
  sector filter-verified.
- **Company size** brackets relabelled to match Glassdoor's real filter buckets: 1–200, 201–500,
  501–1000, 1001–5000, 5000+. The previous labels (1–50, 51–200, 201–1000, …) were off — e.g. the
  "51–200" option actually returns 201–500 companies. Verified against live results.

### [0.1.3] — 2026-07-18

#### Added

- **Job function filter** — narrow a broad search to one kind of role (18 functions: Accounting &
  Finance, Engineering, Healthcare & Medical, Sales, Skilled Trades, Legal, …). E.g. searching
  "manager" with *Accounting & Finance* returns finance managers only. Wired to Glassdoor's numeric
  `sgocId`; the taxonomy was discovered from live jobs and every category was filter-verified. The
  previous scraper's "Domain" filter targeted ids that are now dead on Glassdoor's side — this
  replaces it with the current, working taxonomy.

### [0.1.2] — 2026-07-18

#### Fixed (filters that silently did nothing — live-verified against `/graph`)

- **Company size** now actually filters. It was sending the label (`"HUGE"`) to a filter that
  expects a numeric bucket id — Glassdoor ignored it and returned every size. Now mapped to `1`–`5`.
- **Easy Apply only** now works and no longer breaks the search. It was sending
  `"EASY_APPLY_ONLY"`, which made the whole search query error out; now sends the numeric `1`.
- Both bugs were inherited from the previous scraper's filter mapping (a code bug there meant the
  raw strings were shipped instead of the intended numeric ids, so those filters never worked).

#### Added

- **Industry filter** (Financial Services · Management & Consulting · Human Resources & Staffing ·
  Information Technology). Wired to Glassdoor's numeric `industryNId`, each id verified live. Also
  fixes the industry filter for previous-version inputs, which likewise never worked before.

### [0.1.1] — 2026-07-18

#### Fixed

- Input parse no longer crashes when a dropdown filter is used. Apify's `select` editor sends its
  value as a **string** even when the field is declared `integer`, so `postedWithinDays`, `minRating`
  and `radiusKm` arrived as `"7"`/`"4"`/`"50"` and failed Zod validation (`expected number, received
  string`) → run exited before scraping. These three now coerce at the input boundary and accept both
  strings and numbers. `minRating` still enforces its 0–5 bound. (`minRating`/`radiusKm` were latent —
  they only worked while left at their default.)

### [0.1.0] — 2026-07-17

Initial build. Replaces the broken `glassdoor-jobs-scraper`.

#### Architecture

- **Step 2** — a detached browser service leases one Cloudflare `cf_clearance`; the actor itself is
  a pure-HTTP scraper (`CheerioCrawler` + `impit`, Firefox fingerprint, no browser in the runtime).
- One clearance is fleet-portable and amortises over ≥3600 items, so the browser is marginal.
- Data path: `POST /graph` GraphQL, `gd-csrf-token: 1` (literal — no token minting).

#### Added

- Keyword, company, pasted-search-URL and direct-job-ID search modes.
- Optional `scrapeDetails`: full description + employer dossier (size, industry, revenue,
  8 detailed ratings) — one extra request per job.
- Every dataset row carries **both** the frozen v1 field contract (root) and a clean `v2` block,
  so existing `glassdoor-jobs-scraper` buyers do not break. v1 inputs are auto-detected and
  force `scrapeDetails`.
- Salary, company rating and location arrive in the list query — no detail call needed for them.
- The 2000-results-per-search cap is announced from `totalJobsCount`, never silently truncated.
- Two charge events: `job-list-page` ($0.004), `job-detail` ($0.006).

#### Fixed (vs v1 behaviour, deliberate)

- `job_salary` was corrected: v1 shipped `min`/`max` inverted, `pay_period` hardcoded `"hourly"`,
  and hourly figures for annual jobs. Now `min < max`, real period, matching figures.
- `company_size` / `company_industries` are now populated (v1 hardcoded them to `null`).

#### Quality

- 80 unit tests, offline against captured samples. `lint` / `tsc` strict / `lint:secrets` /
  `lint:kv-leak` all green. End-to-end smoke verified through the production residential proxy.

#### Not yet in this release

- The cookie-bootstrap service (a separate deliverable). Until it exists, run with `COOKIE_FILE`
  pointing at a jar captured via camouxai. **Not production-deployable without it.**
- Facet-splitting past the 2000-per-search cap (BACKLOG #1).
- Performance sweep under concurrency (`/optimize-perf`).

[0.1.8]: https://github.com/bitty-studio/glassdoor-scraper/releases/tag/v0.1.8

[0.1.7]: https://github.com/bitty-studio/glassdoor-scraper/releases/tag/v0.1.7

[0.1.6]: https://github.com/bitty-studio/glassdoor-scraper/releases/tag/v0.1.6

[0.1.5]: https://github.com/bitty-studio/glassdoor-scraper/releases/tag/v0.1.5

[0.1.4]: https://github.com/bitty-studio/glassdoor-scraper/releases/tag/v0.1.4

[0.1.3]: https://github.com/bitty-studio/glassdoor-scraper/releases/tag/v0.1.3

[0.1.2]: https://github.com/bitty-studio/glassdoor-scraper/releases/tag/v0.1.2

[0.1.1]: https://github.com/bitty-studio/glassdoor-scraper/releases/tag/v0.1.1

[0.1.0]: https://github.com/bitty-studio/glassdoor-scraper/releases/tag/v0.1.0
