# Groupon Local Deals Scraper and New-Deal Alert (US) (`superslowsloth/groupon-deals`) Actor

Groupon local deals for any US city and category, one flat row each: title, merchant, price, original value, discount %, rating and review count, location, distance, deal URL - and whether each deal is new or re-priced since the last run.

- **URL**: https://apify.com/superslowsloth/groupon-deals.md
- **Developed by:** [Superslow Sloth](https://apify.com/superslowsloth) (community)
- **Categories:** E-commerce, Lead generation, Automation
- **Stats:** 1 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.40 / 1,000 deal scrapeds

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

## Groupon Local Deals Scraper and New-Deal Alert (US)

Read the deals on [Groupon](https://www.groupon.com/local)'s US city and
category pages, one flat row each, and see which ones are **new since your last
run** or have **changed price**. Give it cities (and optionally categories and
sort orders), schedule it, and turn on *Only new and re-priced deals* to get an
alert feed that costs nothing for deals you have already seen.

No account, no login, no Groupon API key. It reads the public local pages the
same way a browser does.

### Input

| Field | Notes |
|---|---|
| `cities` | City slugs as in `groupon.com/local/<slug>`: `los-angeles` (default), `chicago`, `new-york`, ... A full `groupon.com/local/<slug>` URL also works. |
| `categories` | Category slugs as in `groupon.com/local/<city>/<slug>`. Empty (default) reads only the plain city page. The eight top-level ones: `things-to-do`, `food-and-drink`, `beauty-and-spas`, `automotive`, `retail`, `personal-services`, `home-improvement`, `health-and-fitness`. Sub-categories such as `restaurants` also resolve. |
| `sorts` | Any of `relevance` (default), `rating`, `price:asc`, `price:desc`, `distance`. |
| `maxItems` | Stop after this many distinct deals (default 100). |
| `changesOnly` | Return and charge only deals that are new or re-priced since the last run for the same cities. |
| `proxyConfiguration` | **Required**, Apify datacenter proxy by default. A run with no proxy stops before it fetches or charges anything. |

#### How many deals a page gives, and why there are categories and sorts

Groupon's server renders only the first screen of deals for a page: **9** on a
city or category page (25 on the New York State page, 66 on one automotive
fetch). The rest are loaded by a call its own page makes in the browser, and
the `?page=N` parameter is ignored (pages 0, 2, 3, 50 and 500 of Los Angeles
each returned an ordinary first screen of 9 and never a later one, measured
2026-10-07). What does change the first screen is the **category page** and the
**sort order**: the Los Angeles city page plus its eight top-level categories,
each in all five sorts (45 requests), returned 49 distinct deals. So the way to
read more deals is a longer list of `categories` and
`sorts`, not a bigger `maxItems`.

Two more things measured on the same day:

- The same URL fetched twice can return a different first screen. Groupon
  A/B-tests two front ends (`next` and `tanstack`) and a different ranking
  comes with them. This actor reads the same fields from both.
- Groupon has no newest-first sort, so a "new" deal can show up anywhere in a
  feed, and a deal that was on Groupon all along can look new if an earlier run
  happened not to see it. The memory fills up over the runs: a longer list of
  categories and sorts makes that happen less.

#### Unknown cities

Groupon does not return a 404 for a city or category it does not have: it
answers 200 with the generic `/local` page (or the parent city). This actor
compares the page's canonical link with the one it asked for, discards the
tiles when they differ, and reports the page in the run log instead of labelling
another place's deals with your city.

### Output

| Field | Notes |
|---|---|
| `change` | `new`, `price_drop`, `price_rise` or `unchanged`, compared with the previous run for the same cities. |
| `deal_id`, `deal_uuid`, `url` | `deal_id` is the `/deals/<slug>` slug and is what change tracking keys on. `url` is `https://www.groupon.com/deals/<slug>` without the tracking query. |
| `title`, `merchant`, `merchant_location_count` | The merchant is the line above the title (a chain's name for a chain); `merchant_location_count` is the "(443 Locations)" beside it. |
| `price`, `currency` | Groupon's price, in USD. |
| `original_value` | The crossed-out value. Null when the tile shows none. |
| `discount_percent` | The percentage on the red badge as Groupon publishes it. With a sale price it is measured to the **sale price** (value 94, price 69, sale price 62.10 is "-34%"), so it is not always `1 - price / original_value`. |
| `sale_price` | The price after Groupon's sitewide sale, when the tile shows one. |
| `promo_price`, `promo_code`, `promo_ends_at` | The price with the promo code applied, the code, and when it ends (ISO-8601 UTC). |
| `valid_through` | When the deal stops being sold, from the page's schema.org data. Often absent. |
| `rating`, `rating_count` | Groupon's star rating and its number of ratings. Null for a deal with none. |
| `location`, `distance_text`, `distance_miles` | The tile's location line (a neighbourhood or a street address) and its distance as printed, plus the number when it is in miles. The distance is measured from Groupon's reference point for the page - the city centre on every page read - not from you. |
| `badge`, `is_sponsored` | The label on the tile's image ("Popular Gift", "10% Cashback"), and Groupon's own sponsored flag. |
| `image_url`, `redemption_location_id` | The tile image, and which location of a multi-location deal the tile links to. |
| `city`, `category` | The page the row was read from. A deal listed on several pages carries the first one this run read it on. |

#### Honest nulls

A field Groupon did not give is `null`, never `0` or `""`. `original_value` is
null for a deal shown without a value, `discount_percent` null for one without a
badge - a `0` would read as a deal with no discount, which is a different claim.
`rating` is null for a deal with no ratings, not 0 stars.

#### No "bought" count

The tile does not show how many people bought a deal, and neither the tile's
data nor the page's schema.org list carries it, so there is no such column.

#### Where the fields come from

Each tile carries Groupon's tracking JSON in a `data-bhd` attribute: title,
prices (in cents), discount, location line, rating, image. Merchant, distance
and badge are read off the tile's text. The deal expiry comes from the page's
schema.org ItemList, which can also list a deal that has no tile in the page
as received - such a deal is still returned, with the tile-only fields null.

### Change tracking

The memory (deal id to price) lives in a named key-value store on **your**
Apify account, `groupon-deals-watch`, so no one else can see it. It is keyed on
the **set of cities**, not on categories or sorts: those are only different
views onto the same city's deals, so adding a category reports that category's
unseen deals as `new` and leaves the ones already seen `unchanged`. The first
run for a set of cities labels every deal `new`. From the second on, a deal is
`new` if no earlier run saw it, `price_drop` / `price_rise` if `price` moved,
and `unchanged` otherwise. With `changesOnly`, unchanged deals are neither
returned nor charged.

### Pricing

Pay per event: $0.002 per run (`actor-start`, charged after your input is
validated, so a run that fails on bad input costs nothing), and **$0.0014 per
deal row** written (`deal-scraped`). With `changesOnly`, unchanged deals are not
charged.

### Proxy and blocking

Groupon is behind Cloudflare. A plain request is answered with HTTP 403 and a
"Just a moment..." challenge page; measured 2026-10-07 with a default client
from an ordinary connection, and from Apify's own IP address on both days it
was probed. Apify datacenter proxy exits were answered with the real page, so
the proxy is mandatory here. A 403, a 429, a 5xx, and a 200 that turns out to
be the challenge are all retried from a fresh exit address; a page that is
not Groupon's at all is treated the same way. The actor presents a browser's
TLS fingerprint (curl_cffi, current Chrome).

Each page is 0.8 to 1.3 MB of HTML, about 9 deals, so proxy traffic is
roughly 100 KB per deal.

# Actor input Schema

## `cities` (type: `array`):

Groupon city pages to read, written as the slug in groupon.com/local/<slug>: los-angeles, chicago, new-york, san-francisco. A full groupon.com/local/<slug> URL is accepted too. A city Groupon does not have is reported in the run log and skipped - Groupon answers it with a generic page, which this actor refuses to label as that city.

## `categories` (type: `array`):

Category pages to read for each city, as the slug in groupon.com/local/<city>/<slug>. The eight top-level categories every city page links to are things-to-do, food-and-drink, beauty-and-spas, automotive, retail, personal-services, home-improvement and health-and-fitness; a sub-category such as restaurants also works. Leave empty to read only the plain city page. Groupon renders only the first screen of deals (about 9) for each page, so more categories and sorts is how you get more deals.

## `sorts` (type: `array`):

Sort orders to read each page in; each one returns a different first screen of deals. Any of relevance (Groupon's default), rating, price:asc, price:desc, distance (nearest the city centre). There is no newest-first sort on Groupon.

## `maxItems` (type: `integer`):

Stop after reading this many distinct deals across all the pages. Each page read yields about 9, so the run reads at most a few dozen pages for the default of 100.

## `changesOnly` (type: `boolean`):

Return (and charge for) only deals that are new, or whose price changed, since the last run for the same cities; unchanged ones are skipped and cost nothing. The memory is per set of cities and lives in a key-value store on your account. The FIRST run has nothing to compare against, so it labels every deal new and returns them all.

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

Required. Groupon sits behind Cloudflare and refused Apify's own IP address with a 'Just a moment' 403 on both days it was probed, while Apify datacenter proxy exits were answered with the real page. The default is Apify datacenter proxy; a run with the proxy switched off stops before it fetches anything or charges anything.

## Actor input object example

```json
{
  "cities": [
    "los-angeles"
  ],
  "categories": [
    "things-to-do",
    "food-and-drink",
    "beauty-and-spas",
    "health-and-fitness"
  ],
  "sorts": [
    "relevance",
    "rating",
    "price:asc"
  ],
  "maxItems": 100,
  "changesOnly": false,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

## `items` (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 = {
    "cities": [
        "los-angeles"
    ],
    "categories": [
        "things-to-do",
        "food-and-drink",
        "beauty-and-spas",
        "health-and-fitness"
    ],
    "sorts": [
        "relevance",
        "rating",
        "price:asc"
    ],
    "proxyConfiguration": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("superslowsloth/groupon-deals").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 = {
    "cities": ["los-angeles"],
    "categories": [
        "things-to-do",
        "food-and-drink",
        "beauty-and-spas",
        "health-and-fitness",
    ],
    "sorts": [
        "relevance",
        "rating",
        "price:asc",
    ],
    "proxyConfiguration": { "useApifyProxy": True },
}

# Run the Actor and wait for it to finish
run = client.actor("superslowsloth/groupon-deals").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 '{
  "cities": [
    "los-angeles"
  ],
  "categories": [
    "things-to-do",
    "food-and-drink",
    "beauty-and-spas",
    "health-and-fitness"
  ],
  "sorts": [
    "relevance",
    "rating",
    "price:asc"
  ],
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}' |
apify call superslowsloth/groupon-deals --silent --output-dataset

```

## MCP server setup

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

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/3gq1ID4hiBHGbJXxI/builds/UlLQMJ19f1AeZOPnY/openapi.json
