Skip to content

About

Mercado Libre scraper for Node.js: query mercadolibre.com.mx by keyword from a backend, serverless function or webhook flow and get JSON listings with MXN prices, discount, seller and shipping promise. JavaScript example for the Devil Scrapes Apify Actor.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

Mercado Libre Search Results Scraper — JavaScript example

Mercado Libre Scraper for JavaScript and Node.js: Mexico listings as JSON for your app

Run on Apify JavaScript MIT license CI

Call the Mercado Libre Search Results Scraper from Node.js with this repository. It is one ES module on top of apify-client: you post a keyword, Apify runs a browser against mercadolibre.com.mx from a Mexican residential IP, and you receive plain objects for each organic listing with item ID, title, seller, rating, peso prices, markdown percentage, installment wording and delivery promise. None of that browser work lives in your service, which matters because the site checks visitors before it shows results and that check needs far more memory than a typical API container or serverless function has to spare. By default the rows are saved to results.json and results.csv; import the core call and they stay in memory for your own handler.

Actor: Mercado Libre Search Results Scraper · Publisher: Devil Scrapes · Also in Python: mercadolibre-scraper-python

🔥 What you get

  • Numbers, not strings, for money: salePrice and listPrice arrive as numbers in pesos, with currency set to MXN, so arithmetic works without a cleanup pass.
  • Discounts made explicit: when a card shows a markdown, the old price lands in listPrice and the percentage in discountPercent; full-price items carry null in both.
  • Organic matches only: paid placements, which point at click-tracking redirect links, are dropped before a row is built, so ad slots never pollute a price sample.
  • Spanish text kept as written: installmentsText and shippingText hold the exact strings from the card (for example a meses-sin-intereses offer), which keeps your own parsing honest.
  • Bounded depth: 48 results per page, at most 9 pages and 432 rows per run, controlled by maxItems and maxPages; whichever limit is hit first stops the run.
  • Loud failure instead of a silent empty file: if Mercado Libre's page check cannot be passed after a few fresh attempts, the run ends as failed with a status message saying so.
  • A search with no hits is not an error: the run succeeds with zero rows and, because the start event is charged with the first delivered rows, costs nothing.

🎯 Use cases

Price alerts for a reseller dashboard

A scheduled worker runs your tracked keywords, stores salePrice per itemId and notifies the user when a listing drops below a target. The recipe below shows the comparison step with nothing but Node built-ins.

A repricing input for your own store

If you sell on Mercado Libre yourself, feed competitor rows for your keywords into the service that sets your prices. Numeric prices mean your rules engine compares numbers directly instead of scraping digits out of strings.

Serverless lookups behind an API route

Expose an endpoint that accepts a phrase, starts a run without waiting and returns the run ID. Let an Apify webhook call your function when the run succeeds, then read the dataset there and push the results to the client or a cache.

Catalog enrichment for LatAm products

Match your SKUs to Mercado Libre listings by title and keep itemUrl next to them. Your product team gets a direct link to the live marketplace offer, with the current peso price and delivery wording attached.

⚡ Quick start

You need an Apify account and an API token from Console → Settings → Integrations. Apify's free plan includes monthly usage credit; this Actor's event prices are listed under Pricing below.

git clone https://github.com/devilscrapes/mercadolibre-scraper-javascript.git
cd mercadolibre-scraper-javascript
npm install
export APIFY_TOKEN=your_token_here
node index.js                       # uses input.json
node index.js my-input.json --out data --max-charge 2

The script starts the Mercado Libre Search Results Scraper, waits for it to finish, prints a link to the run in Apify Console and saves every result to results.json and results.csv. It exits with code 1 and the run's status message if the run fails.

🧩 The core call

The integration fits in a handful of lines. call() resolves once the run has finished, and listItems() pulls the stored rows from that run's default dataset.

import { ApifyClient } from 'apify-client';

const client = new ApifyClient({ token: 'YOUR_APIFY_TOKEN' });
const run = await client.actor('DevilScrapes/mercadolibre-scraper').call({"query": "laptop", "tld": "mx", "countryCode": "MX", "maxItems": 10, "maxPages": 1, "proxyConfiguration": {"useApifyProxy": true, "apifyProxyGroups": ["RESIDENTIAL"]}});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
console.log(items);

📥 Input

index.js forwards input.json unchanged. The sample asks for one page of laptop results and stops at 10 rows, a quick check that your token and the output mapping work before you spend more. In a service you would build this object per request; keep tld at mx, countryCode at MX and the residential proxy group, because the Actor validates them and the site needs them.

{
  "query": "laptop",
  "tld": "mx",
  "countryCode": "MX",
  "maxItems": 10,
  "maxPages": 1,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}
Field Type Required Default Allowed values
query string yes "laptop" —
tld string no "mx" mx
countryCode string no "MX" —
maxItems integer no 100 —
maxPages integer no 3 —
proxyConfiguration object no {"useApifyProxy": true, "apifyProxyGroups": ["RESIDENTIAL"]} —

📤 Output

The dataset is an array of flat objects, one per organic listing. Optional values such as rating, listPrice or shippingText are null when the card does not show them, never missing keys, so a TypeScript interface with nullable fields describes the shape exactly.

Field Type Always present
itemId string yes
title string yes
seller string yes
rating number or null no
listPrice number or null no
salePrice number yes
discountPercent integer or null no
installmentsText string or null no
shippingText string or null no
itemUrl string yes
currency string yes
query string yes
page integer yes
scrapedAt string yes

💰 Pricing

Event Price (USD)
Actor start $0.2
Result emitted $0.005

Prices as configured on 2026-10-16. The Actor page always shows the current price. Runs are billed to the Apify account that owns the token. Use --max-charge to cap what a single run can cost.

🛠️ Working with the data in JavaScript

This script keeps the last seen price per listing in last-prices.json, compares it with the fresh rows and prints the drops, largest first. Swap console.table for a fetch to Slack or your own webhook to turn it into an alert. Node.js built-ins only.

import { readFile, writeFile } from 'node:fs/promises';

const rows = JSON.parse(await readFile('results.json', 'utf8'));
const previous = new Map(
    JSON.parse(await readFile('last-prices.json', 'utf8').catch(() => '[]')).map((r) => [r.itemId, r.salePrice]),
);

const changes = [];
for (const row of rows) {
    const before = previous.get(row.itemId);
    if (before !== undefined && before !== row.salePrice) {
        const pct = Math.round(((row.salePrice - before) / before) * 1000) / 10;
        changes.push({ itemId: row.itemId, seller: row.seller, before, now: row.salePrice, pct, url: row.itemUrl });
    }
}

await writeFile('last-prices.json', JSON.stringify(rows.map(({ itemId, salePrice }) => ({ itemId, salePrice }))));
const drops = changes.filter((c) => c.now < c.before).sort((a, b) => a.pct - b.pct);
console.log(`${rows.length} listings checked, ${changes.length} price changes, ${drops.length} drops`);
console.table(drops.slice(0, 10));

❓ FAQ

Will this run inside a Lambda or Vercel function?

Your function only talks to the Apify API, so yes. The browser runs on Apify with 8 GB of memory; for longer searches start the run asynchronously and react to a webhook instead of holding the function open.

Are prices already numbers?

Yes. salePrice is always a number and listPrice is a number or null. Installment and shipping details stay as the Spanish text the card shows, so localise or parse them in your app.

What does a run that finds nothing look like to my code?

It finishes with status SUCCEEDED and an empty dataset, and its status message names the query. Treat it as a normal outcome rather than an exception path.

How do I deduplicate across scheduled runs?

Key on itemId, which looks like MLM followed by digits. itemUrl has the click-tracking fragment removed, so it is also stable enough to store as a link.

Can I cap what a run costs?

Yes. Pass --max-charge to index.js, or set maxTotalChargeUsd on the call options in your own code. Apify stops charging at that amount even if more rows were requested.

🧯 Troubleshooting

  • Set APIFY_TOKEN first: export the token in the same shell, or put it in a local .env file your shell loads. Never commit it.
  • Run finished with FAILED or ABORTED: open the printed Console link; the run log says why. Input validation errors name the field.
  • Fewer rows than expected: a --max-charge cap also limits how many results can be charged. Raise the cap or lower the limits in your input.
  • Something looks wrong in the data: open an issue on the Actor page so the maintainers see it.

🔗 More from Devil Scrapes

Related Actors:


MIT licensed example code. The Actor itself runs on Apify and is maintained by Devil Scrapes. Not affiliated with Mercado Libre or MercadoLibre, Inc..

About

Mercado Libre scraper for Node.js: query mercadolibre.com.mx by keyword from a backend, serverless function or webhook flow and get JSON listings with MXN prices, discount, seller and shipping promise. JavaScript example for the Devil Scrapes Apify Actor.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages