# MercadoLibre Scraper - Products, Prices, Discounts (`s-r/mercadolibre-scraper`) Actor

Scrape MercadoLibre product listings across Argentina, Brazil, Mexico, Colombia, Chile and every other MercadoLibre site. Returns title, current price, previous price, discount percentage, item id, image, seller, shipping and rating from any category or deals page.

- **URL**: https://apify.com/s-r/mercadolibre-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

Pay per event

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?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## MercadoLibre Scraper

Pull product listings off MercadoLibre with the prices intact: what a thing
costs now, what it cost before the discount, and how deep the discount is.
Works across every MercadoLibre site, including Brazil's Mercado Livre.

Give it a category or deals URL and you get back one row per product card.

### Countries covered

Argentina, Brazil, Mexico, Colombia, Chile, Uruguay, Peru, Venezuela, Ecuador,
Bolivia, Paraguay, Costa Rica, Panama and the Dominican Republic. Each row
carries the site code (`MLA`, `MLB`, `MLM`, `MCO` and so on) and the country,
so a run across several markets stays sorted afterwards.

### What each product gives you

- `title` and `item_id`, MercadoLibre's own stable product id
- `price` as a number, plus `currency_symbol`
- `original_price` when the card shows a struck-through price, and `discount`
  as the label displayed, such as `40% OFF`
- `url` to the product page and `image_url`
- `badge` for cards flagged as bestsellers or promotions
- `shipping` and `installments` where the card shows them

MercadoLibre does not print a seller name, a star rating or a review count on
category cards, so this Actor does not return those three. They exist only on
the product detail page, which is a separate fetch per product.

- `source_url`, so every row traces back to the page it came from

### The three things this gets right that are easy to get wrong

**The discounted price is not the first price in the page.** On a discounted
card MercadoLibre renders the *old* price first, struck through, and the
current price after it. Reading the first amount you find gives you the
pre-discount figure labelled as the price, which is wrong in the most damaging
possible way: it looks completely plausible. Current and previous prices are
read from their own elements and reported separately.

**Thousands separators are not the same everywhere.** Argentina and Brazil
write two hundred thousand as `207.999`. Mexico writes one thousand two hundred
and ninety-nine as `1,299.00`. Apply either country's rule to the other's
number and you are off by a factor of a thousand. Each value is inspected for
which convention it uses rather than assumed, so a multi-country run comes back
comparable.

**A page with no products is not the same as a page that was refused.** Some
category URLs are navigation landing pages that genuinely hold no product grid.
Those are reported in the summary as URLs without a product grid. A page that
came back as a short placeholder instead of the real listing is reported as an
error explaining what happened. Both beat silently returning zero rows and
letting you assume the category was empty.

### Which URLs work

Category pages, deals pages, and any browse page that renders the product grid.
Open the page you want in a browser, confirm you can see products, and paste
the address.

Keyword **search** URLs (`listado.mercadolibre.com/...`) are not served as a
full page to anyone. Rather than return an empty result and let you guess, the
actor detects the placeholder and tells you so, naming the URL and suggesting a
category page instead. This is stated up front because it is the one thing you
might reasonably expect to work.

### Input

A list of URLs, and optionally:

- **Maximum products per URL** — an upper bound per page
- **Maximum URLs** — how many to process in one run
- **URLs in parallel** — how many to fetch at once
- **Retries** — attempts with backoff before a URL is reported failed
- **Request from country** — a two-letter code, when a site serves different
  prices or promotions by region

### Run summary

How many URLs were requested, how many produced products, which ones had no
product grid, the total returned, how many carried a price and how many were
discounted, plus a breakdown by country and an average price.

The average price comes with an explicit warning attached: prices are in each
site's own currency, so averaging across a multi-country run mixes pesos with
reais and means nothing. The summary says so in the output rather than leaving
you to notice.

### Scale and cost

Each URL costs exactly one page fetch. There is no pagination loop and no
per-product follow-up request, so a run of fifty category pages is fifty
requests and finishes in seconds. Four category pages across four countries
returned 176 products in under two seconds in testing.

What you get is what the page renders, which is typically 20 to 60 cards. The
actor does not claim to walk deeper than the page actually serves.

### What people use this for

**Competitive price tracking.** Run the same category URLs on a schedule and
join on `item_id`. Because `original_price` and `discount` come through
separately from `price`, you can tell a genuine price cut apart from a card
that simply started advertising a higher reference price, which is a
distinction a single price column cannot make.

**Promotion monitoring.** The deals pages are the fastest read on what a market
is discounting right now, and `badge` surfaces which items the site is pushing
as bestsellers.

**Cross-border comparison.** The same product often sits on several
MercadoLibre sites at very different prices. Run the equivalent category in two
countries, and `site` and `country` on every row keep the two sets separable
afterwards. Convert currencies yourself: the actor deliberately does not, since
a rate baked into the output would be stale the moment it was written.

**Catalogue building.** `item_id`, `title`, `image_url` and `url` are enough to
seed a product catalogue, and the id is stable across runs.

### A note on prices

Prices are read at the moment of the fetch. MercadoLibre runs frequent
time-limited promotions, so two runs hours apart will legitimately disagree,
and that difference is the data rather than an error. If you are tracking price
movement, keep `item_id` and the timestamp of your run: the id is stable, so
joining across runs is straightforward.

# Actor input Schema

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

Category, deals or browse URLs from any MercadoLibre site: Argentina, Brazil, Mexico, Colombia, Chile, Uruguay, Peru and the rest. Open the category you want in a browser and paste the address. Keyword search URLs (listado.mercadolibre.com/...) are not served to anyone and are reported as such rather than returned empty.

## `products_per_url` (type: `integer`):

Upper bound on rows returned for each URL. A category page typically renders between 20 and 60 product cards.

## `max_urls` (type: `integer`):

How many URLs to process in one run. Each costs one page fetch.

## `concurrency` (type: `integer`):

How many URLs to fetch at the same time.

## `retries` (type: `integer`):

Retries with backoff before a URL is reported as an error.

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

Optional two-letter country code to request from, for example ar or br. Useful when a site serves different prices or promotions by region. Leave empty to request from wherever the run happens to sit.

## Actor input object example

```json
{
  "urls": [
    "https://www.mercadolibre.com.ar/c/celulares-y-telefonos",
    "https://www.mercadolivre.com.br/c/eletronicos-audio-e-video"
  ],
  "products_per_url": 200,
  "max_urls": 50,
  "concurrency": 5,
  "retries": 3,
  "country": "ar"
}
```

# Actor output Schema

## `products` (type: `string`):

One row per product card.

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

Counts by country, how many carried a price and how many were discounted.

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

URLs that could not be read, with a code and a redacted message.

# 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.mercadolibre.com.ar/c/celulares-y-telefonos",
        "https://www.mercadolivre.com.br/c/eletronicos-audio-e-video"
    ],
    "products_per_url": 200,
    "max_urls": 50,
    "concurrency": 5,
    "retries": 3
};

// Run the Actor and wait for it to finish
const run = await client.actor("s-r/mercadolibre-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.mercadolibre.com.ar/c/celulares-y-telefonos",
        "https://www.mercadolivre.com.br/c/eletronicos-audio-e-video",
    ],
    "products_per_url": 200,
    "max_urls": 50,
    "concurrency": 5,
    "retries": 3,
}

# Run the Actor and wait for it to finish
run = client.actor("s-r/mercadolibre-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.mercadolibre.com.ar/c/celulares-y-telefonos",
    "https://www.mercadolivre.com.br/c/eletronicos-audio-e-video"
  ],
  "products_per_url": 200,
  "max_urls": 50,
  "concurrency": 5,
  "retries": 3
}' |
apify call s-r/mercadolibre-scraper --silent --output-dataset

```

## MCP server setup

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