# Merkandi Scraper | Wholesale Offers & Price Monitor (`cauldo/merkandi-offer-monitor`) Actor

Export Merkandi wholesale deals with prices, units, stock, minimum orders, countries and manifest links. Filter searches and track new offers or price/stock changes. JSON/CSV, no login. $1/1,000 basic or $3/1,000 detailed offers; no start fee.

- **URL**: https://apify.com/cauldo/merkandi-offer-monitor.md
- **Developed by:** [Cauldo](https://apify.com/cauldo) (community)
- **Categories:** E-commerce, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.00 / 1,000 basic offer saveds

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

## Merkandi Scraper — Wholesale Offers & Price Monitor

Export public Merkandi wholesale offers into a usable dataset, or watch a search for new deals and changes to advertised prices and stock. Get prices **with their units**, minimum orders, supplier countries, product conditions, brands, descriptions, image galleries, payment/shipping options and public manifest links. No Merkandi login is required.

**Pricing: $1 per 1,000 basic offers, or $3 per 1,000 detailed offers. No start fee.** A detailed offer replaces the basic event; it does not incur both. Residential proxy access, retries, comparisons and exports are included. Duplicates, filtered offers, unavailable products and unchanged records skipped by monitoring are not charged. Default settings save up to 20 detailed offers, at most $0.06.

### What you can do

- Search wholesale deals by keyword, category, brand or wholesale topic; combine multiple starting URLs.
- Filter by supplier country and product condition, with optional brand, price, price-unit, starting-price and minimum-order filters.
- Export a watchlist of specific product URLs, deduplicated by Merkandi offer ID.
- Choose quick listing extraction or complete public product details.
- Read advertised volume-price tiers, negotiable-price signals and first-order discount terms where the seller provides them.
- Monitor **new offers**, **price changes**, **quantity changes**, condition, minimum order, country, brands, shipping time, VAT label and availability changes. See exact before/after values.
- Download JSON through the dataset, a prepared CSV, or use Apify's dataset export options and API integrations.
- Obtain direct links to publicly linked PDF/Excel manifests and full-size product images.
- Read a coverage report that explains result/page/time/request limits, skipped offers, missing pages and source errors.

The scraper preserves important distinctions: €500 **per pallet** is not comparable with €10 **per piece**; “from €2.85” is a starting price; “no limits” is not zero stock. Price-change percentages are calculated only when price units, currency and starting-price qualification match.

### Quick start

1. Enter a Merkandi search, category, brand, wholesale-topic or product URL, or enter a **Search phrase**.
2. Choose **Include full product details** and a maximum number of saved offers.
3. Run the Actor. Open **Wholesale offers**, **Download offers CSV** and **Run report and coverage** in the output.

With no URL or search phrase, the Actor demonstrates extraction on the electronics category. It does not attempt to export the entire marketplace by default.

#### Search for used iPhones

```json
{
  "search": "iphone",
  "conditions": ["Used"],
  "includeDetails": true,
  "priceUnits": ["piece"],
  "maxPrice": 150,
  "currency": "EUR",
  "maxResults": 25,
  "maxOffersToScan": 200
}
```

Filters are ANDed across fields and ORed within countries, conditions, brands and include-keyword lists. Exclude keywords reject a match. Keyword checks are literal and case-insensitive; description/brand matching requires detail extraction. A price range rejects missing prices and other currencies. It does not convert currencies or price units.

#### Export a category economically

```json
{
  "startUrls": [{"url":"https://merkandi.com/categories/consumer-electronics/32"}],
  "includeDetails": false,
  "countries": ["Germany"],
  "conditions": ["New"],
  "maxResults": 100,
  "maxPages": 10,
  "maxOffersToScan": 300
}
```

Basic mode supplies listing fields: ID, URL, title, price/currency/unit, starting-price flag, available-quantity text, condition, primary image, badges and observation time. Country/brand filters are applied by the discovery site; country and brand **output fields** require details. `FACETS` contains filter values from the most recently fetched discovery page. Available brands/subcategories can depend on the starting page.

#### Watch product URLs for price and stock changes

```json
{
  "startUrls": [
    {"url":"https://merkandi.com/products/apple-iphone-11-64-128-giga-mix-grade-amp-battery-full-original-2/1493616"},
    {"url":"https://merkandi.com/products/authentic-adidas-lot-1-667-pieces-at-only-12-80-piece-shoes-textiles-accessories/1589357"}
  ],
  "includeDetails": true,
  "mode": "changed",
  "monitorName": "my-wholesale-watchlist",
  "maxResults": 20
}
```

These are example public URLs; offers can change or disappear. Product watchlists require details.

Save the input as an Apify task and schedule it. Reuse the same monitor name and the same URLs, filters, sort order and detail setting. Avoid overlapping runs with the same monitor name. A best-effort lease detects an already active run; storage does not provide an atomic lock.

The first run establishes a baseline and emits `new` offers. Later **changed** runs emit new offers and changed tracked values; **new** emits previously unseen matches only; **all** emits every match and still annotates comparisons when a monitor name is supplied. Changing monitoring mode or run limits is allowed; changing the search configuration requires a new monitor name.

History starts when you run the monitor. It stores the latest comparison snapshot for up to 5,000 offers, at most 7 MB, with 90-day retention. Oldest snapshots are pruned if necessary; pruned/expired offers can later appear as new. Each run's dataset preserves its emitted observations under Apify's storage retention. This is not an API for historical prices from before your first run.

Missing search results are **never automatically marked removed**: ordering can change and scans may be capped. Explicitly unavailable product URLs are recorded in the report. `availability` is the source's advertised schema value, not an independently verified stock status.

### Output

| Fields | Meaning |
|---|---|
| `offerId`, `url`, `title` | Stable source identifier and product reference |
| `price`, `currency`, `priceUnit`, `priceRaw`, `priceIsFrom` | Displayed amount and its qualifications; null when unparseable |
| `priceTiers`, `negotiable`, `discountText`, `discountTerms`, `suggestedRetailPriceRaw` | Advertised quantity breaks and promotions. Retail-price wording is preserved without assuming a per-item value or calculating profit |
| `quantity`, `quantityUnit`, `quantityRaw`, `quantityUnlimited` | Available stock as advertised, preserving unknown/unlimited values |
| `minimumOrder`, `minimumOrderUnit`, `minimumOrderRaw` | Detailed minimum order, without converting units |
| `country`, `countryCode`, `condition`, `brands`, `categories`, `sku` | Public offer classification |
| `description`, `imageUrl`, `imageUrls`, `attachmentUrls` | Product content and links; media/files are not downloaded |
| `vatLabel`, `shippingTime`, `paymentOptions`, `deliveryOptions`, `shippingDestinations` | Seller-advertised ordering information; absent fields remain empty/null |
| `dataQualityNotes` | Detected price/schema, VAT-description or minimum-order/unit inconsistencies |
| `observationType`, `firstSeenAt`, `previousSeenAt`, `observedAt` | Snapshot/new/changed/unchanged and observation timestamps |
| `changedFields`, `changes`, `priceChange`, `priceChangePercent` | Before/after comparisons; price difference is current minus previous |
| `detailStatus`, `sourceUrl`, `fingerprint` | Extraction scope and provenance |

The default dataset contains only successfully extracted, matching output offers. Detail extraction validates the visible product identity and also handles pages without Product structured metadata. **Detailed mode skips an offer if its detail page cannot be retrieved after retries**; it does not silently bill a listing-only row as detailed.

Additional output links:

- **OFFERS.csv:** complete emitted rows with nested fields serialized as JSON. Seller-supplied strings that could execute spreadsheet formulas are prefixed with an apostrophe; original JSON values are preserved.
- **CHANGES.json:** emitted new/changed records. Without a monitor, snapshot records appear only in the main dataset/CSV.
- **OUTPUT:** counts, source requests/retries, limits, per-source coverage, missing URLs, errors, and monitor-store reference.
- **FACETS:** available source filters and category links. A direct-product-only run may not request discovery facets.

### Limits, reliability and billing

Set both **Maximum saved offers** and Apify's **Maximum run charge**. The Actor checks the charge allowance before producing another row. A basic row triggers `basic-offer` at $0.001; a detailed row triggers `detailed-offer` at $0.003. There are no automatic start or dataset-item fees. Result billing uses durable output records and idempotent charge identifiers to recover from an interrupted/restarted run.

`maxOffersToScan` also counts filtered and unchanged offers, bounding work when monitoring emits few results. `maxPages` applies to each starting discovery URL. `maxRequests` includes retries and redirects. The default soft time limit is 540 seconds; increase the Apify run timeout beyond your chosen soft limit for larger exports. A run stopped by a configured cap reports **limited**, with its saved results intact.

Residential proxy access is built in and included in the stated prices. Temporary network, blocking and rate-limit failures retry on fresh connections. Optional Unblocker fallback is off by default and separately capped. Consistently failed sources appear in the report; a wholly failed source-access run fails clearly. A partial run can succeed with **partial** in the report and status message. A genuine zero-match search succeeds with zero rows and zero result charges.

Merkandi currently displays a pagination boundary on large searches. Category counts, promoted placements and ordering can also change during a run. **Whole-marketplace completeness is not promised**, even if a requested limited search finishes. Use narrower category/country/condition searches and inspect the coverage report.

### What the public site does not provide

- Unmasked supplier contacts, addresses, personal names or private account data. `supplierContactAccess` indicates masking; the Actor does not unlock these fields.
- Verified seller stock, guaranteed authenticity, landed costs, verified resale margins or profit forecasts.
- A reliable standardized pack/pallet size across all sellers, or consistent VAT statements. Source wording is preserved and some contradictions are flagged, not silently resolved.
- Pre-existing price history, guaranteed publication timestamps on every offer, or guaranteed exhaustive marketplace coverage.
- Contents of attachment files: public links are returned, but documents are not parsed.

Only the public English `merkandi.com` site is supported in this release. Category/search, `/brands/`, `/wholesale/` topic and direct-product URLs are accepted. Sorting and individual source filters remain subject to Merkandi's own behavior. This Actor is independent of Merkandi.

### API and integrations

Use the Actor's **API** tab to copy a call with your own token and input. The dataset and named monitor store belong to your Apify account. You can use Apify schedules, tasks, webhooks, Make, n8n or dataset API consumers to process exports and change records. No external notification service is configured automatically.

For an issue, provide the run ID, source URL and expected field. Do not include your Apify token or Merkandi credentials.

# Actor input Schema

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

Category, search, brand, wholesale topic or product URLs. English merkandi.com only. Up to 100 URLs; duplicate offers are removed. Leave empty to use Search, or browse electronics if Search is also empty.

## `search` (type: `string`):

Native Merkandi keyword search, applied to each discovery URL. Example: iphone or amazon returns. For product watchlists use Include keywords instead.

## `includeDetails` (type: `boolean`):

Add minimum orders, countries, brands, descriptions, images, shipping/payment options, manifest links, volume-price tiers and discount terms. $3 / 1,000 saved offers. Off: faster listing fields at $1 / 1,000. No start fee.

## `maxResults` (type: `integer`):

Maximum output rows and result charges. Default 20 detailed offers costs at most $0.06. Skipped unchanged offers do not count. Also set a maximum run charge in Apify.

## `countries` (type: `array`):

English country names, ISO codes or Merkandi IDs, e.g. Germany, PL, 68. OR within this list. Native filters on discovery; public country verified on enriched details. See FACETS for source values.

## `conditions` (type: `array`):

Examples: New, Used, Mix / returns, Refurbished, Damaged, Outlet. OR within this list. Values available on the first discovery page appear in FACETS.

## `brands` (type: `array`):

Brand names or source slugs, e.g. Apple, Nike, hp. Native discovery filter; enriched records are also checked. OR within this list. See FACETS.

## `includeKeywords` (type: `array`):

Case-insensitive literal phrases; at least one must occur in the title, or in description/brands when details are enabled.

## `excludeKeywords` (type: `array`):

Skip an offer if any literal phrase occurs in the title, description or brands. Descriptions require details.

## `priceUnits` (type: `array`):

Only these unit labels: piece, pair, pallet, pack, lot, kilogram. No conversion between units. Combine with a price range to compare like-for-like offers.

## `minPrice` (type: `number`):

Inclusive numeric price. Requires a parseable price in the selected currency. No conversion or unit normalization.

## `maxPrice` (type: `number`):

Inclusive numeric price. Use Price units to restrict this to pieces, pallets, etc. From-prices are included unless excluded below.

## `currency` (type: `string`):

Only used with a price range. Compare the displayed amount in this currency; no currency conversion is performed.

## `excludeFromPrices` (type: `boolean`):

Skip offers advertised as a starting price. Otherwise priceIsFrom identifies them in the output.

## `maxMinimumOrder` (type: `number`):

Requires details. Compare the numeric minimum-order quantity as stated by the seller. Check minimumOrderUnit in output: quantities are not converted between pieces, packs or pallets.

## `sort` (type: `string`):

Source uses Merkandi’s existing order. Other options request a native sort. Sorting is per search, not a global merge; promoted offers may appear first. Numeric prices have differing units.

## `mode` (type: `string`):

All returns matching offers. New and Changed require a monitor name. First run establishes a baseline and emits new offers. Changed returns new offers plus changed values; unchanged checks are free.

## `monitorName` (type: `string`):

Optional in All mode; required in New/Changed. Reuse across scheduled runs with the same URLs, filters, sort and detail setting. 1–50 lowercase letters/digits/hyphens. History: 90 days, up to 5,000 offers / 7 MB. Do not overlap runs using the same name.

## `maxPages` (type: `integer`):

Caps discovery pages per starting URL. Large categories may hit a source pagination cap. The run report identifies incomplete scans.

## `maxOffersToScan` (type: `integer`):

Includes filtered and unchanged offers. Bounds the work done even when few rows are emitted. Raise for larger monitoring searches.

## `maxRequests` (type: `integer`):

Total HTML requests across discovery, details, redirects, retries and fallback. Images and attachments are not downloaded.

## `maxRunSeconds` (type: `integer`):

Stop cleanly and export progress near this limit. For longer runs also increase the Apify run timeout beyond this value.

## `maxRetries` (type: `integer`):

Retry temporary connection, rate-limit and blocking failures with fresh residential sessions. Missing products are not retried.

## `maxUnblockerRequests` (type: `integer`):

Only used when fallback is enabled. This limit includes fallback redirects.

## `proxyCountry` (type: `string`):

Residential proxy access is included. This sets the access location, not the supplier-country filter.

## `useUnblockerFallback` (type: `boolean`):

Optional slower fallback after residential retries fail. Included in result pricing, bounded by Maximum fallback requests. Usually unnecessary.

## Actor input object example

```json
{
  "startUrls": [],
  "search": "",
  "includeDetails": true,
  "maxResults": 20,
  "countries": [],
  "conditions": [],
  "brands": [],
  "includeKeywords": [],
  "excludeKeywords": [],
  "priceUnits": [],
  "currency": "EUR",
  "excludeFromPrices": false,
  "sort": "source",
  "mode": "all",
  "maxPages": 10,
  "maxOffersToScan": 200,
  "maxRequests": 500,
  "maxRunSeconds": 540,
  "maxRetries": 2,
  "maxUnblockerRequests": 5,
  "proxyCountry": "GB",
  "useUnblockerFallback": false
}
```

# Actor output Schema

## `offers` (type: `string`):

No description

## `csv` (type: `string`):

No description

## `changes` (type: `string`):

No description

## `report` (type: `string`):

No description

## `facets` (type: `string`):

No description

# 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 = {};

// Run the Actor and wait for it to finish
const run = await client.actor("cauldo/merkandi-offer-monitor").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 = {}

# Run the Actor and wait for it to finish
run = client.actor("cauldo/merkandi-offer-monitor").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 '{}' |
apify call cauldo/merkandi-offer-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,cauldo/merkandi-offer-monitor"
        }
    }
}
```

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/BSthV2y1FM9m8tX6f/builds/cxfrA83Ik90Li7nJx/openapi.json
