# G2 Reviews (`s-r/g2-reviews`) Actor

Give it a G2 product slug and get that product's reviews as rows: title, full body, star rating, pros, cons, date and the source URL. Page back through the review history and cap the count per run. G2 blocks unauthenticated requests, so bring your own proxy.

- **URL**: https://apify.com/s-r/g2-reviews.md
- **Developed by:** [SR](https://apify.com/s-r) (community)
- **Categories:** Business, Marketing
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.00 / 1,000 per-run start fees

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?

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

## G2 Reviews Scraper

Pull real customer reviews for any software product listed on G2 and get them
back as clean, structured rows: who wrote the review, what they do for a
living, how big their company is, the star rating they gave, the headline, the
full text split into what they liked and what they did not, and the date. Point
it at a product, say how many reviews you want, and press start.

G2 is where B2B software buyers go before they buy, which makes its review
corpus the closest thing the category has to a public record of what customers
actually think. This Actor turns that record into a dataset you can query,
chart, feed into a model, or drop into a spreadsheet.

### What you get

One row per review, de-duplicated, with these fields:

| Field | What it holds |
|---|---|
| `review_id` | G2's own identifier for the review. Stable across runs. |
| `review_url` | Direct link to that single review on G2. |
| `title` | The headline the reviewer wrote. |
| `rating` | Their star rating, on a 0 to 5 scale. Half stars occur, and 0 is a real rating. |
| `rating_scale_min`, `rating_scale_max` | Always 0 and 5, stated so you never have to guess the scale. |
| `reviewer` | Display name. Anonymous reviewers appear as "Verified User in ...". |
| `role` | Their job title, when they chose to show it. |
| `industry` | Their industry, when they chose to show it. |
| `company_size` | Small-Business, Mid-Market or Enterprise, with the headcount band. |
| `pros` | Their answer to "What do you like best?" |
| `cons` | Their answer to "What do you dislike?" |
| `problems_solved` | Their answer to "What problems is this solving for you?" |
| `body` | The three answers joined, for when you just want the text. |
| `date_published` | Full timestamp, ISO 8601. |
| `date_displayed` | The date exactly as G2 prints it on the card. |
| `product_name` | The product's name as G2 states it. |
| `product_rating` | The product's overall score, on a 0 to 10 scale. |
| `product_rating_best` | Always 10, for the same reason as above. |
| `product_review_count` | How many reviews G2 holds for the product in total. |
| `view`, `page`, `source_url` | Where the row came from, so any row is reproducible. |

#### About those two scales

G2 publishes two different numbers that both look like ratings, and mixing
them up is the most common mistake in G2 data. Each individual review is
scored **out of 5**. The product's headline score is **out of 10**. A product
showing 9.1 is not a 9.1-star product, and a review showing 4.5 is not
4.5 out of 10. This Actor reports both exactly as G2 states them and ships the
scale bounds alongside each one, so nothing downstream has to infer it.

The review scale starts at **0**, not at 1. Zero-star and half-star reviews
are real and they cluster in the lowest-satisfaction listing, which is
precisely the listing anyone doing a weakness analysis will select. Filtering
on `rating >= 1` would silently drop them.

### Input

| Field | Type | Default | Meaning |
|---|---|---|---|
| `productSlug` | string, required | `slack` | The product's identifier, taken from its own G2 address. For `https://www.g2.com/products/slack/reviews` the slug is `slack`. |
| `maxReviews` | integer | 100 | How many distinct reviews to return, at most. |
| `views` | array | `["default"]` | Which listings to walk. See below. |
| `region` | string | empty | Optional preferred region. Leave empty unless you have a reason. |

#### Getting more than 100 reviews

G2's review listing caps out at 100 reviews for any one ordering. That is a
property of G2, not of this Actor, and no scraper can page past it on a single
listing. What you can do is read a *different* listing of the same product,
which holds its own 100 and largely different reviews.

Four listings are supported, and you can select any combination:

- **Default ordering**, G2's own sort.
- **Newest first**, which surfaces reviews written in the last weeks.
- **Highest rated first**, which surfaces the strongest advocates.
- **Lowest satisfaction scores**, which surfaces the unhappiest customers.

Selecting all four gives you up to 400 distinct reviews for one product.
Overlap between listings is removed for you, so `maxReviews: 400` means 400
different reviews, not 400 rows with repeats in them. In our own runs, the
newest-first listing overlapped the default ordering by about 6 per cent and
the two rating-based listings barely overlapped it at all, so the fourth
listing is still adding new material rather than repeating the first.

If the product has fewer reviews than you asked for, you simply get what
exists and the run reports why it stopped. Nothing is padded.

### Output and the run summary

Reviews go to the dataset. The run also writes a summary record that tells you
what happened, which matters more here than it does for most scrapers:

```json
{
  "productSlug": "slack",
  "status": "ok",
  "reviewsReturned": 200,
  "productName": "Slack",
  "productReviewCount": 39649,
  "viewsWalked": [
    {"view": "default",     "pagesFetched": 10, "rowsNew": 100, "stoppedBecause": "view ceiling reached"},
    {"view": "most_recent", "pagesFetched": 10, "rowsNew": 94,  "rowsDuplicate": 6, "stoppedBecause": "view ceiling reached"}
  ],
  "dataTrustworthy": true
}
```

`status` is the field to check. It is one of:

- `ok` — reviews were returned.
- `no_reviews` — the product exists and genuinely has none matching your
  selection.
- `not_found` — there is no product at that slug. Check the slug against the
  product's own G2 address.
- `blocked` — the source did not serve this run. **No conclusion should be
  drawn about the product from this.**
- `failed` — the run could not start properly, usually a configuration problem.

That distinction is deliberate. A run that returns zero rows because a product
has no reviews and a run that returns zero rows because the source refused it
look identical if you only count rows, and they mean opposite things. This
Actor never lets the second one masquerade as the first: `dataTrustworthy`
tells you in one boolean whether the emptiness is a fact about the product.

The summary also reports `rowsMissingReviewer`, `rowsMissingRating` and
`rowsMissingBody`. On a healthy run those are zero or near it. If they are
large, the rows are suspect and you will know before you use them.

### Worked examples

**A quick look at one product.**
`{"productSlug": "notion", "maxReviews": 25}` returns 25 recent reviews in a
single short run.

**A full competitive read.**
`{"productSlug": "hubspot-crm", "maxReviews": 400, "views": ["default", "most_recent", "highest_rated", "lowest_nps"]}`
returns up to 400 distinct reviews spanning both the advocates and the
detractors, which is the shape you want for a win-loss analysis or a
positioning deck.

**Tracking sentiment over time.**
Run with `views: ["most_recent"]` on a schedule and de-duplicate on
`review_id` in your own store. Each run brings back whatever is new.

**Finding what to fix.**
`views: ["lowest_nps"]` and read the `cons` column. It is the shortest path
from a review corpus to a product backlog.

### Notes and limits

- The 100-per-listing ceiling is G2's. Four listings is the honest maximum for
  a single product, which is 400 reviews.
- Very large products have far more reviews than 400. This Actor does not
  claim otherwise, and `product_review_count` tells you the real total so you
  can see the gap yourself.
- Reviewer job title and industry are optional on G2. Roughly half of all
  reviewers leave one or both blank, and anonymous reviewers leave the name
  blank too. Blank means the reviewer withheld it, not that the Actor missed
  it.
- Reviews are returned as written, including typos and formatting. Nothing is
  rewritten, summarised or scored.
- Free-plan Apify accounts receive the first 10 rows of each run. Paid plans
  are uncapped.

### Operations

The Actor replays a session that is created outside the platform and handed
to it, because the platform's own browser is refused the session every time.
Three values travel together and the Actor refuses a partial set: the session
cookie, the user agent of the browser that created it, and the address
reference it was created through (`region:id`). They can be attached to the
Actor version as secret environment variables (`G2_MINT_COOKIE`,
`G2_MINT_USER_AGENT`, `G2_MINT_PROXY_SESSION`) or supplied on the input under
"Advanced", where the input wins.

To refresh the session, attach it and rebuild in one go:

```
set -a && source ~/.apify-agents.env && set +a
~/CascadeProjects/scraping_script/.venv/bin/python scripts/g2_mint_session.py --attach
```

Things to know:

- A session lives for a limited time and a limited number of requests, and the
  run summary says which of the two ran out (`status: blocked`, with the
  reason in the `errors` record). See the lifetime measured below.
- `credentialSource` in the summary says whether the run used the supplied
  session (`preminted`) or fell back to its own browser (`browser`). On the
  platform the fallback does not work, so `browser` there means the session
  was not attached.
- A redeploy with `scripts/deploy.py` replaces the version's environment
  variables and drops the three session values. Run the refresh command again
  after every deploy.
- The address reference is only valid for as long as the gateway keeps that
  address; a session whose address has gone reports itself as expired.

# Actor input Schema

## `productSlug` (type: `string`):

The product's identifier on G2, taken from its own URL: for https://www.g2.com/products/slack/reviews the slug is 'slack'. Case does not matter.

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

How many distinct reviews to return, at most. Each listing view holds up to 100, so going beyond 100 needs more than one view selected below. Products with fewer reviews than this simply return what they have.

## `views` (type: `array`):

Each view is a different ordering or filter of the same product's reviews and holds its own 100 reviews, largely different from the others. Select more views to go deeper than 100. Duplicates across views are removed for you.

## `region` (type: `string`):

Optional. Which European region the run should read from. Leave empty to let the Actor choose one, which is what most runs should do.

## `mintCookie` (type: `string`):

The session cookie value. Stored encrypted.

## `mintUserAgent` (type: `string`):

The browser user-agent string the session was issued to. Stored encrypted.

## `mintProxySession` (type: `string`):

The address reference the session was issued through, in the form region:id. Stored encrypted.

## Actor input object example

```json
{
  "productSlug": "slack",
  "maxReviews": 100,
  "views": [
    "default"
  ],
  "region": ""
}
```

# Actor output Schema

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

One row per distinct review.

## `output` (type: `string`):

Status, counts, which views were walked and why each one stopped.

## `errors` (type: `string`):

Failures with a code and a message. Absent when the run had none.

# 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 = {
    "productSlug": "slack",
    "maxReviews": 100,
    "views": [
        "default"
    ],
    "region": ""
};

// Run the Actor and wait for it to finish
const run = await client.actor("s-r/g2-reviews").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 = {
    "productSlug": "slack",
    "maxReviews": 100,
    "views": ["default"],
    "region": "",
}

# Run the Actor and wait for it to finish
run = client.actor("s-r/g2-reviews").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 '{
  "productSlug": "slack",
  "maxReviews": 100,
  "views": [
    "default"
  ],
  "region": ""
}' |
apify call s-r/g2-reviews --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,s-r/g2-reviews"
        }
    }
}
```

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/9eZuljq7hdz52GbLm/builds/C9TinOAfisVLv77l1/openapi.json
