# Changelog of Wolt Restaurant Menu Scraper (`teodor_banea/wolt-restaurant-menu-scraper`) Actor

- **URL**: https://apify.com/teodor\_banea/wolt-restaurant-menu-scraper/changelog.md
- **Full Actor documentation**: https://apify.com/teodor\_banea/wolt-restaurant-menu-scraper.md

## Changelog

All notable changes to this Actor are documented here.
This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

### \[0.1.0] - 2026-08-24

Initial release.

#### Added

- **Three scrape modes**
  - `search` — keyword search across a city via Wolt's search page endpoint.
  - `discovery` — the venues Wolt surfaces for a location via its front page endpoint.
  - `venueUrls` — the full menu of specific Wolt venue URLs.
    With `includeMenus`, the first two return menu items instead of venue rows.
- **Full menu extraction with prices.** One request returns a venue's whole assortment; items are
  flattened into rows carrying price, currency, pre-discount price, discount flag, VAT percentage,
  unit info and unit price for retail items, dietary preferences, Wolt+ exclusivity, image, the
  number of customization option groups, and `lowestPrice` where Wolt publishes the EU
  price-transparency figure.
- **Live city resolution.** City names resolve against Wolt's own list of 961 cities across 30
  countries, read at the start of the run, so new cities work the day Wolt adds them. Matching
  ignores case and accents, and an unmatched name fails with "did you mean" suggestions.
- **Menu rows carry their venue.** Beyond `venueId`/`venueSlug`/`venueName`/`url`, each menu item
  carries `venueCity`, `venueCountry`, `venueAddress`, `venueRating` and the `searchQuery` that
  surfaced the venue, so a billed export answers cross-venue price questions without a second
  lookup and a multi-keyword run stays attributable.
- **One dataset.** Everything goes to the run's default dataset and every row there is a result —
  menu items when menus are scraped, venue records otherwise, never a mix. There are no named
  datasets, for two reasons. Emitting venue records alongside menus for free undercut the venue
  modes, which bill per venue: a run capped at one result bought a single menu item and took a whole
  city's venue records with it. And a published Actor runs under Apify's LIMITED\_PERMISSIONS, where
  it may only touch its own default storages — creating a named dataset returns HTTP 403, which on
  the sibling Bolt Food Actor failed the request writing venue rows, meant no menus were ever
  queued, and ended the run with zero results. Failed requests are reported in the run log.
- **Optional store-catalog expansion.** Large Wolt Market venues answer the assortment endpoint with
  `loading_strategy: "partial"`, no items at all, and a tree of 30 to 300 branch categories whose
  `item_ids` are empty; the products sit one level down behind each leaf category slug. Without
  walking that tree such a venue returned nothing, so a run that happened to sample two grocery
  stores produced zero rows and still charged for starting. `expandCatalogs` walks the leaves,
  bounded at 60 requests per venue, and attributes each row to the category page it came from.
  Verified on a Maltese Wolt Market: 0 rows before, 200 rows across 26 categories after. Left off,
  the run names the venues it could not fully read.
- **No default caps.** `maxVenues`, `maxItemsTotal` and `maxMenuItemsPerVenue` all default to `0`,
  meaning no limit, so a run returns everything it finds rather than silently truncating. Input
  validation still fails before the first request.
- **Every row carries `runId`**, so exports from several runs can be merged and split apart again,
  which is what tracking prices over time needs. Null on a local run.
- 56 fixture-driven unit tests covering parsing, city resolution, currency conversion, the mappers,
  the assortment flattener, text cleaning and input validation.

#### Implementation notes

- `HttpCrawler` on `apify/actor-node:20` — no browser. All five endpoints were verified live on
  2026-08-24: the city list, the city front page, keyword search, the venue static page and the
  assortment endpoint.
- Wolt returns prices in ISO 4217 minor units and follows the per-currency exponent: HUF arrives
  with exponent 2, ISK with exponent 0. Conversion to major units respects that rather than dividing
  everything by 100.
- Result slots are claimed before awaiting a push, so concurrent handlers cannot together overshoot
  the user's result cap and bill for rows they capped away. Reaching the cap aborts the crawl rather
  than letting queued requests be fetched only for their handler to discard the response, and
  in-flight pushes are drained before the abort so no reserved row is counted without being written.
- Completion reports how many venues the menu rows actually span rather than how many were visited.
  On a capped run those differ by an order of magnitude, and the visited count would overstate the
  breadth of the data.
- Text is stripped of invisible characters before output. A sweep of 1,184 dishes across 15 Helsinki
  venues found no HTML but did find zero-width spaces inside McDonald's item names, which are
  invisible in a spreadsheet and still break grouping and comparison. Markup stripping is applied
  defensively for the same reason it is on Bolt Food, where merchants do paste HTML.
- Apify Proxy is always on, datacenter group, with nothing to configure. Wolt's API serves datacenter
  IPs normally, verified live.
- Memory is pinned at 1 GB, minimum and maximum, and the run timeout raised to 3600 seconds so a
  large uncapped run is not killed mid-flight.
- 403 stays retryable, since it usually means an IP-level block a rotated session can get past, while
  400/401/404/410 are terminal and skip retries.
- One dataset view listing every field. Two row types share one dataset schema, so a curated
  per-type view silently drops whatever the other type lacks and renders it as a handful of
  near-empty columns.
