# SaaS Churn Signal 📉 (competitor win-back intel) (`tagadanar/saas-churn-signal`) Actor

Find where a competitor's customers are unhappy and what that competitor now charges. Negative-review themes (price, support, reliability, missing features, migration) from Capterra, the App Store and Google Play, joined to live pricing-page changes, one signal row per competitor.

- **URL**: https://apify.com/tagadanar/saas-churn-signal.md
- **Developed by:** [Tagada Data](https://apify.com/tagadanar) (community)
- **Categories:** Lead generation, Automation, AI
- **Stats:** 1 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $35.00 / 1,000 competitor signals

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?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## SaaS Churn Signal

Find where a competitor's customers are unhappy, and what that competitor now
charges. Point this actor at a competitor and it pulls their recent reviews from
Capterra, the Apple App Store and Google Play, sorts the negative ones into the
five reasons customers actually leave a SaaS product (price, support,
reliability, missing features, migration), and joins that to their live pricing
plans and any price change since your last run. You get one signal row per
competitor: the churn themes ranked, sample quotes, review volume and its trend,
the current plans and the last price move. It is a win-back and displacement
brief, not a review dump.

No LLM, no API key, no browser. The classification is a transparent keyword
heuristic, so it costs nothing per review and gives the same answer every time.

### Who uses this

- **Competitive intelligence and product marketing teams** building displacement
  battlecards: which pain points to lead with, and whether the rival just raised
  prices.
- **Sales teams running win-back and competitive deals** who want the specific
  complaint themes and quotes to use against an incumbent.
- **Founders and product managers** watching where a rival's customers are
  frustrated, and how the rival is repricing in response.

### What you get

One `signal` row per competitor, plus the underlying `review` and `pricing`
rows behind it, all in the same dataset (tell them apart by `recordType`).

#### `signal` row

| Field | Description |
| --- | --- |
| `competitor` | The competitor this signal is about |
| `reviewVolume` / `reviewsInWindow` | Reviews pulled, and how many fall in the trend window |
| `negativeReviews` / `negativeShare` | Count and share of negative (1-2 star) reviews |
| `averageRating` / `averageRatingNegative` | Mean rating over all, and over the negative tail |
| `volumeTrend` | `recent` vs `prior` window counts, `direction` (up/down/flat) and `pctChange` |
| `topChurnThemes` | Ranked themes, each with `negativeReviews`, `share` and up to three sample quotes |
| `churnThemeCounts` | Raw count per theme (price, support, reliability, missing-feature, migration) |
| `currentPlans` | The competitor's current plan names and prices |
| `lastPriceChange` | The most recent structured pricing change, or null if none since last run |
| `sourcesOk` / `sourcesFailed` / `partial` / `status` | Exactly which sources answered, and a plain-language status line |

#### `review` row

Each underlying review with `source`, `rating`, `negative`, its matched
`churnThemes` and `primaryTheme`, the `date`, `text` and a link back.

#### `pricing` row

The pricing snapshot: `plans` (name, price, period, per-user, features),
`priceTokens`, `parseStatus`, whether it is the `isBaseline` capture, and
`priceChanges` versus the previous run.

### How the churn themes work

Each negative review's text is scanned for the language customers use when they
are about to leave:

- **price** — expensive, overpriced, price increase, billing, value for money
- **support** — unresponsive support, no reply, slow response, poor service
- **reliability** — bugs, crashes, downtime, slow, sync errors, data loss
- **missing-feature** — lacks, no option to, limited, cannot, wish it had
- **migration** — switching, cancelling, exporting data, looking at alternatives

A review can match several themes, and one that matches none is counted under
`other`. A review counts as negative at 1-2 stars. The themes, volume and trend
on the signal row are always computed from every review pulled, even when you
choose to only keep the negative ones in the output.

### Input examples

Watch one competitor across every source (the prefill):

```json
{
  "competitors": [
    {
      "name": "Slack",
      "capterraUrl": "https://www.capterra.com/p/135003/Slack/reviews/",
      "appStoreId": "618783545",
      "playAppId": "com.Slack",
      "pricingUrl": "https://slack.com/pricing"
    }
  ]
}
```

Compare several competitors, reviews only, over a 90-day trend window:

```json
{
  "competitors": [
    { "name": "Asana", "capterraUrl": "https://www.capterra.com/p/136446/Asana/reviews/", "pricingUrl": "https://asana.com/pricing" },
    { "name": "Monday.com", "playAppId": "com.monday.monday", "pricingUrl": "https://monday.com/pricing" }
  ],
  "reviewsPerSource": 100,
  "negativeOnly": true,
  "sinceDays": 90
}
```

You do not need all four handles. Supply the sources you have, and each one you
supply is pulled and joined.

### Tracking price changes over time

The pricing side remembers each pricing page's last state between runs. The
first time it sees a page it records a baseline; every run after that reports
what changed (a price moved, a plan appeared or disappeared, a feature was added
to a tier) on the `pricing` row and as `lastPriceChange` on the signal. Schedule
the actor and you get a running churn-and-pricing feed on your competitors.

### Pricing

Pay per event, platform usage included, no subscription. Higher Apify plans get
automatic discounts. The valued unit is the joined signal per competitor; the
underlying rows are billed cheaply.

| Event | Price | When |
| --- | --- | --- |
| Run started | $0.001 | Flat, once per run |
| Competitor signal | $0.05 | Per competitor summary produced (the joined judgement) |
| Review scraped | $2.00 per 1,000 | Per underlying review row kept |
| Pricing snapshot | $0.01 | Per pricing page captured this run |

A source that is blocked or empty is never billed; the signal row says which sources answered.

### FAQ

**Does it use AI to read the reviews?** No. Themes come from a transparent
keyword heuristic, which is why it is free per review and deterministic. It is
deliberately simple: a review can be tagged with several themes, and a genuinely
off-topic complaint may land under `other`.

**What counts as a negative review?** One to two stars. The signal's themes are
drawn from those, while volume, trend and average rating use every review pulled.

**Do I need an API key or a login?** No. Every source is read from its own
public endpoint.

**How far back does it read?** As far as "Max reviews per source" allows, newest
first. The trend window is a separate setting and only controls the recency
comparison on the signal row.

**A source returned nothing, did the run fail?** No. If a source is blocked or
has no reviews, the signal row still ships with an explicit status saying which
source did not answer, so a partial answer never looks like a complete one.

**Can I feed it a list from another scraper?** Yes. Competitor entries are
matched by common field names (company, name, packageName, pricing and so on),
so piping another dataset in usually works without reshaping it.

***

Competitor churn analysis, SaaS win-back intelligence, negative review themes,
Capterra reviews, App Store reviews, Google Play reviews, competitor pricing
changes, displacement battlecards, churn risk signals, competitive intelligence,
customer complaint analysis, review sentiment by theme.

# Actor input Schema

## `competitors` (type: `array`):

One object per competitor. Each needs a <code>name</code> and at least one source handle: <code>capterraUrl</code> (a Capterra product/reviews URL), <code>appStoreId</code> (an App Store numeric id, id-string or app URL), <code>playAppId</code> (a Google Play package name or store URL) and <code>pricingUrl</code> (the competitor's pricing page). Give as many handles as you have — every source you supply is pulled and joined.

## `reviewsPerSource` (type: `integer`):

How many recent reviews to pull from each source (Capterra, App Store, Google Play) before summarizing. Newest first. Higher values give a steadier theme mix but cost more.

## `negativeOnly` (type: `boolean`):

When on (default), only the negative reviews (1-2 stars) ship as <code>review</code> rows and are billed. The churn themes, volume and trend on the <code>signal</code> row are always computed from every review pulled, whichever way this is set. Turn off to also keep the positive reviews in the output.

## `sinceDays` (type: `integer`):

The recency window used for the volume trend on the signal row: reviews in the last N days are compared against the N days before that. Does not limit how far back reviews are pulled (that is set by "Max reviews per source").

## Actor input object example

```json
{
  "competitors": [
    {
      "name": "Slack",
      "capterraUrl": "https://www.capterra.com/p/135003/Slack/reviews/",
      "appStoreId": "618783545",
      "playAppId": "com.Slack",
      "pricingUrl": "https://slack.com/pricing"
    }
  ],
  "reviewsPerSource": 10,
  "negativeOnly": true,
  "sinceDays": 180
}
```

# Actor output Schema

## `signals` (type: `string`):

One signal row per competitor plus the underlying review and pricing rows, in the default dataset.

# 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 = {
    "competitors": [
        {
            "name": "Slack",
            "capterraUrl": "https://www.capterra.com/p/135003/Slack/reviews/",
            "appStoreId": "618783545",
            "playAppId": "com.Slack",
            "pricingUrl": "https://slack.com/pricing"
        }
    ],
    "reviewsPerSource": 10,
    "sinceDays": 180
};

// Run the Actor and wait for it to finish
const run = await client.actor("tagadanar/saas-churn-signal").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 = {
    "competitors": [{
            "name": "Slack",
            "capterraUrl": "https://www.capterra.com/p/135003/Slack/reviews/",
            "appStoreId": "618783545",
            "playAppId": "com.Slack",
            "pricingUrl": "https://slack.com/pricing",
        }],
    "reviewsPerSource": 10,
    "sinceDays": 180,
}

# Run the Actor and wait for it to finish
run = client.actor("tagadanar/saas-churn-signal").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 '{
  "competitors": [
    {
      "name": "Slack",
      "capterraUrl": "https://www.capterra.com/p/135003/Slack/reviews/",
      "appStoreId": "618783545",
      "playAppId": "com.Slack",
      "pricingUrl": "https://slack.com/pricing"
    }
  ],
  "reviewsPerSource": 10,
  "sinceDays": 180
}' |
apify call tagadanar/saas-churn-signal --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,tagadanar/saas-churn-signal"
        }
    }
}

```

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/uFwTZdbSdd5Vb7v5p/builds/fMykBecCKeOdxoUkh/openapi.json
