# ResQ Club Scraper - Surplus Food Offers & Venues (`abotapi/resq-club-surplus-food-scraper`) Actor

Scrape ResQ Club surplus food offers in Finland, Sweden and Estonia: name, price, current price, discount, portions left, pickup and order windows, tags and photo, plus the venue address, phone, website, coordinates and ratings. Search by area, name or country, or paste venue links.

- **URL**: https://apify.com/abotapi/resq-club-surplus-food-scraper.md
- **Developed by:** [Abot API](https://apify.com/abotapi) (community)
- **Categories:** E-commerce, Automation, Developer tools
- **Stats:** 2 total users, 1 monthly users, 83.3% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.20 / 1,000 offer or venue records

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

## ResQ Club Scraper - Surplus Food Offers & Venues

ResQ Club (resq-club.com) is the Nordic surplus-food marketplace where restaurants, cafes, bakeries, grocery stores and florists sell what would otherwise be thrown away, usually at a large discount. This scraper turns that live inventory into a dataset: every surplus offer on sale right now, with its price, its discount, how many portions are left, when it can be collected, and the venue behind it.

It covers the whole footprint in one run. Ask for a city, a radius, a venue name or a country, or paste the venue links you already care about, and get a flat row per offer with 46 columns. Turn on the venue directory option and you also get every partner venue that happens to have nothing listed today, which is how you build a complete map of the network rather than a snapshot of one morning.

### Why This Scraper?

- **46 columns per row**, covering the offer (name, description, price, current price, discount, portions left, pickup window, order window, tags, photo) and the venue behind it (name, description, address, country, coordinates, phone, website, rating counts).
- **Two ways in.** Search by area and radius, by venue name, or by country. Or paste venue links and read exactly those.
- **The whole network, not one city.** One run can cover Finland, Sweden and Estonia together; the venue list carried 3,403 partners when this actor was built.
- **Venue directory on demand.** Optionally return partner venues that list nothing right now, so the output is a directory of the network rather than only today's deals.
- **Sold-out offers are optional.** Off by default you get only what can still be bought; switch them on to measure how fast things go.
- **Built for scheduling.** Incremental mode returns only NEW, UPDATED, REAPPEARED and EXPIRED rows, so a daily run is cheap and the changes are the output.
- **Honest empty results.** A run that could not read the source fails loudly instead of returning an empty dataset that looks like "nothing on sale today".

### Data You Get

> Sample shape - values are illustrative placeholders, not from a live listing.

| Field | Example |
| --- | --- |
| `recordType` | `offer` |
| `offerId` | `10000001` |
| `url` | `https://app.resq-club.com/offer/10000001` |
| `offerName` | `Sample Surprise Bag` |
| `offerDescription` | `Sample description of what is inside the bag.` |
| `price` | `5.9` |
| `currentPrice` | `4.9` |
| `currency` | `EUR` |
| `discountPercent` | `16.9` |
| `unitsLeft` | `2` |
| `isSoldOut` | `false` |
| `pickupStart` | `2026-01-01T00:00:00.000Z` |
| `pickupEnd` | `2026-01-01T00:00:00.000Z` |
| `tags` | `["snack","dessert"]` |
| `imageUrl` | `https://resq-club.com/app/img/offers/00000000.jpg` |
| `venueId` | `00000001` |
| `venueUrl` | `https://resq-club.com/app/#provider/00000001` |
| `venueName` | `Sample Cafe` |
| `address` | `Sample Street 1, 00100 Sample City` |
| `country` | `fin` |
| `latitude` | `60.1700` |
| `longitude` | `24.9400` |
| `phone` | `+358000000000` |
| `website` | `https://example.com` |
| `reviewsTotal` | `33` |

Every row also carries each price in the currency's smallest denomination (`priceMinorUnits`, `currentPriceMinorUnits`), the order window (`orderStart`, `orderEnd`), the resolved tag labels (`tagNames`), the venue description, country name, distance from the search point, language, live offer and portion counters, the full rating breakdown (`reviewsGood`, `reviewsOkay`, `reviewsBad`, `positiveReviewPercent`), and the incremental quartet (`changeType`, `changedFields`, `firstSeenAt`, `lastSeenAt`): 46 columns in total.

A directory record (`recordType: "venue"`) carries every venue column above and leaves the offer columns empty.

### How to Use

**Everything on sale near a point**

```json
{
  "mode": "search",
  "locations": ["60.1699,24.9384"],
  "radiusKm": 5,
  "maxItems": 50
}
```

**A whole country, only bakery-style offers, including what is already gone**

```json
{
  "mode": "search",
  "countries": ["fin"],
  "tags": ["pastry", "dessert"],
  "includeSoldOutOffers": true,
  "maxItems": 200
}
```

Add `"includeVenuesWithoutOffers": true` to any search to also receive the partner venues that list nothing right now, which turns the same run into a full directory of the network. Those records are billed as the venue-directory event.

**Specific venues by link**

```json
{
  "mode": "url",
  "urls": [
    "https://resq-club.com/app/#provider/282",
    "1835"
  ],
  "maxItems": 50
}
```

**Daily monitoring, only what changed**

```json
{
  "mode": "search",
  "locations": ["Helsinki"],
  "radiusKm": 10,
  "incrementalMode": true,
  "emitExpired": true,
  "maxItems": 0
}
```

### Input Parameters

| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| `mode` | string | `search` | `search` picks venues out of the venue list; `url` reads only the venues you paste. |
| `locations` | array | (empty) | Search mode. One area per entry: `lat,lng` or a place name. Empty means every country. |
| `radiusKm` | integer | `25` | Search mode. How far from each area to look, 1 to 500. |
| `searchPhrase` | string | (empty) | Search mode. Keep only venues whose name contains this text. |
| `includeVenuesWithoutOffers` | boolean | `false` | Search mode. Also return venues that list nothing right now. Each such record is billed as the venue-directory event. |
| `urls` | array | (empty) | URL mode. Venue links such as `https://resq-club.com/app/#provider/282`, or bare venue ids. |
| `countries` | array | (empty) | Keep only `fin`, `swe` or `est` venues. Applies in both modes. |
| `tags` | array | (empty) | Keep only offers carrying one of these tag ids. Applies in both modes. |
| `includeSoldOutOffers` | boolean | `false` | Also return offers with no portions left. Applies in both modes. |
| `maxItems` | integer | `20` | Stop after this many records. `0` means no limit. |
| `maxPages` | integer | `0` | Optional. Venues are read 25 at a time; `0` goes as deep as the list allows. |
| `resumeFromRunId` | string | (empty) | Continue an interrupted run without repeating records it already returned. |
| `incrementalMode` | boolean | `false` | Return only what changed since the last run with the same settings. |
| `stateKey` | string | (empty) | Name a monitoring campaign, or deliberately share state between runs. |
| `emitUnchanged` | boolean | `false` | Also return UNCHANGED rows in incremental mode. |
| `emitExpired` | boolean | `false` | Also return EXPIRED rows in incremental mode. |
| `proxyConfiguration` | object | `{ "useApifyProxy": true }` | Apify Proxy settings. |
| `mcpConnectors` | array | (empty) | Optional MCP connectors to pipe results into. Never changes the dataset. |
| `notionParentPageUrl` | string | (empty) | Notion page under which item pages are created. Notion connector only. |
| `maxNotifyListings` | integer | `50` | Cap on items written to each connector per run. Does not affect the dataset. |

### Output Example

> Sample shape - values are illustrative placeholders, not from a live listing.

```json
{
  "recordType": "offer",
  "offerId": 10000001,
  "url": "https://app.resq-club.com/offer/10000001",
  "offerName": "Sample Surprise Bag",
  "offerDescription": "Sample description of what is inside the bag.",
  "price": 5.9,
  "currentPrice": 4.9,
  "currency": "EUR",
  "discountPercent": 16.9,
  "priceMinorUnits": 590,
  "currentPriceMinorUnits": 490,
  "unitsLeft": 2,
  "isSoldOut": false,
  "pickupStart": "2026-01-01T00:00:00.000Z",
  "pickupEnd": "2026-01-01T00:00:00.000Z",
  "orderStart": "2026-01-01T00:00:00.000Z",
  "orderEnd": "2026-01-01T00:00:00.000Z",
  "tags": ["snack", "dessert"],
  "tagNames": ["Snack", "Dessert"],
  "imageUrl": "https://resq-club.com/app/img/offers/00000000.jpg",
  "venueId": 1,
  "venueUrl": "https://resq-club.com/app/#provider/1",
  "venueName": "Sample Cafe",
  "venueDescription": "Sample venue description.",
  "address": "Sample Street 1, 00100 Sample City",
  "country": "fin",
  "countryName": "Finland",
  "latitude": 60.17,
  "longitude": 24.94,
  "distanceKm": 1.234,
  "phone": "+358000000000",
  "website": "https://example.com",
  "isBestOf": true,
  "isNewVenue": false,
  "venueLanguage": "fi",
  "venueOffersLeft": 2,
  "venueUnitsLeft": 3,
  "reviewsGood": 22,
  "reviewsOkay": 4,
  "reviewsBad": 7,
  "reviewsTotal": 33,
  "positiveReviewPercent": 66.7,
  "changeType": "NEW",
  "changedFields": [],
  "firstSeenAt": "2026-01-01T00:00:00.000Z",
  "lastSeenAt": "2026-01-01T00:00:00.000Z"
}
```

### Send results into your apps (MCP connectors)

Results can optionally be piped straight into the tools you already use, through Model Context Protocol (MCP) connectors. Authorize a connector once under Apify, Settings, API & Integrations, then select it in the `mcpConnectors` field. Notion receives a rich page per record; other connectors receive a best-effort write or a digest. Set `notionParentPageUrl` to tell the Notion export where to create the pages, and `maxNotifyListings` to cap how many records each connector receives in one run. Leaving `mcpConnectors` empty skips the export entirely, and the export never changes what lands in the dataset.

### Plan Requirement

Only public, unauthenticated venue and offer data is read; no account is created and nothing is purchased. Offers are short-lived by nature, so a venue that had five offers this morning can have none by the afternoon. Keep usage respectful of the source's terms.

### 🔗 Want more grocery data?

Pair this actor with these related scrapers from the same team:

<table>
<tr><td>🥦 <a href="https://apify.com/abotapi/foodhero-surplus-grocery-scraper"><b>FoodHero Scraper</b></a><br>Scrape FoodHero surplus grocery deals across Canada by area, store or offer ID. Extract...</td><td>🥦 <a href="https://apify.com/abotapi/instacart-grocery-price-scraper"><b>Instacart Scraper</b></a><br>Scrape Instacart grocery catalogs with per-store prices. Run keywords across several...</td></tr>
<tr><td>🥦 <a href="https://apify.com/abotapi/lidl-es-scraper"><b>Lidl.es Products, Weekly Offers, Prices &amp; Reviews Scraper</b></a><br>Scrape Lidl.es grocery and non-food products across weekly offers, tools, home, garden...</td><td>🥦 <a href="https://apify.com/abotapi/rewe-de-scraper"><b>REWE.de Scraper</b></a><br>Scrape REWE Germany (rewe.de) grocery products: current price, was-price and discount...</td></tr>
<tr><td>🥦 <a href="https://apify.com/abotapi/flashfood-grocery-deals-scraper"><b>Flashfood Scraper</b></a><br>Scrape Flashfood discounted groceries and stores by location or store. Extract current...</td><td>🥦 <a href="https://apify.com/abotapi/aldi-com-au-scraper"><b>ALDI AU Scraper</b></a><br>Scrape ALDI Australia (aldi.com.au) products: name, brand, price, was-price, savings...</td></tr>
</table>

👉 [Browse all abotapi scrapers](https://apify.com/abotapi)

### 💬 Support & custom scrapers

- 🐞 **Found a bug or a missing field?** Open a ticket on the [Issues tab](https://apify.com/abotapi/resq-club-surplus-food-scraper/issues/open). We usually reply within hours.
- 🛠️ **Need another site, extra fields or a private build?** Email <abotapi@proton.me> or message [Telegram @abotapi](https://t.me/abotapi).
- ⭐ **Enjoying it?** A quick review on the actor page helps other users find it.

# Actor input Schema

## `mode` (type: `string`):

Search picks venues out of the ResQ Club venue list by area, name and country. URL mode reads only the venue links or venue ids you paste.

## `locations` (type: `array`):

Search mode only. One entry per area: coordinates as lat,lng (60.1699,24.9384), or a place name (Helsinki, Tampere, Tallinn) which is looked up for you and cached. Leave empty to cover every country at once.

## `radiusKm` (type: `integer`):

Search mode only. How far from each area to look. Ignored when no area is given, and ignored in URL mode.

## `searchPhrase` (type: `string`):

Search mode only. Keep only venues whose name contains this text, matched without case. Ignored in URL mode.

## `includeVenuesWithoutOffers` (type: `boolean`):

Search mode only. Off by default, so only venues that currently advertise surplus are read. Turn it on to get the full partner directory as well, including venues with nothing listed today. This reads every venue in range instead of only the active ones and each directory record is billed as the venue-directory event. Ignored in URL mode, where a pasted venue always comes back.

## `urls` (type: `array`):

URL mode only. One venue per entry: a venue link such as https://resq-club.com/app/#provider/282, or just the number 282. Multi-URL supported. Area, name and directory settings are ignored here; the filters below still apply. An offer share link does not name a venue and is reported per entry.

## `countries` (type: `array`):

Keep only venues in these countries. Applies in BOTH search and URL mode. Leave empty for every country.

## `tags` (type: `array`):

Keep only offers carrying at least one of these tags. Applies in BOTH search and URL mode. Use the tag ids the source itself publishes, for example snack, meal, dessert, pastry, sandwich, grocery-bag, lactose-free, gluten-free, vegetarian. Leave empty for every tag.

## `includeSoldOutOffers` (type: `boolean`):

Off by default, so only offers with portions left are returned. Turn on to also return offers that are listed but already gone. Applies in BOTH search and URL mode.

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

Stop after this many records (0 = no limit; the run then stops when the venue list runs out).

## `maxPages` (type: `integer`):

Optional. Venues are read 25 at a time; 0 means as deep as the list goes, bounded by Max items.

## `resumeFromRunId` (type: `string`):

Paste a previous run ID or dataset ID to continue a large pull without returning records already collected there.

## `incrementalMode` (type: `boolean`):

Turn this on for daily or recurring monitoring. The first run returns every matching record as NEW. Later runs normally return only NEW, UPDATED and REAPPEARED records. Turn on Emit unchanged or Emit expired only when you also want those rows returned (and billed). State is kept separately for each search, filter and lookup setup; use State key to name or deliberately share a monitoring campaign.

## `stateKey` (type: `string`):

Optional. Name this monitoring campaign to keep its state stable, or deliberately share state across differently configured runs. Leave empty to let the actor derive a key automatically from the search and filter settings.

## `emitUnchanged` (type: `boolean`):

Off by default. Turn on to also return records that have not changed since the last run, marked UNCHANGED. This returns, and bills, extra rows you already have.

## `emitExpired` (type: `boolean`):

Off by default. Turn on to also return records that were present in a previous run but are no longer found, marked EXPIRED. Only produced once a run has fully scanned the tracked search.

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

Apify Proxy settings. Works on every proxy plan; leave the defaults unless you need a specific exit.

## `mcpConnectors` (type: `array`):

Optionally send results into the apps you already use, via Model Context Protocol (MCP) connectors. Authorize one under Apify, Settings, API & Integrations, then select it here. Notion gets a rich page-per-item export; other connectors get a best-effort write or digest. Leave empty to skip; never changes the dataset output. Supported: Notion (https://mcp.notion.com/mcp), Linear (https://mcp.linear.app/sse), Airtable (https://mcp.airtable.com/mcp), Apify (https://mcp.apify.com).

## `notionParentPageUrl` (type: `string`):

URL or id of the Notion page under which item pages are created. Required to enable the Notion export; ignored by other connectors.

## `maxNotifyListings` (type: `integer`):

Cap on items written to each connector per run. Does not affect the dataset.

## Actor input object example

```json
{
  "mode": "search",
  "locations": [
    "60.1699,24.9384"
  ],
  "radiusKm": 25,
  "includeVenuesWithoutOffers": false,
  "urls": [
    "https://resq-club.com/app/#provider/282"
  ],
  "tags": [],
  "includeSoldOutOffers": false,
  "maxItems": 20,
  "maxPages": 0,
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false,
  "proxyConfiguration": {
    "useApifyProxy": true
  },
  "maxNotifyListings": 50
}
```

# Actor output Schema

## `overview` (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 = {
    "mode": "search",
    "locations": [
        "60.1699,24.9384"
    ],
    "radiusKm": 25,
    "includeVenuesWithoutOffers": false,
    "urls": [
        "https://resq-club.com/app/#provider/282"
    ],
    "tags": [],
    "includeSoldOutOffers": false,
    "maxItems": 20,
    "maxPages": 0,
    "incrementalMode": false,
    "emitUnchanged": false,
    "emitExpired": false,
    "proxyConfiguration": {
        "useApifyProxy": true
    },
    "maxNotifyListings": 50
};

// Run the Actor and wait for it to finish
const run = await client.actor("abotapi/resq-club-surplus-food-scraper").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 = {
    "mode": "search",
    "locations": ["60.1699,24.9384"],
    "radiusKm": 25,
    "includeVenuesWithoutOffers": False,
    "urls": ["https://resq-club.com/app/#provider/282"],
    "tags": [],
    "includeSoldOutOffers": False,
    "maxItems": 20,
    "maxPages": 0,
    "incrementalMode": False,
    "emitUnchanged": False,
    "emitExpired": False,
    "proxyConfiguration": { "useApifyProxy": True },
    "maxNotifyListings": 50,
}

# Run the Actor and wait for it to finish
run = client.actor("abotapi/resq-club-surplus-food-scraper").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 '{
  "mode": "search",
  "locations": [
    "60.1699,24.9384"
  ],
  "radiusKm": 25,
  "includeVenuesWithoutOffers": false,
  "urls": [
    "https://resq-club.com/app/#provider/282"
  ],
  "tags": [],
  "includeSoldOutOffers": false,
  "maxItems": 20,
  "maxPages": 0,
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false,
  "proxyConfiguration": {
    "useApifyProxy": true
  },
  "maxNotifyListings": 50
}' |
apify call abotapi/resq-club-surplus-food-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,abotapi/resq-club-surplus-food-scraper"
        }
    }
}
```

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/WEOAy5gVQXLvc5paf/builds/na9piQ8HSD2CGB3BI/openapi.json
