Integration Guide

How to Detect Suspicious Glassdoor Review Activity

'Mandatory Glassdoor review with mandatory screenshots' gets 106 upvotes on r/DarkCorporate for a reason — coordinated review campaigns are common and visible in the data. Here's how to spot a burst, plus catching reviews that quietly vanish.

Written by Alex P.

  • Glassdoor
  • review monitoring
  • fake reviews
  • employer brand
  • reputation management

r/recruitinghell and r/DarkCorporate both surface the same story repeatedly: a company has a bad stretch, then either a wave of suspiciously similar five-star reviews shows up (“Mandatory Glassdoor review with mandatory screenshots… Send me screenshots of your submission for proof”), or negative reviews that were there last month quietly aren’t anymore. Searching for it turns up plenty of vendor pages selling “employer brand protection” — and none of them show you how to actually look at the data yourself.

You don’t need a service for this. Glassdoor’s own review listing carries everything needed to spot both patterns: a stable id to diff on, and a timestamp precise enough to catch a burst.


The two signals, and why they need different code

Coordinated activity looks like an unusual number of reviews landing in a short window, often skewed toward one extreme rating. Removal looks like a review that was in a previous pull and isn’t in the current one. Neither is provable from the API alone — a burst could be a genuine wave of new hires, and a missing review could have scrolled past your poll window — but both are worth flagging for a human to look at, which is a very different bar than “prove it happened.”

const API_KEY = process.env.FETCHLAYER_API_KEY;

async function fetchReviews(company) {
  const res = await fetch('https://api.fetchlayer.dev/glassdoor/company-reviews', {
    method: 'POST',
    headers: { Authorization: `Bearer ${API_KEY}`, 'Content-Type': 'application/json' },
    body: JSON.stringify({ company, sortBy: 'date', sortDirection: 'desc', limit: 100 }),
  });
  const { reviews, totalReviewCount } = await res.json();
  return { reviews, totalReviewCount };
}

Each review carries a stable reviewId — unlike some review platforms, there’s no need to fingerprint on content — plus reviewedAt, ratingOverall, ceoRating, recommendToFriend, jobTitle, location, employmentStatus, and lengthOfEmploymentYears. That’s enough structure to detect a burst that looks organized rather than organic.


Detecting a burst

A coordinated push tends to look different from ordinary review flow in two ways at once: an unusual count in a short window, and unusual similarity within that window (many reviews the same day, skewed rating, thin or repetitive text). Check both — count alone flags normal spikes (a company that just did a big all-hands survey push isn’t necessarily gaming anything):

function detectBurst(reviews, { windowDays = 3, minCount = 8, ratingSkewThreshold = 0.85 } = {}) {
  const byDay = new Map();
  for (const r of reviews) {
    const day = r.reviewedAt?.slice(0, 10);
    if (!day) continue;
    if (!byDay.has(day)) byDay.set(day, []);
    byDay.get(day).push(r);
  }

  const flagged = [];
  for (const [day, dayReviews] of byDay) {
    if (dayReviews.length < minCount) continue;

    const positive = dayReviews.filter((r) => r.ratingOverall >= 4).length;
    const skew = Math.max(positive, dayReviews.length - positive) / dayReviews.length;

    if (skew >= ratingSkewThreshold) {
      flagged.push({
        day,
        count: dayReviews.length,
        skew,
        direction: positive > dayReviews.length / 2 ? 'positive' : 'negative',
      });
    }
  }

  return flagged;
}

A day with 8+ reviews where 85%+ land on the same side of the rating scale is worth a look — not proof of a “mandatory review week,” but exactly the shape that pattern leaves behind. Widen minCount for a large company where that volume is routine, and narrow it for a small one where even three same-day reviews would be unusual.


Catching removed reviews

This is the same shape of problem as tracking deleted Trustpilot reviews, applied to employer reviews — diff on id, and don’t false-alarm when something simply scrolled past your poll window rather than being taken down:

import { readFile, writeFile } from 'node:fs/promises';

const STATE_FILE = './glassdoor-state.json';

async function run(company) {
  let seen;
  try {
    seen = new Map(JSON.parse(await readFile(STATE_FILE, 'utf8')));
  } catch {
    seen = new Map();
  }

  const { reviews } = await fetchReviews(company);
  const currentIds = new Set(reviews.map((r) => r.reviewId));
  const isFirstRun = seen.size === 0;

  const newReviews = reviews.filter((r) => !seen.has(r.reviewId));
  const missingIds = [...seen.keys()].filter((id) => !currentIds.has(id));

  // A missing review is only a confident removal if fewer new reviews
  // arrived than the poll window covers — otherwise it may have simply
  // scrolled past a full window of newer reviews.
  const confidentRemovals = newReviews.length < reviews.length ? missingIds : [];

  for (const r of reviews) seen.set(r.reviewId, { rating: r.ratingOverall, reviewedAt: r.reviewedAt });
  await writeFile(STATE_FILE, JSON.stringify([...seen]));

  if (isFirstRun) {
    console.log(`Seeded ${reviews.length} reviews.`);
    return;
  }

  const burst = detectBurst(reviews);
  console.log(`${newReviews.length} new, ${confidentRemovals.length} likely removed, ${burst.length} burst day(s) flagged.`);
}

Widening the sweep past limit

company-reviews allows limit up to 2000 and pages up to 100 in one request, a much higher ceiling than a platform like Trustpilot’s per-filter cap — a full weekly sweep of even a large company’s recent reviews fits in a handful of pages rather than requiring partitioning by rating:

async function fetchAllRecent(company, maxPages = 5) {
  const res = await fetch('https://api.fetchlayer.dev/glassdoor/company-reviews', {
    method: 'POST',
    headers: { Authorization: `Bearer ${API_KEY}`, 'Content-Type': 'application/json' },
    body: JSON.stringify({ company, sortBy: 'date', sortDirection: 'desc', pages: maxPages, limit: 30 * maxPages }),
  });
  return (await res.json()).reviews;
}

sortBy and sortDirection are applied locally over whatever window the request collected, not across the company’s entire review history — sorting by date desc on a request that only walks a few pages still only reorders those pages, which is exactly what you want for a recency-based poll.


Practical notes

  • employerResponses being empty doesn’t mean the employer never replies. Public replies are sparse across Glassdoor generally — treat a missing reply as “no reply on this review,” not as a signal about the employer’s overall responsiveness.
  • company takes an id or URL, not a name. Resolve a brand name to an id once via search-companies and cache it — passing a display name directly to company-reviews won’t resolve.
  • company-salaries reads only the first page of the upstream listing — there’s no pages or cursor on that endpoint. Treat its percentile figures as “the most-reported roles,” not a complete roster of every role at the company.

Next Steps