ResQ Club Scraper - Surplus Food Offers & Venues
Pricing
from $1.20 / 1,000 offer or venue records
ResQ Club Scraper - Surplus Food Offers & Venues
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.
Pricing
from $1.20 / 1,000 offer or venue records
Rating
0.0
(0)
Developer
Abot API
Maintained by CommunityActor stats
0
Bookmarked
1
Total users
0
Monthly active users
2 days ago
Last modified
Categories
Share
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".
Use Cases
- Local deals apps and bots: pull live surprise-bag listings by city, radius or venue into your own app, map, or Telegram or Discord bot.
- Discount and availability tracking: watch price, current price, discount percent and portions left over time to see how fast a venue's offers move.
- Food waste and sustainability research: measure how many offers, portions and venues are active across Finland, Sweden and Estonia, and how that changes day to day.
- Partner directory and outreach: turn on the venue directory option to build a full list of participating cafes, bakeries, grocery stores and florists, with address, phone and website.
- Daily monitoring and alerting: schedule incremental runs so each morning's run returns only the offers that are new, changed or gone since yesterday, ready to feed a notification.
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://s3-eu-west-1.amazonaws.com/partner-images.resq-club.com/images/00/000000/sample.jpg |
venueId | 1 |
venueUrl | https://resq-club.com/app/#provider/1 |
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
{"mode": "search","locations": ["60.1699,24.9384"],"radiusKm": 5,"maxItems": 50}
A whole country, only bakery-style offers, including what is already gone
{"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
{"mode": "url","urls": ["https://resq-club.com/app/#provider/282","1835"],"maxItems": 50}
Daily monitoring, only what changed
{"mode": "search","locations": ["Helsinki"],"radiusKm": 10,"incrementalMode": true,"emitExpired": true,"maxItems": 0}
Run it from your code
Python:
from apify_client import ApifyClientclient = ApifyClient("<YOUR_APIFY_TOKEN>")run = client.actor("abotapi/resq-club-surplus-food-scraper").call(run_input={"mode": "search", "locations": ["Helsinki"], "radiusKm": 10, "maxItems": 50})for record in client.dataset(run["defaultDatasetId"]).iterate_items():print(record["offerName"], record["currentPrice"], record["unitsLeft"])
JavaScript:
import { ApifyClient } from 'apify-client';const client = new ApifyClient({ token: '<YOUR_APIFY_TOKEN>' });const run = await client.actor('abotapi/resq-club-surplus-food-scraper').call({mode: 'search', locations: ['Helsinki'], radiusKm: 10, maxItems: 50 });const { items } = await client.dataset(run.defaultDatasetId).listItems();
Or connect it to Make, Zapier, n8n, Google Sheets or webhooks from the Integrations tab.
How Max items bounds a run
Max items stops the run once that many rows have been returned, not once that many venues have been scanned or offers inspected: a search that filters out most of what it scans still returns exactly Max items matching rows when enough exist, and scanning more to find them costs nothing extra.
Incremental mode's EXPIRED tombstones are one exception worth knowing about. A run is only allowed to tombstone at all once its live scan finishes under the cap, but once it is allowed, every tracked record missing from that scan is reported EXPIRED regardless of Max items. A capped, incremental run with emitExpired on can therefore push, and be billed for, more rows than Max items declares, some of them tombstones rather than live offers. If a hard ceiling on a single run's cost matters, turn emitExpired off, or plan headroom above Max items for the campaign's expected tombstone count.
Resume and recurring updates
Two different tools for two different situations, and today they do not talk to each other.
Resume (resumeFromRunId) is meant to continue one interrupted pull. As currently wired, pasting a previous run or dataset ID only feeds the incremental-mode bookkeeping: it protects records that run already returned from being wrongly marked EXPIRED if this run's own scan does not happen to revisit them. It does not stop this run from re-fetching, re-extracting and re-pushing, and re-billing, a record the earlier run already delivered, even though the run log describes it as "skipping" those records. Do not rely on resumeFromRunId alone to avoid paying twice for an interrupted large pull.
Incremental mode (incrementalMode) is the tool that actually avoids re-billing on a recurring schedule. Each record is classified NEW, UPDATED (with changedFields), UNCHANGED (suppressed and not billed unless emitUnchanged is on), REAPPEARED or EXPIRED (only after a run that scanned the whole scope, and only with emitExpired), keyed by stateKey or a key derived automatically from the search and filter settings. Turning on resumeFromRunId together with incrementalMode on the first incremental run for a scope does not fold the resumed run's records into the new baseline: they are fetched, extracted and classified fresh, typically as NEW, on top of anything resumeFromRunId already contributed. Set up incremental mode's own stateKey for a recurring campaign instead of leaning on resume for that purpose.
If an UPDATED row's changedFields list is only countryName, currency or tagNames, treat it as a data-source hiccup rather than a real change: it usually means the country and tag lookup document could not be read for that run, so those three fields fell back to empty while the offer and venue themselves did not actually change. Expect the same thing once more on the next healthy run, when those three fields fill back in: every offer the hiccup touched is reported, and billed, as UPDATED on both runs.
Only share a stateKey between runs that cover the same area and filters. The key decides which baseline a run is compared against, and with emitExpired on, every tracked record the current run did not visit is reported EXPIRED. Reusing one key for a wide search and then a narrower one (a smaller area, a venue-name phrase, a country filter) therefore returns, and bills, EXPIRED rows for offers that are still on sale, simply because the narrower run never looked at them. Leave stateKey empty and each distinct search keeps its own baseline automatically.
Place names that cannot be looked up widen the search instead of failing it. When a place name in locations cannot be turned into coordinates, that entry is skipped with a warning in the run log. If that happens to every entry, the run does not fail and does not return nothing: with no area left to filter by, it searches every venue in every country (the other filters still apply), returns and bills up to Max items rows from the whole network, and still ends with an ordinary "Returned N record(s)" status. With incremental mode on, that whole-network result also becomes the stored baseline for that place name, so the next run where the name does resolve reports everything outside the area as EXPIRED (when emitExpired is on). To rule this out, give areas as lat,lng coordinates rather than place names, and keep a sensible Max items on place-name searches.
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. If no place name can be looked up, the run searches every country instead (see "Resume and recurring updates" above). |
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. Leave it off in URL mode: a pasted venue with no offer comes back either way, but with this switched on its row is also 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) | Paste a previous run or dataset ID. Protects its records from being wrongly marked EXPIRED this run; does not by itself stop them from being re-fetched and re-billed (see "Resume and recurring updates" above). |
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. Sharing one key between runs with different areas or filters produces billed EXPIRED rows for offers that are still live (see "Resume and recurring updates" above). |
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.
{"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://s3-eu-west-1.amazonaws.com/partner-images.resq-club.com/images/00/000000/sample.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. The default connection setting works out of the box; no specific proxy group is required for this source. 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.
FAQ
How much does it cost?
You pay per record returned (each offer, plus each extra directory row when the venue directory option is on), plus a one-time actor-start charge. Directory rows also carry the venue-directory event; in URL mode that event is only avoided by leaving the venue directory option off. The Pricing tab shows the current rates. Use Max items to cap the cost of a live scan, and see the "How Max items bounds a run" note above for the one case, EXPIRED tombstones under incremental mode, where a capped run can still return more rows than Max items.
Is it legal to scrape ResQ Club?
This actor collects only publicly available offer and venue data. You are responsible for how you use it: follow ResQ Club's terms and the laws that apply to you, and get legal advice if you plan commercial redistribution.
Can I get only new or changed offers on a schedule?
Yes. Schedule the actor from the Schedules tab and turn on Incremental mode with a stable State key. Each run then returns only new, updated, reappeared and, if you turn on Emit expired, expired records, and unchanged ones are not billed unless you also turn on Emit unchanged.
Does resumeFromRunId skip offers I already collected?
Not on its own. Today it only protects those records from being wrongly marked EXPIRED if this run does not happen to revisit them; it does not stop this run from re-fetching and re-billing them. For a recurring pull that should not repeat what it already delivered, use Incremental mode with a stable State key instead, since that is the mechanism that actually suppresses, and does not bill, unchanged records.
Why did my run fail instead of returning an empty dataset?
If the venue index, or every venue this run tried to read, could not be reached, the run stops with a clear message so "nothing on sale" is never confused with "nothing could be read". Run it again in a few minutes. If only some venues or pasted entries could not be read, you still get what was found, and the run's status message discloses the shortfall.
Can I use it with AI agents or MCP?
Yes. Call it from any Apify integration or MCP client, and use the connector field to push results into Notion, Linear or Airtable.
π Want more grocery data?
Pair this actor with these related scrapers from the same team:
| π₯¦ FoodHero Scraper Scrape FoodHero surplus grocery deals across Canada by area, store or offer ID. Extract... | π₯¦ Flashfood Scraper Scrape Flashfood discounted groceries and stores by location or store. Extract current... |
| π₯¦ Instacart Scraper Scrape Instacart grocery catalogs with per-store prices. Run keywords across several... | π₯¦ Lidl.es Products, Weekly Offers, Prices & Reviews Scraper Scrape Lidl.es grocery and non-food products across weekly offers, tools, home, garden... |
| π₯¦ REWE.de Scraper Scrape REWE Germany (rewe.de) grocery products: current price, was-price and discount... | π₯¦ ALDI AU Scraper Scrape ALDI Australia (aldi.com.au) products: name, brand, price, was-price, savings... |
π Browse all abotapi scrapers
π¬ Support
- π Found a bug or a missing field? Open a ticket on the Issues tab. We usually reply within hours.
- π οΈ Need another site, extra fields or a private build? Email contact@abotapi.com.
- β Enjoying it? A quick review on the actor page helps other users find it.