# Review Radar: G2 + Capterra SaaS Reviews & AI Battlecards (`outstanding_zinc/review-radar`) Actor

Scrape SaaS product reviews from G2 and Capterra into normalized records (rating, pros/cons, reviewer firmographics). Optional AI battlecards for competitor analysis: strengths, weaknesses, top complaints, and switching reasons.

- **URL**: https://apify.com/outstanding_zinc/review-radar.md
- **Developed by:** [Radar Tools](https://apify.com/outstanding_zinc) (community)
- **Categories:** AI, Lead generation, Developer tools
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per event

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

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

## Review Radar: G2 + Capterra SaaS Reviews & AI Battlecards

Scrape **public SaaS product reviews** from [G2](https://www.g2.com) and [Capterra](https://www.capterra.com) into **normalized review records**, and optionally generate one **AI battlecard** per product for competitor analysis (strengths, weaknesses, top complaints, switching reasons, comparison notes).

Built for competitive intel, win/loss research, RAG corpora, and agent workflows — without hand-copying review pages.

### Why Review Radar?

- **Dual-source in one run** — mix G2 and Capterra URLs, slugs, or names.
- **Normalized schema** — same fields across platforms (`rating`, `pros`, `cons`, `text`, reviewer role / company size when shown).
- **Safe Capterra resolution** — Capterra keys products by numeric `/p/<id>/`. Plain names (`capterra:HubSpot`) and wrong id+slug URLs are mapped via a curated catalog, or rejected with a clear `type: "error"` row. Never returns reviews from the wrong product.
- **Optional AI battlecard** — off by default; uses Apify's OpenRouter route (billed to the Apify account; no separate OpenAI/Anthropic key required on platform).
- **Filters** — max reviews, star buckets, published-after/before; pagination + dedupe by `reviewId`.
- **Proxy defaults** — Apify Proxy **UNBLOCKER** by default (no browser). Playwright fallback is off unless you enable it.
- Platform extras: scheduling, API, datasets (JSON/CSV/Excel), monitoring, `maxTotalChargeUsd` charge cap.

### What data can it extract?

| Field | G2 | Capterra | Notes |
|-------|----|----------|-------|
| platform | yes | yes | `g2` or `capterra` |
| productName / productSlug | yes | yes | |
| rating, date, title, pros, cons, text | yes | yes | |
| url / reviewId | yes | yes | when present |
| reviewerRole | yes | yes | when shown |
| reviewerCompanySize | yes | **N/A** | Capterra list pages do not expose company size reliably |
| reviewerIndustry | **N/A** | **N/A** | Not present on list / Turbo Frame / RSC pages we scrape |
| type=`battlecard` | optional | optional | AI summary row per product |

### Pricing (pay-per-event)

| Event | When | Price (USD) |
|-------|------|-------------|
| `actor-start` | Once per run | **$0.005** |
| `review-scraped` | Each kept review row | **$0.0015** |
| `battlecard-generated` | Each AI battlecard row | **$0.025** |

Platform compute + proxy (Unblocker) are separate Apify usage costs charged to the runner. Set **`maxTotalChargeUsd`** on the run to cap PPE spend. Always use a low `maxReviews` while testing.

**Example:** 1 product × 20 reviews, no AI ≈ `$0.005 + 20×$0.0015 = $0.035` PPE (plus proxy/compute).

### How to scrape G2 or Capterra reviews

1. Open this Actor in Apify Console (or call the API).
2. Paste one or more product review URLs or identifiers into **Product URLs or slugs**.
   - G2: `https://www.g2.com/products/notion/reviews`, `notion`, or `g2:notion`
   - Capterra: `https://www.capterra.com/p/186596/Notion/reviews/` (Notion id **186596**), `186596/Notion`, or `capterra:Notion`. Slack is `/p/135003/Slack/`
3. Set **Max reviews per product** (default **20** for cheap Store tests; pagination supports a few hundred).
4. Leave proxy on **UNBLOCKER** (default). Keep Playwright off unless debugging.
5. Optionally turn on **Generate AI battlecard**.
6. Click **Start** and download the dataset.

#### Example inputs

**Cheap Store test (matches the form prefill: Notion on G2 + Capterra):**

```json
{
  "productUrls": [
    "https://www.g2.com/products/notion/reviews",
    "https://www.capterra.com/p/186596/Notion/reviews/"
  ],
  "maxReviews": 20,
  "aiBattlecard": false
}
```

**Slack on both platforms (correct Capterra id 135003):**

```json
{
  "productUrls": [
    "https://www.g2.com/products/slack/reviews",
    "https://www.capterra.com/p/135003/Slack/reviews/"
  ],
  "maxReviews": 20,
  "aiBattlecard": false
}
```

**Names / slugs (catalog resolves common Capterra names):**

```json
{
  "productUrls": [
    "g2:asana",
    "capterra:Asana"
  ],
  "maxReviews": 50,
  "aiBattlecard": true
}
```

**Star + date filters:**

```json
{
  "productUrls": ["https://www.g2.com/products/slack/reviews"],
  "maxReviews": 100,
  "starRatings": ["1", "2", "3"],
  "publishedAfter": "2025-01-01",
  "aiBattlecard": false
}
```

### Output

Dataset items are `type: "review"`, `type: "battlecard"`, or `type: "error"` (block, mismatch, empty page, battlecard skip). Download as JSON, CSV, or Excel.

Example review:

```json
{
  "type": "review",
  "platform": "g2",
  "productSlug": "slack",
  "productName": "Slack",
  "rating": 4.5,
  "date": "2026-07-15T10:00:00.000Z",
  "title": "Default team chat",
  "pros": "Channels and integrations everywhere",
  "cons": "Noise without good hygiene",
  "reviewerRole": "Engineering Manager",
  "reviewerCompanySize": "501-1000"
}
```

#### Battlecard row (`type: "battlecard"`)

Existing string arrays stay as-is for compatibility. Newer fields stamp the filters used and attach verified quotes per bullet:

| Field | Meaning |
|-------|---------|
| `strengths`, `weaknesses`, `topComplaints`, `switchingReasons`, `comparisonNotes` | Short bullet strings (unchanged shape) |
| `filtersApplied` | Which filters produced the card: `starRatings`, `publishedAfter`, `publishedBefore`, `maxReviews`, plus `reviewsScraped` vs `reviewsSentToAi`, caps (`maxReviewsForAi` / `maxCharsForAi`), and `anyReviewTextTruncated` when a field was shortened to fit |
| `strengthsEvidence` (and parallel `*Evidence` fields) | Per-bullet `{ claim, supportingReviewCount, quotes[] }`. Quotes are verbatim substrings of scraped review text, with `date`, `rating`, `reviewId`, `reviewUrl` when available. Counts come from cited review indices, not a free LLM number. |
| `reviewCountAnalyzed` | Same as `filtersApplied.reviewsSentToAi` (kept for older integrations) |
| `summary`, `model`, `provider` | Overview text and which model wrote the card |

A one-star-only run is identifiable because `filtersApplied.starRatings` is `[1]` (or whatever stars you selected). Empty/`null` star or date filters mean “no filter applied.”

### Limits & tips

- Keep **UNBLOCKER** as the proxy group; residential/datacenter alone often get Cloudflare 403 on Capterra and DataDome on G2.
- Concurrency 1–2 is safest on challenge-prone sites.
- For Capterra, prefer `capterra:ProductName` or a verified `/p/<id>/` URL (examples: Notion `186596`, Slack `135003`, HubSpot CRM `152373`, Asana `184581`).
- AI battlecard aims to include every scraped review when possible (cap ~200 reviews / ~100k characters). Long fields are trimmed per review before any review is dropped; `filtersApplied` records coverage and whether any text was truncated. Leave AI off for the cheapest raw exports.
- Industry is **not available** on either source’s list pages; Capterra company size is **N/A**.
- Respect site Terms of Service and robots rules for your use case; this Actor scrapes publicly visible review content.

### FAQ

**Does it work with product names only?**\
Yes for common SaaS names via `capterra:Name` / `g2:slug`. Ambiguous or unknown Capterra names return an error row instead of wrong-product data.

**Why are some firmographic fields empty?**\
G2 sometimes shows role and company size; Capterra list/RSC pages typically do not expose company size; neither exposes industry on the pages we scrape. Empty/`null` is honest, not a bug.

**Will it burn my budget?**\
Use low `maxReviews`, keep `aiBattlecard` false, and set run **`maxTotalChargeUsd`**. The Actor stops charging further `review-scraped` events when the charge limit is reached.

**Do I need my own OpenAI key?**\
No on Apify — battlecards use `https://openrouter.apify.actor/api/v1` billed through the platform.

**Browser / Playwright?**\
Off by default. HTTP + Unblocker is the supported path; browser fallback is for debugging only and is slower/costlier.

# Actor input Schema

## `productUrls` (type: `array`):

G2 and/or Capterra product review URLs, slugs, or names. G2: full URL, slug, or g2:slug. Capterra: full /p/<id>/<slug>/ URL, id/slug, or capterra:Name (catalog resolves common SaaS names to the correct numeric id). Wrong id+slug combinations are corrected from the catalog or rejected with an error row — never wrong-product reviews.

## `maxReviews` (type: `integer`):

Upper bound of review rows to keep per product after filters. Paginated; a few hundred is supported.

## `publishedAfter` (type: `string`):

ISO date or timestamp. Only keep reviews on/after this date. Leave empty for all.

## `publishedBefore` (type: `string`):

ISO date or timestamp. Only keep reviews on/before this date. Leave empty for all.

## `starRatings` (type: `array`):

Only keep reviews in these 1–5 star buckets. Leave empty for all.

## `aiBattlecard` (type: `boolean`):

When on, after scraping a product, call Apify OpenRouter to produce one battlecard row (strengths, weaknesses, top complaints, switching reasons, comparison notes). Off by default. Bills a separate PPE event when monetized.

## `aiModel` (type: `string`):

Optional OpenRouter model id, e.g. openai/gpt-4o-mini. Used only when AI battlecard is on.

## `proxyConfiguration` (type: `object`):

Defaults to Apify Proxy group UNBLOCKER (required for reliable G2 + Capterra). Override groups only if you know what you are doing.

## `maxConcurrency` (type: `integer`):

How many products to scrape in parallel (1–5). Keep at 1–2 for challenge-prone sites.

## `requestTimeoutSecs` (type: `integer`):

Per-request timeout in seconds for HTTP fetches (and Playwright if enabled).

## `useBrowserFallback` (type: `boolean`):

Off by default. When on, retries empty/blocked first pages with headless Chromium (slow, expensive, often fails through Unblocker CONNECT).

## `debugProbeUrls` (type: `array`):

If set, Actor only fetches these URLs and stores PROBE_RESULTS then exits. For parser debugging.

## Actor input object example

```json
{
  "productUrls": [
    "https://www.g2.com/products/notion/reviews",
    "https://www.capterra.com/p/186596/Notion/reviews/"
  ],
  "maxReviews": 20,
  "aiBattlecard": false,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "UNBLOCKER"
    ]
  },
  "maxConcurrency": 1,
  "requestTimeoutSecs": 60,
  "useBrowserFallback": false
}
```

# Actor output Schema

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

No description

# 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 = {
    "productUrls": [
        "https://www.g2.com/products/notion/reviews",
        "https://www.capterra.com/p/186596/Notion/reviews/"
    ],
    "maxReviews": 20
};

// Run the Actor and wait for it to finish
const run = await client.actor("outstanding_zinc/review-radar").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 = {
    "productUrls": [
        "https://www.g2.com/products/notion/reviews",
        "https://www.capterra.com/p/186596/Notion/reviews/",
    ],
    "maxReviews": 20,
}

# Run the Actor and wait for it to finish
run = client.actor("outstanding_zinc/review-radar").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 '{
  "productUrls": [
    "https://www.g2.com/products/notion/reviews",
    "https://www.capterra.com/p/186596/Notion/reviews/"
  ],
  "maxReviews": 20
}' |
apify call outstanding_zinc/review-radar --silent --output-dataset

```

## MCP server setup

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

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/pBOu1oUqFbcdbMGuw/builds/PJZhoQ6Ve5mWuDcXb/openapi.json
