Integration Guide

How to Monitor Trustpilot Reviews (New and Deleted)

Build an alert for new Trustpilot reviews — and for the ones that quietly disappear. Fingerprinting, the 200-review ceiling, and telling a real removal from a review that just scrolled off page one.

Written by Alex P.

  • Trustpilot monitoring
  • review alerts
  • reputation management
  • review removal
  • Trustpilot API

Most review-monitoring advice covers half the problem: get notified when a new review comes in. That half is table stakes. The half people skip is that Trustpilot reviews don’t always stay up — a company disputes one, a reviewer’s account gets removed, or Trustpilot pulls something during an investigation that resolves days or weeks later. If your monitoring only watches for additions, a review disappearing is invisible to you until a customer, or a lawyer, brings it up.

This guide builds both halves: an alert for new reviews, and a way to detect when the set of reviews you’re tracking shrinks — without false-alarming every time a company’s reviews scroll off page one because ten new ones arrived first.

This uses the Trustpilot Reviews API. If you haven’t pulled reviews before, start there for the full endpoint reference.


The architecture

Cron → company-reviews, sort: recency → diff against last snapshot → new reviews + missing reviews → alert

Sort by recency, not relevance. Relevance is Trustpilot’s own ranking and can reorder on its own between polls, which produces false “new review” alerts the same way it does on any platform with a re-ranking default. Recency gives you a stable, chronological read: anything genuinely new sorts to the top.


Fingerprinting reviews

Trustpilot’s company-reviews listing gives you rating, title, text, language, date of experience, author, and the company’s reply. Whether a stable review id comes back on every entry can vary by response shape, so build your diff on content rather than assume one:

function fingerprint(review) {
  // Author plus a text prefix plus the experience date is stable across
  // polls even without a dedicated id field on the listing.
  return `${review.author}::${review.dateOfExperience}::${review.text.slice(0, 80)}`;
}

If your response does carry a review id or permalink, prefer it — it’s a firmer key than content hashing. Either way, store fingerprints rather than full review bodies unless you need the text for later analysis; a Set of hashes stays small even for a company with tens of thousands of reviews.


A working monitor: new reviews

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

const API_KEY = process.env.FETCHLAYER_API_KEY;
const COMPANY = process.env.TRUSTPILOT_COMPANY; // e.g. "hellofresh.com"
const STATE_FILE = './trustpilot-state.json';
const ALERT_AT_OR_BELOW = 2;

async function fetchRecent(company) {
  const res = await fetch('https://api.fetchlayer.dev/trustpilot/company-reviews', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${API_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      company,
      sort: 'recency',
      limit: 50,
    }),
  });
  if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`);
  const { reviews } = await res.json();
  return reviews;
}

const fingerprint = (r) => `${r.author}::${r.dateOfExperience}::${r.text.slice(0, 80)}`;

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

  const reviews = await fetchRecent(COMPANY);
  const isFirstRun = seen.size === 0;

  const fresh = reviews.filter((r) => !seen.has(fingerprint(r)));
  reviews.forEach((r) => seen.add(fingerprint(r)));
  await writeFile(STATE_FILE, JSON.stringify([...seen]));

  // Cold start: don't page someone with 50 "new" reviews on the first run.
  if (isFirstRun) {
    console.log(`Seeded ${reviews.length} existing reviews.`);
    return;
  }

  const urgent = fresh.filter((r) => r.rating <= ALERT_AT_OR_BELOW);
  if (urgent.length) await alert(urgent);
  console.log(`${fresh.length} new, ${urgent.length} needing a response.`);
}

The 200-review ceiling, and why it matters here

Trustpilot serves at most 200 reviews per filter combination to an anonymous reader. For a company with a few thousand reviews, page one under sort: recency still covers you fine for day-to-day monitoring — new reviews are always at the top. But if you’re doing a wider sweep (backfilling history, or checking a specific segment), 200 is a hard wall per query, and raising limit past it won’t help.

The way past it is to partition by star rating, language, or date window and run several requests instead of one bigger one:

const stars = [1, 2, 3, 4, 5];
const allReviews = [];

for (const star of stars) {
  const res = await fetch('https://api.fetchlayer.dev/trustpilot/company-reviews', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${API_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      company: COMPANY,
      stars: [star],
      sort: 'recency',
      limit: 200,
    }),
  });
  const { reviews } = await res.json();
  allReviews.push(...reviews);
}

Five requests at 200 each covers up to 1,000 reviews per sweep — far more than one call at any limit you could ask for directly.


Detecting removals without false alarms

A fingerprint that was in yesterday’s snapshot and isn’t in today’s has one of two explanations: it was removed, or it scrolled off the window you’re polling (page one, 50 reviews) because enough newer reviews arrived first. Only the first one deserves an alert.

The fix is to only trust a “missing” signal when your poll window is wide enough to have kept it, given how many new reviews actually arrived:

function findMissing(previousFingerprints, currentReviews, currentFingerprints) {
  const missing = [...previousFingerprints].filter((fp) => !currentFingerprints.has(fp));

  // If fewer new reviews arrived than your poll window covers, anything
  // missing from that same window is a real removal, not a scroll-off.
  const newCount = currentReviews.filter((r) => !previousFingerprints.has(fingerprint(r))).length;
  const windowSize = currentReviews.length;

  return newCount < windowSize ? missing : []; // window was fully replaced — can't tell, skip
}

When you do get a confident “missing” result, it’s worth one more check before alerting: request the specific review by id or permalink (if you stored one) via the single-review lookup. A 404 there confirms removal; a successful read means it just aged out of your poll window’s date range but is still live.


Alerting

async function alert(reviews) {
  await fetch(process.env.SLACK_WEBHOOK_URL, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      text: `${reviews.length} new Trustpilot review(s) needing a response`,
      blocks: reviews.map((r) => ({
        type: 'section',
        text: {
          type: 'mrkdwn',
          text: `*${'★'.repeat(r.rating)}${'☆'.repeat(5 - r.rating)}* — ${r.author}\n${r.text.slice(0, 300)}`,
        },
      })),
    }),
  });
}

Tracking reply rate alongside new reviews

Since you’re already polling, repliedOnly and verifiedOnly are worth pulling into the same job to watch a reputation metric over time rather than just individual reviews:

async function replyRate(company) {
  const [all, replied] = await Promise.all([
    fetchRecent(company),
    fetch('https://api.fetchlayer.dev/trustpilot/company-reviews', {
      method: 'POST',
      headers: { 'Authorization': `Bearer ${API_KEY}`, 'Content-Type': 'application/json' },
      body: JSON.stringify({ company, repliedOnly: true, limit: 50, sort: 'recency' }),
    }).then((r) => r.json()),
  ]);
  return replied.reviews.length / all.length;
}

A dropping reply rate over a few weeks is often a leading indicator that support capacity hasn’t kept up with review volume — worth its own weekly log next to the alert pipeline.


Practical notes

  • Reviewer names are personal data. A display name is personal data under GDPR even though it’s shown publicly. If you’re storing history in the EU or UK, hash the fingerprint’s author component rather than keeping raw names unless you have a specific reason to.
  • dateWindow narrows what you search, not what you store. Use it (last30days, last3months) to keep a targeted sweep fast, but don’t rely on it alone for your removal check — a review outside the window you asked for isn’t “missing,” it’s just not part of that particular request.
  • Company is a domain, not a display name. Every route takes the company’s domain (hellofresh.com), not its Trustpilot display name. Use search-companies once to resolve a brand name you don’t already have the domain for.

Next Steps