Extract wine data from Vivino: ratings, reviews, prices, vintages, taste profiles. Filter by region (Burgundy, Bordeaux, Champagne...), grape variety, producer. Use it for wine list building, market research, cellar tracking, or producer comparison.
All notable changes to Vivino Wine Scraper are documented here.
v2.1.6 (2026-06-02)
Changed (simplify — DRY consolidation, no behavior change)
Adopted shared wine-core@0.5.0 helpers: summarizeQuality (replaces the inline computeNullRates+detectDrift wiring and its unsafe cast) and createErrorSink (replaces the local error-row accumulator/flush; charge.ts public API unchanged). Also flush accumulated error rows from main.ts's catch so telemetry survives a mid-run throw (L2).
Removed alcohol from the output. Vivino's Explore and Winery list APIs do not return ABV (it lives only on the per-wine detail page, which this actor does not fetch in standard mode), so the field was 100% null in practice and misleading. fizziness is retained — it is legitimately null for still wines but populated for sparkling. Audit 2026-06-01 finding X-3.
v2.1.4 (2026-06-02)
Added (SEV-3 observability — selector-drift early warning)
The OUTPUT KV record now carries fieldNullRates (per-field null-rate % over the result wines) and driftWarnings. When a drift-prone field (wineName, rating, regionName) exceeds 80% null on a run of ≥10 wines, the run logs a loud "possible selector drift" warning. This turns silent Vivino-response-shape changes from a 14-day blind window into a self-flagging run. Uses shared wine-core@0.4.0computeNullRates/detectDrift. Audit 2026-06-01 finding X-2.
v2.1.3 (2026-06-02)
Fixed (SEV-4 billing — error rows were auto-charged)
EXPLORE_ERROR / VINTAGE_ERROR / FETCH_ERROR rows are no longer written to the default dataset (where the synthetic apify-default-dataset-item event auto-billed them at $0.003 each). They are now accumulated and flushed to the KV store key run-errors, surfaced as errorCount in the OUTPUT summary. Infrastructure failures deliver zero value and must not be billed (Apify PPE doctrine). pushErrorItem → recordError + flushErrors. The no-vintage placeholder (a complete wine record minus vintage breakdown) remains a billed result — it carries real value. Audit 2026-06-01 finding X-1.
v2.1.2 (2026-06-01)
(Apify auto-bumped from 2.1.1 -> 2.1.2 at push time; a prior 2.1.1 build slot already existed on the platform.)
extractWineryWineData (src/winery.ts) now falls back from statistics.ratings_average -> statistics.wine_ratings_average (and the matching *_count siblings), matching the explore-path extractor in src/explore.ts (lines 130-131). Long-standing parity bug inherited from the JS monolith: when the Winery API omits the primary fields but populates the wine_ratings_* siblings, the wine's rating was null and silently dropped by the minRating filter even when Vivino did report a rating. RawWineryWine already declares both fields in src/types.ts:181-185.
Step 2 (Winery-API backfill in MODE B) now skips wineries that already saturated their representation during Step 1 (Explore). A new wineryWineCounts: Map<wineryId, number> is incremented in the Explore loop; wineries whose count is >= STEP2_SATURATION_THRESHOLD (5) are filtered out of the map passed to collectWinesFromWineries, with a log.info entry per skip. Previously every winery discovered in Step 1 was re-queried even when Vivino had already returned its top 5+ wines on the Explore page - paying for a second fetch + taste lookup per wine for marginal recall gain on saturated producers.
Tests
+2 unit tests in tests/winery.test.ts (rating fallback chain: wine_ratings_average used when primary missing; primary preferred when both present).
Total: 68 tests passing (was 66 in v2.1.0).
v2.1.0 (2026-05-31)
Changed
Adopted wine-core@0.3.0's createSpendingLimitChecker(pricePerItem) helper inside src/charge.ts. The stateful pushData counter + isSpendingLimitReached() public API is preserved (no consumer change in run.ts / winery.ts); only the internal env-parsing and limit-comparison logic is delegated to the shared helper (consolidation, no behavior change).
Build: tsup ESM bundling into dist/main.js (~51 KB). Adopts workspace conventions and wine-core for shared helpers (sleep, randomInt, getRandomUserAgent).
Charge: Actor.pushData(item) without explicit eventName (synthetic PPE event apify-default-dataset-item). Spending limit checked via manual estimate (rows times pricePerItem vs ACTOR_MAX_TOTAL_CHARGE_USD).
The legacy JS sources (src/main.js, src/regions.js) were removed. Git history preserves them.
Output schema (dataset_schema.json, input_schema.json) is unchanged: existing consumers see no row-shape difference.
PPE event (apify-default-dataset-item at $0.003) is unchanged.
v1.0.104 (2026-05-18)
Added
## FAQ section with 6 Q&A (cost per wine, legal, MCP integration, default maxWines rationale, wines without prices, rate-limit handling)
Changed
## Performance & costs H2 renamed to canonical ## Pricing
Pricing math reconciled with pay_per_event.json: cost table now reflects $0.003/wine event price (previous "$0.05 per 100 wines" implied $0.0005/wine, inconsistent with the actual PPE)
Vintage-mode cost added explicitly: ~10-20 rows/wine, $3 to $6 per 100 wines
Cross-promo table: "Wine Searcher" → "Wine-Searcher" (hyphenated), reordered to popularity-first phrasing, fixed "8 major critics" → "18"
Removed 2× etc. (sub-regions row, grape parameter row)
Removed all em-dashes (zero em-dash policy 2026-05-18)
v1.0.103 (2026-04-11)
Screenshot updated: the README header image now shows the v1.0.100+ form layout (4 primary filters at the root + 6 collapsed sections: Rating Range, Price Range, Regional Settings, Output Settings, Vintage Analysis, Advanced)
The PNG was uploaded to the existing Apify KV store (vivino-scraper-images) by replacing vivino-apify.png. The README URL is unchanged.
Local backup committed to screenshots/vivino-powerful-scraper.png for versioning
v1.0.102 (2026-04-11)
New "At a Glance" section at the top of the README: a visual ASCII mockup of the actual input form, mirroring the Apify console UI exactly (4 primary filters at the root + 6 grouped sections)
The mockup shows real prefill values (Bourgogne, White, 3.5, 100, 2005) and dropdown counts (299 regions, 21 grapes, 15 markets, 8 currencies)
Validates schemas: input_schema.json confirmed at 16 fields / 7 groups, parity 1:1 with the README; all .actor/*.json (input, output, dataset, pay-per-event) aligned with build 1.0.101+
v1.0.101 (2026-04-11)
README Input Parameters section rewritten to mirror the Apify console Input tab: four primary filters at the top, followed by six dedicated sections (Rating Range, Price Range, Regional Settings, Output Settings, Vintage Analysis, Advanced)
Every section caption, field order, and default value now matches input_schema.json exactly. One-to-one visual parity with the form shown in the Apify UI.
Documentation-only change: no behavior change, no schema change, no code change
v1.0.100 (2026-04-11)
Input form reorganized: the four primary filters (Region, Wine Type, Grape, Producer) are now at the root of the form for immediate visibility. The "Wine Filters" section has been removed.
New "Rating Range" section groups Minimum and Maximum Rating, mirroring the adjacent "Price Range" section for a consistent range-filter UX
No behavior changes: identical field names, defaults, and prefills. The automated daily test is unaffected.
v1.0.99 (2026-04-10)
Code cleanup: removed unreachable "Unknown region" warning (Apify input schema enum blocks invalid region slugs at HTTP 400 before reaching code)
Simplified regionIds/wineTypeIds/grapeIds initialization into single-line ternary expressions
Stress test: 5/5 runs on v1.0.98 SUCCEEDED. 100% success rate across volume, producer, vintages, obscure region, and separator validation scenarios.
v1.0.95 (2026-04-07)
Fixed price retrieval in Producer Search: wines searched by producer name now include marketplace prices (was always null)
New enrichWithExplorePrices() queries the Explore API with winery_ids[] filter after collecting wines from the Winery API
Tested on Domaine Coche-Dury: 14/17 wines enriched with prices (3 without = no marketplace listing on Vivino)
Discovered Explore API supports winery_ids[] parameter (array format, like region_ids[] and wine_type_ids[])
v1.0.92 (2026-04-06)
Major reliability update: 7 bug fixes. Proper Actor.exit(), content-type validation (CAPTCHA/geo-block detection), 30s fetch timeout, batch pushData (OOM-safe on 200k+ records), safe pushErrorItem, parseFloat edge case fix, countryCodes array coercion, fetchWithRetry unification, User-Agent bumped to Chrome 134.
Full taste profile with flavor keywords: primaryKeywords and secondaryKeywords with mention counts per group. Breaking change: flavors field replaced by tasteProfile. Winery wines enriched via /api/wines/{id}/tastes.
Earlier history (v1.0.0 to v1.0.80, 2025-12 to 2026-03-25)
Initial release with region/grape/wineType/producer filters, support for wines without prices via winery API, 299 regions across 32 countries, full taste profile with flavor keywords, faster HTML-based winery search, multiple reliability fixes (Actor.exit, batch pushData, fetch timeout, OOM prevention), and dataset table view rendering.