# Vinted Cross-Country Price Comparison - Find Arbitrage Deals (`lukehunter/vinted-cross-country-prices`) Actor

Find which Vinted country sells an item cheapest: compare public prices across 27 country sites in EUR, with free per-country price summaries and the median price gap for resale arbitrage. No login, proxies or seller/member data.

- **URL**: https://apify.com/lukehunter/vinted-cross-country-prices.md
- **Developed by:** [Luke Hunter](https://apify.com/lukehunter) (community)
- **Categories:** E-commerce, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$2.00 / 1,000 listings

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

## Vinted Cross-Country Price Comparison - Find Arbitrage Deals

**Vinted cross-country price comparison for resellers and market researchers.** Search the same term across selected Vinted countries and get comparable listing prices in EUR, one free statistical summary per country, and the cheapest-vs-dearest median gap for each search term.

Pay-per-result: **$0.002 per delivered listing row. Summary rows are free.**

### What you get

- `listing` rows with the same public listing fields as [Vinted Scraper](https://apify.com/lukehunter/vinted-scraper), plus `country` and fixed-rate `priceEur`.
- One free `summary` row per search term per country: count, min, p25, median and p75 in local currency and EUR, plus the cheapest listing URL.
- An **Arbitrage by search term** dataset view showing the country with the cheapest EUR median, the country with the dearest EUR median and the percentage gap.
- Coverage of the same 27 public Vinted country domains as the Vinted Scraper, without login, member/profile pages, private API endpoints, proxies or User-Agent rotation.

### Quick start

1. Enter 1-20 search terms such as `nike jacket` or `ralph lauren polo`.
2. Pick the Vinted countries to compare. The default is UK, France, Germany, Spain, Italy, Netherlands and Poland.
3. Set `maxItems` as your paid-listing cost cap and click **Start**. Use the Listings, Country summaries and Arbitrage dataset views to inspect the results.

#### Input example

```json
{
  "searchTerms": ["nike jacket"],
  "countries": ["uk", "fr", "de", "es", "it", "nl", "pl"],
  "priceFrom": 10,
  "priceTo": 80,
  "maxPagesPerCountry": 1,
  "maxItems": 300,
  "currency": "EUR"
}
```

`priceFrom` and `priceTo` are interpreted in the selected `currency` (EUR by default). Before requesting each public `/catalog` page, the Actor converts them through the fixed FX table to that storefront's local currency. Output comparison fields remain in EUR.

### Output example

A paid listing row:

```json
{
  "recordType": "listing",
  "itemId": "10130168789",
  "url": "https://www.vinted.co.uk/items/10130168789-denim-jacket",
  "title": "Denim jacket",
  "brand": "F&F",
  "size": "12-18 months / 80 cm",
  "condition": "Very good",
  "price": 2,
  "currency": "GBP",
  "priceEur": 2.35,
  "totalPriceWithFees": 2.46,
  "favouritesCount": 20,
  "imageUrl": "https://images1.vinted.net/t/02_02342_cydg21eTMjvKza26quCJQrxD/310x430/135402a3.webp?s=8800adb881f545deaac5906a7976555a108b2856",
  "country": "uk",
  "searchTerm": "jacket",
  "page": 1,
  "position": 1,
  "scrapedAt": "2026-10-02T00:00:00.000Z"
}
```

A free summary row:

```json
{
  "recordType": "summary",
  "searchTerm": "jacket",
  "country": "uk",
  "currency": "GBP",
  "count": 96,
  "minPrice": 2,
  "p25Price": 3.88,
  "medianPrice": 5,
  "p75Price": 20,
  "minPriceEur": 2.35,
  "p25PriceEur": 4.56,
  "medianPriceEur": 5.88,
  "p75PriceEur": 23.52,
  "cheapestListingUrl": "https://www.vinted.co.uk/items/10130168789-denim-jacket",
  "status": "truncated"
}
```

The summary numbers above are computed from the repository's real `catalog-jacket.raw.txt` UK fixture with the fixed 2026-10-02 GBP/EUR rate. Quartiles use linear interpolation (`(n - 1) × q`).

### Real-fixture summary and arbitrage regression example

The parser tests reuse **both** committed real captures from the live base Actor: `catalog-jacket.raw.txt` from `vinted.co.uk` and `catalog-nike-fr.raw.txt` from `vinted.fr`. Using the fixed 2026-10-02 FX table and the same linear-interpolation quartiles, those captures produce:

```json
[
  {
    "recordType": "summary",
    "country": "uk",
    "currency": "GBP",
    "count": 96,
    "minPrice": 2,
    "p25Price": 3.88,
    "medianPrice": 5,
    "p75Price": 20,
    "minPriceEur": 2.35,
    "p25PriceEur": 4.56,
    "medianPriceEur": 5.88,
    "p75PriceEur": 23.52,
    "cheapestListingUrl": "https://www.vinted.co.uk/items/10130168789-denim-jacket"
  },
  {
    "recordType": "summary",
    "country": "fr",
    "currency": "EUR",
    "count": 96,
    "minPrice": 1,
    "p25Price": 3.5,
    "medianPrice": 6,
    "p75Price": 18.5,
    "minPriceEur": 1,
    "p25PriceEur": 3.5,
    "medianPriceEur": 6,
    "p75PriceEur": 18.5,
    "cheapestListingUrl": "https://www.vinted.fr/items/10156165899-pacco-prenotato"
  }
]
```

If those two fixture summaries are passed through the arbitrage calculation as one comparison bucket, the arithmetic is:

```json
{
  "arbitrageCheapestCountry": "uk",
  "arbitrageCheapestMedianEur": 5.88,
  "arbitrageDearestCountry": "fr",
  "arbitrageDearestMedianEur": 6,
  "arbitrageGapPercent": 2.04
}
```

**That arbitrage object is a regression/math illustration, not a market conclusion:** the UK fixture was captured for `jacket` and the France fixture for `nike`. A real Actor run computes arbitrage only across countries for the **same** `searchTerm`.

### Use cases

- **Resellers buying in one country and selling in another:** compare current listing-price distributions before deciding where to source. A median gap is a research signal, not guaranteed profit; shipping, fees, taxes and cross-border availability are not included.
- **Price research:** compare the same product or search phrase across country storefronts using normalized EUR prices instead of manually converting currencies.
- **Brand pricing by market:** run brand/item searches such as `nike dunk` or `patagonia fleece` to see how listing medians and quartiles differ by market.

### Pricing and cost control

Only delivered `listing` rows are charged, at **$0.002 per listing**. `summary` rows and the derived arbitrage columns are free.

| Paid listing rows | Cost |
|---:|---:|
| 50 | $0.10 |
| 300 | $0.60 |
| 1,000 | $2.00 |
| 5,000 | $10.00 |

`maxItems` is a hard run-wide cap across all search terms and countries. If it is 300, this Actor cannot deliver more than 300 paid listing rows even if more countries still have results.

### Fixed FX rates

For reproducible comparisons, `src/fx.js` commits the **European Central Bank reference rates dated 2026-10-02**. The table stores local-currency units per EUR and covers every currency used by the 27 supported Vinted storefronts.

These rates are **approximate and intentionally not live**. They are approximate reference rates, not card, bank, marketplace or transaction rates. A later Actor run keeps using the committed snapshot until a code update deliberately changes it, so two runs do not move just because an FX feed changed.

### How it reads Vinted

This Actor uses the same conservative public-page approach as [Vinted Scraper](https://apify.com/lukehunter/vinted-scraper):

1. Check each target domain's `robots.txt` for `/catalog`; if disallowed or permission cannot be confirmed, skip that country/search and record the status.
2. Fetch only public server-rendered `/catalog?search_text=...` pages, with about one request every 3 seconds.
3. Parse the same listing-card fields as the existing Vinted Scraper, then convert each asking price with the fixed FX table.
4. If Vinted returns a block/challenge page, 403 or persistent 429, report it honestly and stop that target rather than attempting to evade it.

### Limitations

- This compares **asking prices in public search results**, not completed-sale prices or guaranteed resale margins.
- Shipping, buyer/seller fees beyond the card's displayed `totalPriceWithFees`, taxes, customs, currency-conversion fees and cross-border buying eligibility are not part of the arbitrage gap.
- Search inventories can differ materially by country, so compare specific terms/brands and inspect the underlying listings before drawing conclusions.
- FX is a fixed 2026-10-02 snapshot and is approximate. `currency` can be EUR, USD, GBP, PLN, AUD, CZK, DKK, HUF, RON or SEK; output comparison fields remain in EUR.
- `maxItems` is global. When it is reached, later country/search targets still get a free zero-count summary with `status: "skipped-max-items"`, but are not fetched.
- Vinted can serve a block/challenge page. This Actor does not use proxies, login, cookies, member/profile pages, private JSON APIs or User-Agent rotation to get around access controls.
- This is an independent tool, not affiliated with or endorsed by Vinted. Use the data in accordance with Vinted's terms and applicable law.

### Use it from your code, Make, Zapier or n8n

Built for automated pipelines: small runs, stable field names, one row per item, and you pay only for rows actually delivered.

**One API call that runs the Actor and returns the data** (replace `$APIFY_TOKEN` with your token from Apify Console → Settings → Integrations):

```bash
curl -s -X POST "https://api.apify.com/v2/acts/lukehunter~vinted-cross-country-prices/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"searchTerms": ["nike jacket"], "maxPagesPerCountry": 1, "maxItems": 20, "currency": "EUR"}'
```

**JavaScript** (`npm i apify-client`):

```js
import { ApifyClient } from 'apify-client';
const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('lukehunter~vinted-cross-country-prices').call({
  "searchTerms": [
    "nike jacket"
  ],
  "maxPagesPerCountry": 1,
  "maxItems": 20,
  "currency": "EUR"
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
```

**Python** (`pip install apify-client`):

```python
import os
from apify_client import ApifyClient
client = ApifyClient(os.environ['APIFY_TOKEN'])
run = client.actor('lukehunter~vinted-cross-country-prices').call(run_input={"searchTerms": ["nike jacket"], "maxPagesPerCountry": 1, "maxItems": 20, "currency": "EUR"})
items = client.dataset(run['defaultDatasetId']).list_items().items
```

**Schedules, webhooks and no-code tools:** save your input as a Task, add a Schedule (e.g. hourly or daily), and attach a webhook or one of the Integrations tab's connectors (Make, Zapier, n8n, Google Sheets, Slack) to push each run's results into your own system.

**Reliability:** an automated test runs this Actor's parser against the live site every day, and failures are fixed fast.

**Feedback:** an honest rating on the Store page, good or bad, helps others decide if this Actor fits their needs. Missing a field or found a problem? Open an issue on the Actor page.

### FAQ

#### How is the arbitrage percentage calculated?

For each search term with at least two countries that produced a numeric median, the Actor finds the lowest and highest `medianPriceEur` and calculates `(dearest - cheapest) / cheapest × 100`. The arbitrage columns are populated on one `summary` row per term so the dataset keeps exactly two record types: `listing` and `summary`.

#### Are summary rows charged?

No. Only rows with `recordType: "listing"` pass through the `listing` charge event. Summary rows are pushed separately and are free.

#### Does this scrape seller information?

No. It reads only the same public listing-card fields as the Vinted Scraper and never requests member/profile pages.

### Need one country's listings or alerts instead?

- **[Vinted Scraper & New Listing Alerts](https://apify.com/lukehunter/vinted-scraper):** use this when you want one country's raw Vinted listings, custom catalog URLs or scheduled new-listing alerts instead of cross-country price comparison.

### Related Actors

Other data tools from the same developer, built to the same standard: official or public sources, hard cost caps, and honest documentation of limits.

- **[Vinted Scraper](https://apify.com/lukehunter/vinted-scraper)**: Vinted search results with prices, brands and sizes in any Vinted country, plus scheduled new-listing alerts sent to Discord or Telegram.
- **[Vinted Item Scraper by URL](https://apify.com/lukehunter/vinted-item-tracker)**: price-drop and sold alerts for specific Vinted item URLs: price, brand, size, condition and availability, with a changes-only mode.
- **[Shopify Product Scraper by URL](https://apify.com/lukehunter/shopify-product-tracker)**: price, compare-at price and stock for specific Shopify product URLs, with a change-only feed.
- **[Walmart Category Scraper](https://apify.com/lukehunter/walmart-category-scraper)**: product names, prices, was-prices and ratings from Walmart category pages.
- **[Shopify Store Products Scraper](https://apify.com/lukehunter/shopify-store-products-scraper)**: full product catalogues from any Shopify store, with prices, sale prices, variants and stock.
- **[AliExpress Search Scraper](https://apify.com/lukehunter/aliexpress-scraper)**: AliExpress search results with prices, discounts, ratings and sold counts, by keyword.
- **[Temu Scraper](https://apify.com/lukehunter/temu-scraper)**: Temu search results with prices, product links and images for any keyword, with a new-products-only mode for scheduled runs.
- **[YouTube Channel Scraper](https://apify.com/lukehunter/youtube-stats-tracker)**: new-video alerts and channel monitoring for any list of YouTube channels: subscribers, latest videos and a new-uploads-only mode for scheduled runs.

# Actor input Schema

## `searchTerms` (type: `array`):

1-20 search phrases to compare across every selected Vinted country, e.g. "nike jacket".

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

Vinted country domains to compare. Each search term is run once on every selected country.

## `priceFrom` (type: `number`):

Optional minimum in the selected comparison currency (EUR by default). The Actor converts it with its fixed FX table before sending the equivalent local-currency filter to each Vinted country.

## `priceTo` (type: `number`):

Optional maximum in the selected comparison currency (EUR by default). The Actor converts it with its fixed FX table before sending the equivalent local-currency filter to each Vinted country.

## `maxPagesPerCountry` (type: `integer`):

1-5 catalog pages to fetch for each search-term/country combination. Default 1.

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

Hard cap across all countries and search terms. Default 300, maximum 5,000. Summary rows are free and do not count against this cap.

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

Currency used to interpret priceFrom and priceTo before converting those filters to each storefront currency. Output comparison fields remain in EUR.

## Actor input object example

```json
{
  "searchTerms": [
    "nike jacket"
  ],
  "countries": [
    "uk",
    "fr",
    "de",
    "es",
    "it",
    "nl",
    "pl"
  ],
  "maxPagesPerCountry": 1,
  "maxItems": 50,
  "currency": "EUR"
}
```

# Actor output Schema

## `listings` (type: `string`):

No description

## `summaries` (type: `string`):

No description

## `arbitrage` (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 = {
    "searchTerms": [
        "nike jacket"
    ],
    "maxPagesPerCountry": 1,
    "maxItems": 50,
    "currency": "EUR"
};

// Run the Actor and wait for it to finish
const run = await client.actor("lukehunter/vinted-cross-country-prices").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 = {
    "searchTerms": ["nike jacket"],
    "maxPagesPerCountry": 1,
    "maxItems": 50,
    "currency": "EUR",
}

# Run the Actor and wait for it to finish
run = client.actor("lukehunter/vinted-cross-country-prices").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 '{
  "searchTerms": [
    "nike jacket"
  ],
  "maxPagesPerCountry": 1,
  "maxItems": 50,
  "currency": "EUR"
}' |
apify call lukehunter/vinted-cross-country-prices --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,lukehunter/vinted-cross-country-prices"
        }
    }
}
```

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/FeUNoPzZ4cQGzowd2/builds/qyZpvroJ3JP7yUTGa/openapi.json
