# Price Scraper for Any Product URL - Breaks 99% of Blocks (`s-r/universal-price-scraper`) Actor

Scrape the live price from any product URL, on any webshop. One URL in, the price out. Breaks through 99% of blocks. Up to 100 URLs per run: price, EAN/GTIN, brand, images, rating and specs. Blocked or non-product URLs are never charged.

- **URL**: https://apify.com/s-r/universal-price-scraper.md
- **Developed by:** [SR](https://apify.com/s-r) (community)
- **Categories:** E-commerce, Business
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$1.00 / 1,000 product delivereds

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

## Universal Price Scraper - Any Product URL, Any Webshop

**Paste product URLs from any online shop. Get the live price back.**

One scraper instead of one per retailer. It reads the product data shops already publish in their own page markup, so it works on shops it has never seen before rather than on a hand-maintained list.

#### Reaches 9 out of 10 shops

Measured, not claimed: **999 of 1,100 retail domains** returned a usable product page. It gets there by climbing a ladder of routes and browser identities, stopping at the first one that works, so an easy shop costs a fraction of a second and a hard one still comes back.

#### You are never charged for a row that is not a product

This is the part most price scrapers get wrong, and it is the reason to pick this one.

A dead product URL rarely returns an error. Shops quietly redirect it to the parent category, which still has a heading and a "from" price on it, and a naive scraper hands you that as your product and bills you for it. Challenge screens return a normal HTTP 200 with a title. Page footers parse as titles.

Every one of those is refused here, reported with the reason, and not charged.

### What you get

One row per URL, up to 33 fields:

| Field | What it holds |
|---|---|
| `price`, `price_currency` | Current price as a number, with its ISO currency |
| `price_original` | The pre-discount price, when the page shows one |
| `shipping_cost`, `price_with_shipping` | Shipping when the page states it, and the total |
| `title`, `brand`, `identifier` | Product name, brand, and the shop's own product code |
| `gtin`, `gtin_extra` | EAN / GTIN / UPC / ISBN, plus any further codes for variants |
| `availability`, `condition` | Stock status, and new / refurbished / used |
| `image`, `images` | Main image and the full gallery, with logos and icons filtered out |
| `rating`, `review_count` | Average score and number of reviews |
| `specs` | Specification table as name/value pairs |
| `description`, `category`, `categories` | Description and the breadcrumb path |
| `offers` | Every merchant offer, on price-comparison pages that list more than one |
| `seller` | Who is selling it, when the page names one |
| `buybox_*` | Amazon buybox fields. Populated only where the page publishes them; null otherwise |

### Input

```json
{
  "urls": [
    "https://www.mediamarkt.de/de/product/_apple-u4-cel-49-blk-ti-trbl-ob-3071784.html",
    "https://www.gamma.nl/assortiment/ring-battery-video-deurbel-plus/p/B237182"
  ]
}
```

Up to **100 URLs per run**. Anything past the hundredth is ignored and listed in the run's `OUTPUT` so you know exactly what was skipped.

Optionally set `country` to ask for a price from a particular country. Leave it on Automatic unless a shop changes price, currency or availability by where the visitor is.

When a country is named and the page is clearly priced from a different one, that row is reported free of charge rather than billed. `fetch_method` on every row shows which route produced the price.

Give it the actual product page URL. A category, search or listing page has no single product on it and will be reported as such.

A bare EAN or GTIN code is not a URL and has no page to read, so codes are skipped free of charge and listed in `OUTPUT.failed` rather than attempted. Use an EAN-to-offers scraper for code lookups.

### Output

```json
{
  "url": "https://www.gamma.nl/assortiment/ring-battery-video-deurbel-plus/p/B237182",
  "domain": "gamma.nl",
  "title": "Ring Battery Video Deurbel Plus",
  "brand": "Ring",
  "gtin": "0840268915315",
  "price": 149.99,
  "price_currency": "EUR",
  "availability": "InStock",
  "rating": 4.6,
  "review_count": 50,
  "images": ["https://..."],
  "specs": [{ "name": "Kleur", "value": "Zwart" }]
}
```

### You are not charged for rows you did not get

This is the part most price scrapers get wrong. A dead product URL rarely returns an error. Shops quietly redirect it to the parent category, which still has a heading and a "from" price on it, and a naive scraper hands you that as your product.

Three checks run before anything is delivered or billed:

- **The row has to carry a price or a product identifier.** A title on its own comes from category headings and page footers, and is not a result. This is what catches a dead URL that has been redirected to a category page: the page is real and has a heading, but it is not a product.
- **The page has to be a real page.** Interstitials and challenge screens return a normal HTTP 200 with a title on them. Those are reported as blocked, not as "this product has no price".
- **The page has to be the product you asked for.** If the URL redirected to a category page, a home page or a login screen, the row is rejected rather than returned as a different product.

Anything rejected appears in `OUTPUT.failed` with the reason, free of charge. A hundred URLs in can mean ninety rows out, and the run tells you which ten dropped and why.

### Coverage, measured

Across 41 live product URLs on 41 different shops in 12 countries: **31 rows delivered, 30 with a price.** A hundred URLs finish well inside the run timeout.

Of the ten that returned nothing, four were not our failure to report: three URLs had gone dead and now redirect to a category page, and one carried no price or product code at all. All four were rejected rather than guessed at, and none were billed. **On URLs that were still live products, 31 of 37 came back.**

Coverage is best on shops that publish structured product data, which is most of retail. A small number running the heaviest bot protection will not return data here.

### Good for

- Competitor price monitoring across a list of product pages
- Repricing feeds and margin checks
- Catalogue enrichment: turning a URL into an EAN, images and specs
- MAP and minimum-advertised-price compliance checks
- Filling gaps where a marketplace API gives you no price

### Notes

- Give it product URLs, not search or category URLs.
- `gtin` is only set when the shop publishes it unambiguously. A null means it could not be determined, never that a wrong code was returned.
- `offers` is populated on price-comparison pages that list several merchants. On an ordinary shop page there is one price, and it is in `price`.

# Actor input Schema

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

Product page URLs, one per line. Maximum 100 per run; anything beyond the hundredth is ignored and listed in the run's OUTPUT. Use the exact product page URL, not a category or search page.

## `country` (type: `string`):

Which country to appear to shop from. Only matters for shops that change price, currency or availability depending on where the visitor is. Leave on Automatic and the run picks whichever route reaches the shop fastest.

## Actor input object example

```json
{
  "urls": [
    "https://www.mediamarkt.de/de/product/_apple-u4-cel-49-blk-ti-trbl-ob-3071784.html",
    "https://www.gamma.nl/assortiment/ring-battery-video-deurbel-plus/p/B237182"
  ],
  "country": ""
}
```

# Actor output Schema

## `results` (type: `string`):

One row per URL that yielded a product. URLs that were blocked, that redirected off the product page, or that carried no price and no identifier are reported in OUTPUT.failed instead.

## `output` (type: `string`):

OUTPUT record with the run's counts, the route each product was fetched over, and the URLs that returned nothing.

## `errors` (type: `string`):

Failures with a code and a redacted message. Absent when the run had none.

# 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 = {
    "urls": [
        "https://www.mediamarkt.de/de/product/_apple-u4-cel-49-blk-ti-trbl-ob-3071784.html",
        "https://www.gamma.nl/assortiment/ring-battery-video-deurbel-plus/p/B237182"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("s-r/universal-price-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 = { "urls": [
        "https://www.mediamarkt.de/de/product/_apple-u4-cel-49-blk-ti-trbl-ob-3071784.html",
        "https://www.gamma.nl/assortiment/ring-battery-video-deurbel-plus/p/B237182",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("s-r/universal-price-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 '{
  "urls": [
    "https://www.mediamarkt.de/de/product/_apple-u4-cel-49-blk-ti-trbl-ob-3071784.html",
    "https://www.gamma.nl/assortiment/ring-battery-video-deurbel-plus/p/B237182"
  ]
}' |
apify call s-r/universal-price-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,s-r/universal-price-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/X5JhjtY6IMPeNaVUF/builds/u6YJ7CZRagAYQYlXv/openapi.json
