# Biedronka Scraper: Polish Grocery Prices (`hexagone-data/biedronka-scraper`) Actor

Scrape zakupy.biedronka.pl product listings and pages: price in PLN, per-kg/per-litre price, pack size, bottle deposit, brand, category and images. Aisle or product URLs, pay per result.

- **URL**: https://apify.com/hexagone-data/biedronka-scraper.md
- **Developed by:** [Hexagone Data](https://apify.com/hexagone-data) (community)
- **Categories:** E-commerce, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$1.50 / 1,000 products

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

## Biedronka Scraper — Polish Grocery Product Prices

Collect product information from **zakupy.biedronka.pl**, the online shop of Biedronka, Poland's largest grocery chain (Jerónimo Martins). Paste aisle URLs for full product listings with prices, or product URLs for their product-page details. Products are deduplicated within the run.

All prices are served to an **anonymous visitor of the national online shop** (no store selected, no loyalty card). This verified context is attached to every record.

### Try it with 20 products

1. Paste an aisle URL into **Start URLs**.
2. Set **Maximum number of products** to `20`.
3. Start the Actor and open its dataset (the **Prices** view shows product, brand, price and per-unit price).
4. Read **Run summary** before exporting: a successful run can still contain rejected URLs or warnings.

```json
{
  "startUrls": [{"url": "https://zakupy.biedronka.pl/napoje-2/"}],
  "maxItems": 20
}
```

Set `maxItems` explicitly in API calls (`0` removes the limit). Listings are read page by page — 32 products per page — until the total announced by the site or your limit.

### Choose aisle or product URLs

| Input | Data |
|---|---|
| Aisle `…/<aisle>/` (any depth, e.g. `/drogeria/papier-toaletowy-i-chusteczki/papier-toaletowy/`) | Every product of the listing, paginated to the end, with id, name, brand, price, per-unit price, pack size and category |
| Product `…-<10 digits>.html` | The product-page record: same price fields plus availability (`inStock`/`outOfStock`) |

You can mix both modes. Explicit product URLs are processed first; a product-page record takes precedence over its duplicate listing record. A URL pasted with `?page=N` is read from its page 1.

Sorting (`srule`), filters (`prefn`, `prefv`, `pmin`, `pmax`), `pid`, search (`q`) and filtered-listing paths (containing `+`) are disallowed by the site's robots.txt and rejected with a reason. An invalid URL never fails the others.

### Output

| Field | Meaning |
|---|---|
| `productId` | Biedronka product id (tile `data-pid`), 10 digits. Deduplication key. |
| `name`, `brand`, `url`, `image` | Product identity as published by the site. |
| `price`, `priceCurrency` | Displayed price in PLN. Polish decimal comma converted: 7,99 zł → 7.99. The 0,00 zł cart total shown in the page header is never used as a product price. |
| `regularPrice`, `promotionLabel` | Always null in the current site version: the anonymous no-store view never displays a crossed-out price or a promotion label. If the site ever serves a non-standard price block, the page is refused rather than guessed. |
| `pricedPer` | What the displayed price buys, as shown next to it: `/szt.` (piece), `/but.` (bottle), `/opak.` (pack), `/100g`, `/kg`. |
| `unitPrice`, `unit` | Price per kg/l/szt. as displayed under the name (e.g. 7,99 zł / l → `unitPrice` 7.99, `unit` `l`). |
| `packaging` | Pack size as displayed: `1l`, `0.16kg`, `16szt.`, `ok. 350 g ~ 0.35kg`. |
| `deposit` | Returnable-bottle deposit (`kaucja`) in PLN when displayed. |
| `availability` | From product pages only (schema.org offer): `inStock` or `outOfStock`. Listings carry a CSS-driven template text, not a per-product state: null there. |
| `category` | Site category path, e.g. `Napoje > Soki > Soki świeże`. |
| `priceContext` | Verified no-store context (see above), with the page markers it was read from. |
| `position`, `page`, `sourceUrl`, `scrapedAt` | Collection provenance; `position`/`page` are null for product pages. |

An absent field is `null`, never an empty string. This Actor returns observations, not a maintained price history: retain `scrapedAt` with your exports.

#### Real output example

Collected on **22 October 2026**; it demonstrates the format, not today's price or stock.

```json
{
  "productId": "0000007412",
  "name": "Tymbark Sok 100 % multiwitamina 1 l",
  "brand": "Tymbark",
  "url": "https://zakupy.biedronka.pl/tymbark-tymbark-sok-100-multiwitamina-1-l-0000007412.html",
  "image": "https://zakupy.biedronka.pl/dw/image/v2/BKFJ_PRD/on/demandware.static/-/Sites-PL_Master_Catalog/default/dwdd18cf55/images/hi-res/8AF1972A2C849412BBCAFCD4DB6DA82C.jpg?sw=270&sh=270&sm=fit",
  "price": 5.95,
  "priceCurrency": "PLN",
  "regularPrice": null,
  "pricedPer": "/szt.",
  "unitPrice": 5.95,
  "unit": "l",
  "packaging": "1l",
  "deposit": null,
  "promotionLabel": null,
  "availability": null,
  "category": "Napoje > Soki > Soki owocowe 100%",
  "priceContext": {"state": "no_store_selected", "store": null, "deliveryPostalCode": null},
  "position": 1,
  "page": 1,
  "sourceUrl": "https://zakupy.biedronka.pl/napoje-2/",
  "scrapedAt": "2026-10-22T06:00:00+00:00"
}
```

### Coverage, limits and run summary

- The site serves **32 products per page** and announces its total; the Actor paginates with `?page=N` until that total. Beyond the last page the site returns the last page again instead of an empty page — the Actor detects this and stops the URL with a `partial_collection` status rather than looping or duplicating products.
- The site has no per-category search-engine ceiling in the measured configuration, but totals above a few thousand products were not measured; prefer narrower aisles for targeted collection.
- Availability and stock: the online shop requires a store choice before checkout; listings do not publish a per-product stock state. Do not treat results as local-store inventory.

If **Run summary** stays blank, open the run's **Storage → Key-value store** tab and click the eye icon on the **SUMMARY** row for the HTML report; **OUTPUT** holds the same summary as JSON, with the per-URL statuses (`done`, `no_products`, `not_found`, `stopped_at_limit`, `partial_collection`, …), rejected URLs with reasons, and automatic check warnings (empty list when everything is normal).

### Pricing

Pay-per-event: **$1.50 per 1,000 products** delivered to the dataset, prorated. A run that delivers nothing charges nothing. Your maximum charge per run is enforced by the platform: collection stops at that budget.

### Troubleshooting

| Symptom | Meaning |
|---|---|
| `rejected` URLs in the summary | Sorting/filter/search parameters or a non-catalog page: paste the plain aisle or product URL. |
| `no_products` | An information page or an aisle empty at read time. |
| `not_found`, `http_error`, `blocked_by_site`, `network_error` | The site refused or failed one URL; the others still ran. A proxy (Advanced section) helps if blocks repeat. |
| `partial_collection` | The site served something other than the requested page during pagination. |
| No product at all, failed run | Every URL failed: check the URLs, then retry later. |

# Actor input Schema

## `startUrls` (type: `array`):

zakupy.biedronka.pl pages to scrape. Accepted: aisle and sub-aisle listing pages (https://zakupy.biedronka.pl/napoje-2/, any depth) and product pages (…-<10 digits>.html). Sorting (srule), filters (prefn, pmin, pmax), pid and search (q) parameters are disallowed by the site's robots.txt and rejected with a reason. An invalid URL never fails the others.

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

Stops after this many unique products across all start URLs (0 = no limit). Listings are read page by page (32 products per page) until the announced total or this limit.

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

Not needed by default: the site answers the Chrome-impersonation client without a proxy. Enable it only if your runs get blocked.

## Actor input object example

```json
{
  "startUrls": [
    {
      "url": "https://zakupy.biedronka.pl/napoje-2/"
    }
  ],
  "maxItems": 20,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

## `records` (type: `string`):

All product records, one per product and source URL, with every field.

## `prices` (type: `string`):

Price view: product, brand, price in PLN, per-unit price, pack size.

## `summary` (type: `string`):

Readable per-URL summary: status, products delivered, reason for any URL that returned nothing.

## `summaryJson` (type: `string`):

The same summary as JSON (OUTPUT record), for programmatic use.

# 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 = {
    "startUrls": [
        {
            "url": "https://zakupy.biedronka.pl/napoje-2/"
        }
    ],
    "maxItems": 20
};

// Run the Actor and wait for it to finish
const run = await client.actor("hexagone-data/biedronka-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 = {
    "startUrls": [{ "url": "https://zakupy.biedronka.pl/napoje-2/" }],
    "maxItems": 20,
}

# Run the Actor and wait for it to finish
run = client.actor("hexagone-data/biedronka-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 '{
  "startUrls": [
    {
      "url": "https://zakupy.biedronka.pl/napoje-2/"
    }
  ],
  "maxItems": 20
}' |
apify call hexagone-data/biedronka-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,hexagone-data/biedronka-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/EqqvRZYqurOSPQ26c/builds/o80Vnq5iduWZjYnX1/openapi.json
