# eBay Price & Inventory Monitor (`alex1313/ebay-price-inventory-monitor`) Actor

Price and inventory monitor for eBay resellers, dropshippers, and flippers: instant alerts the moment a competitor's price drops or rises, an item sells out, or a new listing appears.

- **URL**: https://apify.com/alex1313/ebay-price-inventory-monitor.md
- **Developed by:** [Alejo Sosa](https://apify.com/alex1313) (community)
- **Categories:** E-commerce, Automation, AI
- **Stats:** 2 total users, 1 monthly users, 58.3% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $100.00 / 1,000 results

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/platform/actors/running/actors-in-store#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

## eBay Price & Inventory Monitor

An eBay price and inventory scraper built for one job: stop refreshing eBay by hand. Point this
Actor at the listings you care about — a competitor's store, a product you resell, a search
you'd otherwise keep re-running — and it tells you the moment something changes: a **price
drop**, an item **selling out**, a **new listing** appearing, or one **disappearing**. No coding
required: add URLs, put it on a schedule, and let it push alerts to you instead of checking
manually.

Intended for personal research and competitive price monitoring — you're responsible for using
it in a way that complies with eBay's own terms of service.

> ⚠️ **Before your first run: enable Residential proxy.** eBay blocks Apify's default proxy
> outright — without Residential proxy enabled on your Apify account, every request fails
> immediately with a 403 error. Check **Proxy & Anti-scraping → Residential proxy** under your
> account settings first (billed separately, per GB, by Apify — see
> [Reliability & cost](#reliability--cost) for what that typically costs). This is the single
> most common reason a first run fails, and takes 30 seconds to check.

**Jump to:** [Who this is for](#who-this-is-for) · [Markets](#markets-supported) ·
[Reliability](#built-for-reliability) · [Quick start](#quick-start) · [Pricing](#pricing) ·
[Example output](#what-youll-see) · [Agents & MCP](#automating-with-agents-mcp-and-downstream-tools) ·
[Input](#input) · [Output](#output) · [Troubleshooting](#troubleshooting)

### Who this is for

- **Resellers** tracking a supplier listing's price so a margin-eroding increase doesn't slip by.
- **Dropshippers** watching multiple source listings for price or stock changes before a customer order fails.
- **Arbitrage buyers / flippers** catching a price drop or restock the moment it happens, before other buyers do.
- **Dealers and retailers** keeping tabs on what a competitor charges for the same or similar items.
- **Collectors** watching a specific listing for a price drop or restock on something scarce.
- **AI agents and automation pipelines** consuming structured, MCP-ready output instead of a human checking a page.

### Markets supported

Works on any eBay country site — not just `ebay.com` — with no extra setup, configuration, or
charge: point a URL at `ebay.co.uk`, `ebay.de`, `ebay.com.au`, `ebay.ca`, `ebay.fr`, `ebay.it`,
`ebay.es`, or any other eBay country domain and it's tracked the same way. The proxy pool is
deliberately not pinned to a country, so adding more market coverage never means configuring
anything new.

### Built for reliability

Monitoring only earns your trust if it doesn't quietly break. A few things this Actor does
specifically so a bad run never costs you real data:

- **A failed check never erases your history.** If a listing can't be scraped this run, its last
  known price and stock carry forward instead of being wiped or falsely reported as removed —
  eBay's occasional bot-block never looks like a fake "item deleted" event.
- **Blocked requests get a fresh identity, not a retry against the same wall.** A 403 is detected
  automatically and retried on a different proxy session, out of a pool of up to 50 per run — see
  [Reliability & cost](#reliability--cost).
- **Currency swaps can't fake a price change.** Every price is normalized before two runs are
  compared, and if that lookup ever fails, the Actor declines to guess rather than risk a false
  alert — see [Currency normalization](#currency-normalization).
- **You're told when something's actually wrong, separately from routine alerts.** A sustained
  scrape-failure streak or a listing stuck on stale data for 3+ runs fires its own alert, so a real
  problem doesn't get lost in day-to-day price/stock notifications — see
  [Reliability alerts](#reliability-alerts) and [Possibly-delisted items](#possibly-delisted-items).
- **Actively maintained.** Check this Actor's Information tab on its Apify Store page for the full
  changelog — fixes and improvements ship regularly, not just at launch.

### Quick start

1. Add one or more eBay URLs to **URLs to monitor** — individual item pages, search result
   pages, or seller pages. Any eBay country site works (`ebay.co.uk`, `ebay.de`,
   `ebay.com.au`, etc.), not just `ebay.com` — no extra setup or charge either way. Search/
   seller pages track up to 5 organic items found on that page — sponsored/promoted listings
   are skipped, so you're never tracking or paying for an ad slot.
2. Optionally set a **price drop alert threshold** and an **alert webhook URL** to get notified
   automatically the moment something worth reacting to happens.
3. Run the Actor once to establish a baseline.
4. Open the **Schedules** tab in the Apify Console and set it to run automatically (e.g. daily
   at 9am). Every run after the first compares against the one before it.

Requires **residential proxy** access on your Apify plan (billed per GB by Apify, separately
from this Actor's own price) — eBay blocks Apify's default proxy outright. See
[Reliability & cost](#reliability--cost) for the full picture.

### Pricing

This Actor uses Apify's **Pay-per-event** pricing — you're charged per tracked item written to
the results, not per compute unit, so your cost stays predictable regardless of how eBay's bot
detection behaves on a given run. It's also priced for *signal*, not just effort: an item that's
new, removed, or has a price/stock change costs the full **Result** price; an item that's simply
confirmed unchanged from last run costs the much smaller **Checked, unchanged** price instead —
tracking 20 stable listings doesn't cost the same as tracking 20 volatile ones.

| Event | Price |
|---|---|
| **Result** — an item that's new, removed, or has a price/stock change | $0.12 (Free/Bronze), $0.11 (Silver), $0.10 (Gold/Platinum/Diamond) |
| **Checked, unchanged** — an item confirmed unchanged from the previous run | $0.01 |
| **Actor start** | $0.00005 per GB of memory allocated (effectively free) |
| **AI enrichment** — only if you enable it, once per item actually enriched | See [AI enrichment](#ai-enrichment-optional) |
| **Price insight** — only if you enable it, once per signal actually fired | $0.015 |

Some examples to help you estimate:

- Tracking **5 items** in a one-off run, all new, costs about **$0.60**.
- Tracking **20 items** checked once a day for a month (≈600 checks): if none of them ever
  change, that's about **$6** at the Free/Bronze rate; if every check finds a change, that's the
  old worst case of about **$72**. Most real usage lands well below that ceiling, since most
  checks on most days don't find a change.

**Tip: track individual item URLs, not a search page, if you want the lower end of that range.**
A search or seller page's result set can reshuffle between runs (ranking/sponsored rotation)
independent of whether any tracked product's price actually moved, so items discovered that way
tend to look "new" more often than items tracked by a direct `ebay.com/itm/<id>` URL, which stays
the same item run after run. If you already know which listings you want to watch, add their item
URLs directly — you'll see more `Checked, unchanged` events (cheap) and fewer `Result` events
(the full price) than a search page tracking the same underlying items would produce.

### What you'll see

Say you're tracking a competitor's listing and its price quietly drops from $334.99 to $289.99.
The next scheduled run picks it up and adds a record like this to your `changes` dataset:

```json
{
  "title": "Nintendo Switch OLED Console - White",
  "priceChanged": true,
  "priceChangePercent": -13.2,
  "previous": { "price": 334.99, "quantityAvailable": 1 },
  "current": { "price": 289.99, "quantityAvailable": 4 },
  "listingUrl": "https://www.ebay.com/itm/123456789012"
}
```

If a `priceDropThresholdPercent` is set and this crosses it, the same information is also
POSTed to your `alertWebhookUrl` as JSON — ready to plug into **Zapier**, **Make.com**, or your
own endpoint, and from there route to Slack, Discord, email, or a spreadsheet. See
[Output](#output) for the full field reference.

For resellers/dropshippers tracking a *supplier* or competitor listing, a price going up matters
just as much as a price going down — it's the one that silently eats your margin. Set
`priceIncreaseThresholdPercent` (off by default) to also alert when a tracked price rises by at
least that percentage.

For ongoing competitor monitoring: add the specific competitor listing URLs you want to watch
(see the pricing tip above for why direct item URLs beat a search page here), set
`priceDropThresholdPercent` to whatever counts as "worth reacting to" in your niche, point
`alertWebhookUrl` at a Zapier/Make webhook feeding into Slack or Discord, and schedule the Actor
to run daily. From then on you'll get a `changes` dataset each day showing exactly which
competitors dropped price or sold out, plus a push notification the moment a big drop happens —
instead of manually rechecking listings. A search page still works well for the other common
case — discovering *new* listings in a category as they appear — where you want the result set to
refresh each run rather than track a fixed list.

### Automating with agents, MCP, and downstream tools

Every record this Actor produces — the `changes` dataset, the `snapshot` dataset, and the
webhook payload — is flat, typed JSON with no free-text parsing required, so it's MCP-ready
output: just as usable as a trigger for an LLM agent or automation pipeline (called directly via
[Apify's MCP server](https://mcp.apify.com), or through the Apify API) as it is for a human
dashboard. Two patterns that fit well:

- **Webhook-triggered agent.** Point `alertWebhookUrl` at an endpoint that hands the payload to
  an agent (e.g. a Zapier/Make step that calls your agent's API, or your own webhook receiver).
  Because the payload already carries `previous`/`current` prices, `priceChangePercent`, and
  `stockHitZero`, the agent doesn't need to re-derive what changed — it can go straight to
  deciding what to do about it (renegotiate a listing, restock, alert a buyer).
- **Dataset-polling agent.** An agent that runs on its own schedule can instead call the Apify
  API for the `changes` dataset directly and treat a nonzero result as its trigger, without
  needing a webhook receiver at all.

Nothing above requires special configuration — it's the same output every run already produces.

### How it works

1. You give it a list of eBay URLs: individual item pages, search result pages, or seller pages.
2. Search/seller pages are crawled first to discover individual item URLs (up to 5 per page, to
   keep run time and proxy cost bounded), which are then visited one by one.
3. Each item page is parsed with `CheerioCrawler` (plain HTTP + HTML parsing, no browser) using
   JSON-LD structured data where available, falling back to DOM selectors. This keeps compute
   cost low. If a page doesn't yield usable data over plain HTTP (e.g. it requires JS rendering
   or returns a bot-check page), the Actor automatically retries that URL with `PlaywrightCrawler`.
4. The current results are compared against the previous run's snapshot, which is stored in a
   **named, persistent key-value store** (`ebay-monitor-state`, key `PREVIOUS_SNAPSHOT`) — not the
   run's default key-value store, which on Apify is fresh and empty for every run and would not
   actually persist anything across scheduled runs. This store is shared across every run of the
   Actor, including ones with a different `urls` input (e.g. a one-off test run tracking
   different items than your regular schedule). Both the diff and the persisted update are
   scoped to only the items the current run actually requested, so a run with a narrower or
   different URL list can't misreport your other tracked items as removed, or erase their
   history — it merges its own results in rather than replacing the whole store.
5. Two datasets are written: a full `snapshot` of every tracked item, and a `changes` dataset
   containing only items that are new, removed, or have a price/stock change.
6. If `alertWebhookUrl` is set, a JSON payload of "alert-worthy" changes (price drops beyond
   your threshold, or items that hit zero stock) is POSTed to that URL. A summary is always
   logged to the run log, and also written to the run's **Output** tab, regardless of webhook
   configuration.

### Input

| Field | Type | Default | Description |
|---|---|---|---|
| `urls` | `string[]` | — (required) | eBay item URLs, search result pages, or seller pages to track. Any eBay country site works, not just ebay.com. |
| `checkIntervalHours` | `number` | `24` | Documentation only, recorded for your reference. Configure actual run frequency via Apify's Schedule feature. |
| `priceDropThresholdPercent` | `number` | `10` | Minimum price drop (%) vs. the previous run that triggers a webhook alert. |
| `priceIncreaseThresholdPercent` | `number` | `0` (off) | Minimum price *increase* (%) vs. the previous run that triggers a webhook alert. Off by default — useful for resellers/dropshippers watching a supplier or competitor listing, where a rising price erodes margin the same way a drop erodes a buyer's deal. |
| `alertWebhookUrl` | `string` | — (optional) | If set, alert-worthy changes are POSTed here after each run. Paste a Discord or Slack incoming webhook URL directly — no relay needed if `notificationFormat` matches. |
| `notificationFormat` | `string` | `"raw"` | Webhook payload shape: `raw` (full JSON, for Zapier/Make/your own endpoint), `discord`, or `slack`. |
| `alertOnStockZero` | `boolean` | `true` | Include items whose stock dropped to 0 in the webhook alert. |
| `alertDigestIntervalHours` | `number` | `0` (off) | Hold price drop/increase alerts and send them together at most once per this many hours, instead of one webhook per run. See [Alert digests](#alert-digests-optional) before enabling. |
| `targetCurrency` | `string` | `"USD"` | 3-letter ISO code every price is converted to for accurate cross-run comparison. See [Currency normalization](#currency-normalization). |
| `aiEnrichment` | `boolean` | `false` | Extract brand/model/category from each title via an LLM. See [AI enrichment](#ai-enrichment-optional) before enabling. |
| `enableInsightSignals` | `boolean` | `false` | Flag a 3-run price-drop streak, a new all-time-low price, or a price well below the item's own rolling average. Also reorders the dataset so the most actionable items come first — see [Price insight signals](#price-insight-signals-optional) before enabling. |
| `trackListingVariations` | `boolean` | `false` | Track each size/color/etc. combination on a variation listing as its own item. See [Tracking listing variations](#tracking-listing-variations-optional) before enabling — it's a real cost multiplier, not just a data-shape change. |
| `stateNamespace` | `string` | — (optional) | Leave empty unless you run more than one schedule of this Actor tracking different items — see [Running more than one schedule](#running-more-than-one-schedule) before you need this. |

Example input:

```json
{
  "urls": [
    "https://www.ebay.com/sch/i.html?_nkw=nintendo+switch+oled",
    "https://www.ebay.com/itm/123456789012"
  ],
  "priceDropThresholdPercent": 15,
  "alertWebhookUrl": "https://hooks.example.com/ebay-alerts",
  "alertOnStockZero": true
}
```

### Output

The same current-state records are written to three places every run: the run's **default
dataset** (what the Console's Dataset/API/export tabs show for a run), and two named datasets,
`snapshot` and `changes`, for consumers that specifically want the full-state history
(`snapshot`) or only the diffs (`changes`) kept separate from run to run.

#### Run summary (Output tab)

Every run writes a small summary to its default key-value store under the `OUTPUT` key, which
the Apify Console shows directly on the run's **Output** tab — no need to dig through the log
or open a dataset to see whether the run actually worked:

```json
{
  "trackedItemCount": 2,
  "directlyTrackedItemCount": 2,
  "freshlyScrapedCount": 2,
  "carriedForwardCount": 0,
  "unrecoverableCount": 0,
  "reliabilityDegraded": false,
  "proxyHealthDegraded": false,
  "siblingRunsDetected": 0,
  "startupJitterMs": 0,
  "changesDetected": 1,
  "emptyListingPageCount": 0,
  "possiblyDelistedCount": 0,
  "possiblyDelisted": [],
  "priceInsightCount": 0,
  "summary": "Monitor run summary: 1 price drop(s) >= 10%, 0 item(s) out of stock, 0 new listing(s), 0 listing(s) removed, 1 total change(s)."
}
```

- `freshlyScrapedCount` — items successfully scraped this run.
- `carriedForwardCount` — items that failed to scrape this run but were filled in from the last
  known snapshot (see [Reliability & cost](#reliability--cost)); not a sign anything needs fixing.
- `unrecoverableCount` — items that failed this run *and* have no previous snapshot to fall back
  on (only possible on an item's very first run). Worth checking manually if this is nonzero.
- `reliabilityDegraded` — whether the scrape failure rate has been sustained across recent runs;
  see [Reliability alerts](#reliability-alerts).
- `proxyHealthDegraded` — whether this run (or the one right before it) saw a near-total block
  rate across your directly-tracked items — a different, faster-reacting signal than
  `reliabilityDegraded` above. See [Reliability & cost](#reliability--cost) for what this Actor
  automatically does about it.
- `siblingRunsDetected` — other runs of this same Actor that were active under your account right
  when this run started, e.g. another schedule firing at the same time. Purely informational —
  it doesn't change this run's behavior. `0` if none were detected, or if this run doesn't have
  permission to list other runs (a restricted, non-owner token). See
  [Running more than one schedule](#running-more-than-one-schedule).
- `startupJitterMs` — how long this run actually waited (in milliseconds, up to 2 minutes) before
  its first request to `ebay.com`, in direct response to `siblingRunsDetected` above. `0` whenever
  no sibling run was detected — a run with nothing else happening never waits.
- `emptyListingPageCount` — search/seller pages (not individual items) in your input that returned
  zero item links even after the browser fallback, likely because eBay changed that page's markup.
  Worth checking manually if this is nonzero.
- `possiblyDelistedCount`/`possiblyDelisted` — durable, always-on version of the
  [Possibly-delisted items](#possibly-delisted-items) webhook alert: items running on
  carried-forward data for 3+ consecutive runs, visible here every run regardless of whether
  `alertWebhookUrl` is configured or that alert's webhook POST succeeded.
- `priceInsightCount` — how many [price insight signals](#price-insight-signals-optional) fired
  this run. Always `0` when `enableInsightSignals` is disabled. Per-item detail lives on each
  item's row in the dataset, not repeated here.

#### `snapshot` dataset

One record per tracked item, every run, representing its full current state:

```json
{
  "itemId": "123456789012",
  "title": "Nintendo Switch OLED Console - White",
  "price": 289.99,
  "currency": "USD",
  "normalizedPrice": 289.99,
  "shippingCost": 0,
  "condition": "New",
  "quantityAvailable": 4,
  "sellerUsername": "example_seller",
  "sellerFeedbackScore": 15234,
  "watchersCount": 12,
  "bidCount": null,
  "currentBid": null,
  "timeRemaining": null,
  "reserveNotMet": false,
  "change24hPercent": -5.2,
  "change7dPercent": -12.1,
  "change30dPercent": null,
  "aiBrand": null,
  "aiModel": null,
  "aiCategory": null,
  "aiEnrichmentAttempted": false,
  "consecutiveDropStreak": null,
  "isNewAllTimeLow": false,
  "isUnderpricedVsHistory": false,
  "dataStale": false,
  "dataStaleReason": null,
  "variationKey": null,
  "parentItemId": null,
  "listingUrl": "https://www.ebay.com/itm/123456789012",
  "scrapedAt": "2026-08-01T12:00:00.000Z"
}
```

`aiBrand`/`aiModel`/`aiCategory` stay `null` unless `aiEnrichment` is enabled — see
[AI enrichment](#ai-enrichment-optional). `aiEnrichmentAttempted` is `true` once enrichment has
actually run for an item's current title, even if it found nothing (a bundle/lot listing with no
identifiable brand, say) — that distinction is what keeps such an item from being re-enriched
every run forever just because its brand/model/category are still `null`.
`consecutiveDropStreak`/`isNewAllTimeLow`/`isUnderpricedVsHistory` stay `null`/`false` unless
`enableInsightSignals` is enabled — see [Price insight signals](#price-insight-signals-optional).

`dataStale` is `true` only when this run couldn't scrape the item fresh (eBay bot-blocked the
request, etc.) and this row is its last successfully-scraped data carried forward instead —
`scrapedAt` reflects that earlier successful run, not this one. Check this before trusting a row
as "what eBay shows right now." An item stuck on `dataStale: true` for 3+ consecutive runs also
shows up in `possiblyDelisted` on the run's `OUTPUT` record — see
[Possibly-delisted items](#possibly-delisted-items).

`dataStaleReason` explains *why* `dataStale` is true (`null` whenever it's false): `"blocked"` means
every crawler this run exhausted its retries outright. `"skipped-recently-confirmed"` means the
browser-fallback retry pass was skipped entirely — the plain-HTTP pass hit a full block, but this
item had already been successfully scraped within the last hour, so the recent price was served
instead of paying for a second full retry ladder a fresh block is unlikely to survive either. Both
mean the same thing for trusting the row (it's not fresh this run), but only the second is a
deliberate cost-saving decision rather than eBay actually resisting every attempt.

`variationKey`/`parentItemId` are non-null only on a row produced by `trackListingVariations` —
see [Tracking listing variations](#tracking-listing-variations-optional). Both stay `null` for
every ordinary row, including every row in the example above.

`bidCount`/`currentBid`/`timeRemaining` are only ever non-null for auction-format listings — a
Buy It Now listing like the example above always has them `null`. `reserveNotMet` is `false` for
both "no reserve" and "reserve met"; only a genuine "Reserve not met" badge on the page sets it
`true`, since the two can't be distinguished from page content otherwise.

`change24hPercent`/`change7dPercent`/`change30dPercent` track the price trend over a rolling
window (up to 30 tracked data points per item, not 30 days), separate from the single-previous-run
`priceChangePercent` in the `changes` dataset below. Each is `null` until the item has been
tracked long enough for that window to have a baseline — a brand-new item has all three `null`
for its first month, as in the `change30dPercent` above. If you schedule this Actor more often
than roughly once a day, note that the 30-point history window covers less than 30 days, so
`change7dPercent`/`change30dPercent` can go permanently `null` for an item once its older points
age out, not just during that first-month ramp-up.

`price`/`currency` are the raw values eBay actually displayed for this scrape. `normalizedPrice`
is `price` converted to `targetCurrency` — see [Currency normalization](#currency-normalization)
— and is what change detection actually compares across runs. `normalizedPrice` is `null` when
conversion wasn't possible (unrecognized currency, or the exchange-rate lookup failed that run).

`firstSeenAt` is the timestamp of the first run this Actor ever observed this item, and stays fixed
across every later run — unlike `scrapedAt`, which updates every run. It's what
`listingLifespanDays` (below, in the `changes` dataset) is computed from once the item is later
detected removed.

#### `changes` dataset

Only items with detected differences from the previous run, with before/after values:

```json
{
  "itemId": "123456789012",
  "listingUrl": "https://www.ebay.com/itm/123456789012",
  "title": "Nintendo Switch OLED Console - White",
  "changeTypes": ["priceChanged", "stockChanged"],
  "newListing": false,
  "listingRemoved": false,
  "listingLifespanDays": null,
  "priceChanged": true,
  "priceChangePercent": -13.2,
  "stockChanged": true,
  "stockHitZero": false,
  "previous": { "price": 334.99, "quantityAvailable": 1 },
  "current": { "price": 289.99, "quantityAvailable": 4 },
  "detectedAt": "2026-08-01T12:00:00.000Z"
}
```

`listingLifespanDays` is non-null only when `listingRemoved` is true: the number of days between
this Actor's first-ever observation of the item (`firstSeenAt` in the main dataset, carried
forward unchanged across every run since) and the run that detected it gone. This is not exact
"time to sell" — a listing can disappear for reasons other than a sale (delisted, ended without a
buyer, temporarily hidden), and the clock only starts from when this Actor first saw it, not from
when it was actually first listed on eBay. It's the closest signal derivable from data this Actor
already collects, with no new scraping.

#### Webhook alert payload

Sent as a POST with `Content-Type: application/json` when `alertWebhookUrl` is set and at
least one alert-worthy change is detected. The shape depends on `notificationFormat`:

**`raw`** (default) — the full structured payload, for Zapier ("Catch Hook" trigger), Make.com
("Custom Webhook" trigger), or your own endpoint:

```json
{
  "triggeredAt": "2026-08-01T12:00:00.000Z",
  "priceDropThresholdPercent": 10,
  "priceIncreaseThresholdPercent": 15,
  "alertOnStockZero": true,
  "changes": [ /* array of change objects, same shape as the changes dataset above */ ],
  "reliabilityEvent": "degraded" /* only present on a reliability transition, see below */,
  "possiblyDelisted": [ /* only present when nonempty, see below */ ],
  "emptyListingPages": [ /* only present when nonempty, see below */ ],
  "priceInsights": [ /* only present when nonempty, see below */ ],
  "alertDigestIntervalHours": 24 /* only present when alertDigestIntervalHours is set, see Alert digests */
}
```

**`discord`** — `{ "content": "Monitor run summary: ...\nPrice drop: ... (https://...)" }`, a
plain-text summary in the envelope Discord's incoming webhooks actually expect. Paste a Discord
channel's webhook URL straight into `alertWebhookUrl` — no relay needed.

**`slack`** — the same summary as `{ "text": "..." }`, for a Slack incoming webhook URL.

#### Reliability alerts

Separate from price/stock alerts, the webhook payload can include a `"reliabilityEvent"` field
(`"degraded"` or `"recovered"`) when at least half of your directly-tracked items have failed to
scrape (with no previous data to fall back on) across 3 consecutive runs — a sign of something
sustained, like eBay changing its markup, rather than one bad run. It fires once on the
transition into and out of that state, not on every run while it persists, so it won't spam
your webhook. This is separate from the per-run `unrecoverableCount`/`carriedForwardCount`
fields on the `OUTPUT` tab, which reflect only the current run.

#### Possibly-delisted items

The per-run carry-forward safety net (see [How it works](#how-it-works)) is deliberately
generous — it keeps serving an item's last known data indefinitely rather than ever failing, so
a permanently-delisted item would otherwise just go silently stale forever with no alert. The
webhook payload can include a `"possiblyDelisted"` array (`{ itemId, title, listingUrl,
consecutiveCarriedForwardRuns }`) once an item has been running on carried-forward data for 3
consecutive runs. Like the reliability alert, this fires once when the streak first crosses the
threshold, not on every run after — if the item later scrapes successfully again, its streak
resets, and a future 3-run streak would trigger a fresh alert.

#### Empty listing pages

If a search or seller page URL in your input returns zero item links, even after retrying with a
full browser, that's a sign eBay changed that page's markup (or the query/store genuinely has no
matching listings), not a normal empty result. The webhook payload can include an
`"emptyListingPages"` array (`{ listingUrl }`) whenever this happens on a run, in addition to the
`emptyListingPageCount` field always written to the `OUTPUT` tab (see
[Run summary](#run-summary-output-tab)).

#### Price insight signals

When `enableInsightSignals` is on, the webhook payload can include a `"priceInsights"` array
(`{ itemId, title, listingUrl, signal }`, where `signal` is
`{ type: "consecutiveDrops", consecutiveDrops: 3 }`, `{ type: "allTimeLow", price: 42.5 }`, or
`{ type: "underpricedVsHistory", price: 42.5, rollingAverage: 55, percentBelowAverage: 23.6 }`)
whenever a signal fires on a run. See [Price insight signals](#price-insight-signals-optional) for
what these mean and how they're computed.

### AI enrichment (optional)

Off by default. When `aiEnrichment` is enabled, each item's title is sent to an LLM to extract
structured `aiBrand`/`aiModel`/`aiCategory` fields — useful for filtering, grouping, or
searching a large tracked list where titles are inconsistently worded. Uses Apify's own
OpenRouter proxy, so this Actor never needs (or asks you for) a separate OpenAI/Anthropic API
key — the LLM call is billed the same way compute and proxy usage already are, through your
Apify account.

Only items actually scraped fresh this run are enriched, and only if their title is new or
changed since last run — a daily-scheduled item with an unchanged title is never re-enriched
(and never re-charged) on subsequent runs. Enrichment is billed as a separate `ai-enrichment`
event, charged once per item actually enriched.

**Before enabling this**, add a custom `ai-enrichment` event with your own price to this
Actor's monetization setup in the Apify Console (Actor pricing step) — it isn't billable until
you do. If you're the Actor's owner testing this yourself, note that Apify's OpenRouter proxy
recommends a paid Apify plan; free-tier accounts may be blocked by anti-fraud protections.

### Price insight signals (optional)

Off by default. A single run-over-run diff can be noisy — a search page's result ranking can
reshuffle between checks independent of whether any price actually moved, so one drop in
isolation isn't always worth reacting to. When `enableInsightSignals` is enabled, three patterns
get flagged specifically because they're harder to fake with noise:

- **3 consecutive price drops** (`consecutiveDropStreak: 3` on the item's dataset row, or a
  `consecutiveDrops` signal in the webhook payload) — real downward momentum across several
  checks, not a single-run blip.
- **A new all-time-low price** (`isNewAllTimeLow: true`, or an `allTimeLow` signal) — this
  item's price has never been lower since you started tracking it. Never fires on an item's
  first-ever observation, since every item's first price is trivially "the lowest so far".
- **Underpriced vs. its own history** (`isUnderpricedVsHistory: true`, or an
  `underpricedVsHistory` signal) — this item's price just dropped to 15%+ below its own rolling
  average, computed entirely from this Actor's own persisted price history rather than eBay's
  sold/completed-listings data (which requires a logged-in session this Actor deliberately
  doesn't use). Needs at least 3 prior price points before it can fire, so a newly-tracked item
  can't trivially trigger it off a one- or two-point "average".

Both are computed entirely from the price history this Actor already persists for
`change24hPercent`/`change7dPercent`/`change30dPercent` above — no extra scraping, no external
API call. Each fires once, on the run it first becomes true, the same way the
[reliability](#reliability-alerts) and [possibly-delisted](#possibly-delisted-items) alerts do —
not re-flagged on every run while the condition continues to hold. Billed as a separate
`price-insight` event, once per signal actually fired (an item can fire both signals in the same
run, billed as two events).

**Before enabling this**, add a custom `price-insight` event with your own price to this Actor's
monetization setup in the Apify Console (Actor pricing step) — it isn't billable until you do.

`enableInsightSignals` also changes the order rows are written to the dataset in: instead of
scrape order, an item with a fired signal comes first (new all-time low, then underpriced vs.
history, then a longer drop streak before a shorter one), and everything else follows sorted by
its most recent 24-hour price-change magnitude, biggest mover first. This only reorders the
dataset — nothing about diffing, billing, or the `changes`/webhook payload changes. With
`enableInsightSignals` off, rows stay in plain scrape order as they always have.

### Alert digests (optional)

Off by default — every alert-worthy change sends its own webhook immediately, as it always has.
If you check often enough that this gets noisy, set `alertDigestIntervalHours` to hold price
drop/increase alerts and send them together at most once per that many hours instead of one
webhook per run.

A stock-out (`stockHitZero`) alert always sends immediately regardless of this setting — running
out of stock is time-sensitive in a way a price change usually isn't, so it's never held back.
Reliability, possibly-delisted, empty-listing-page, and price-insight alerts aren't held either;
this only affects price drop/increase alerts specifically, since those are the noisy, frequent
ones a digest is meant for.

When a digest fires, `changes` in the webhook payload contains every held price alert since the
last digest was sent, not just this run's — each entry still carries its own `detectedAt`
timestamp, so you can tell which run actually found it.

### Tracking listing variations (optional)

Off by default. An eBay listing with a size/color/etc. picker only ever shows **one** price for
the whole listing until a shopper actually picks a combination — this Actor's ordinary single-row
extraction has no way to know which combination that displayed price/stock belongs to, and on a
listing where nothing is pre-selected, it can end up reading an unrelated number entirely (a real
example encountered while building this: a t-shirt listing returning a raw price of $750). Every
tracked listing has always had this blind spot; `trackListingVariations` is what actually fixes it,
rather than just working around it.

When enabled, and a tracked item URL has eBay's variation picker, this Actor clicks through every
real combination (capped at 24 per listing) the same way a shopper would, and tracks each one as
its own item — `itemId` becomes `{parentItemId}::{variationKey}` (e.g.
`186349877487::Size M|Color Black`), with `parentItemId` and `variationKey` on the dataset row so
you can group combinations back to their shared listing. The ambiguous single-row price is never
recorded for a listing with variations — only the real per-combination ones.

**This changes cost, not just data shape.** Each combination bills as its own
`result`/`checked-unchanged` event, same as any other tracked item — a listing with 8 real
combinations now costs roughly 8x what tracking it used to cost. Enable this only for listings
where knowing each combination's actual price/stock is worth that.

**Known limitations of this feature specifically** (unlike the rest of this Actor):

- eBay's variation picker is an undocumented custom widget, not a native HTML control — this
  clicks through it using its currently-observed markup, which could change without notice and
  silently stop matching. If a listing's combinations stop appearing in your output, that's the
  most likely reason.
- Variation rows don't yet participate in the [carry-forward safety net](#reliability-alerts) or
  [possibly-delisted tracking](#possibly-delisted-items) that direct item URLs get — a combination
  that fails to scrape one run is simply missing from that run's output rather than falling back
  to its last known price.

### Reliability & cost

#### Why the default input is a search query, not a specific item

Apify runs every published Actor daily against its default (prefilled) input and requires a
successful run with a non-empty result — an Actor that fails this for 3 days straight gets
flagged "under maintenance," and eventually deprecated if it stays broken. A single hardcoded
item URL is a bad fit for that: individual listings end or sell out permanently, and a
prefill pointing at a dead item would fail the check forever until someone noticed and updated
it. The default input here (`https://www.ebay.com/sch/i.html?_nkw=usb+cable`) is a broad,
high-volume search query instead — whatever's currently live shows up automatically, so there's
no specific item ID to go stale (this is a constraint on the *default*, not a recommendation for
your own input — see the pricing tip above for why your own tracked items are usually better off
as direct item URLs). This is why search-page tracking is capped at 5 items per page (see above):
it bounds cost and run time for this self-healing default just as much as for any user's own
search-page input.

eBay's page markup changes periodically and can serve bot-detection challenges to plain HTTP
requests. This Actor extracts from JSON-LD structured data first (the most stable source) and
falls back to DOM selectors, then to a real browser (Playwright) if HTTP-only extraction comes
back empty. If eBay changes its markup significantly, the DOM-selector fallback fields
(shipping cost, watchers count, seller feedback score) are the most likely to need updating —
`itemId`, `title`, and `price`, sourced primarily from JSON-LD, are more resilient. Note that
some listings genuinely don't display a watcher count at all, so `watchersCount: null` isn't
always a sign of a broken selector.

#### Proxy requirements

eBay's bot detection (Akamai) blocks Apify's default datacenter proxy outright — this Actor
requires **RESIDENTIAL** proxy access on your Apify plan
(`Actor.createProxyConfiguration({ groups: ['RESIDENTIAL'] })`), which is billed per GB.
Without it, every request will fail with `Request blocked - received 403 status code.`

Even with residential proxy, **eBay's blocking is probabilistic, not deterministic**. Expect
occasional request failures regardless of retries — this is normal, not a sign of
misconfiguration. Several mitigations are built in:

- Both crawlers retry each URL up to 3 times, and automatically detect blocked/403 responses
  and retry with a fresh proxy session rather than reusing a flagged one (`retryOnBlocked`).
- A session pool of up to 50 residential identities is used per run, so a single flagged IP
  doesn't take down the whole run.
- If an entire run fails to scrape a tracked item, its last known data is carried forward into
  the persisted snapshot instead of being treated as `listingRemoved` or silently dropped — so a
  bad run doesn't erase your price history or trigger false "removed" changes. You'll see a
  `Failed to scrape ... carrying forward last known data` warning in the log, and the run's
  `carriedForwardCount` output field, when this happens.

For a scheduled monitor, occasional failed *items* within an otherwise successful run are
expected and self-correct on the next scheduled run — you don't need every single item to
succeed on every run.

**Automatic backoff on a near-total block rate.** Occasional single-item failures are normal, as
above — but if 80%+ of your directly-tracked items fully exhaust their retries without succeeding
(summed across the last few runs if you track only 1-2 items at a time, so a small run's normal
noise isn't misread as a pattern — at least 3 attempts total either way), that's different: it
usually means eBay is hard-blocking outright, not just unlucky on a few items. When that happens,
this Actor automatically reduces its own retry budget — for the rest of the run showing the
pattern, and the start of the next one — instead of repeating a full-cost retry ladder that's
already shown not to work. See `proxyHealthDegraded` in the [run summary](#run-summary-output-tab)
to see when this kicks in. It self-heals: once recent runs return to a normal success rate, the
next run returns to the full retry budget automatically. This protects both a single run tracking
many URLs (a bad early stretch won't compound into every later item paying the same wasted cost)
and a schedule that only ever tracks one or two items per run (three bad runs in a row are
recognized as a pattern, not treated as three unrelated one-offs).

**Preventive pacing against eBay, on every run.** The backoff above only kicks in after a run has
already shown a degraded pattern — it's a reactive safety net, not a first line of defense. To
reduce the odds of ever tripping it in the first place, this Actor caps itself to at most 2
concurrent requests to `ebay.com` and waits at least 1 second between consecutive requests to the
same domain, on every run, regardless of history. This isn't configurable, because it's cheap
(it costs at most a few extra seconds on a typical run) and it lowers baseline block risk for
everyone, including a first-ever run with no prior signal to react to.

#### Running more than one schedule

If you run this Actor under more than one Apify Schedule or Task — for example, one tracking
phone listings and another tracking shoe listings — set a different `stateNamespace` value for
each one. This Actor's price history, carry-forward safety net, and every other piece of
cross-run state live in Apify storage scoped to your whole account, not to an individual
schedule; two schedules left at the default (empty) `stateNamespace` would silently share and
corrupt each other's history rather than staying independent. A single schedule tracking one
list of items never needs to set this — the default behaves exactly as it always has.

Separately from `stateNamespace`, also stagger the schedules' fire times — e.g. `:00` and `:15`
past the hour rather than both at `:00`. Each run already paces its own requests to `ebay.com`
(see above), but that per-run pacing can't see other runs happening at the same time under a
different schedule; two schedules firing together effectively doubles the concurrent request
rate against the same site, which raises block risk for both. A few minutes of offset is enough
— there's no need to spread them further apart than that.

Each run also checks, right when it starts, whether any other run of this Actor is already
active under your account, and warns in the log if so — surfaced as `siblingRunsDetected` in the
[run summary](#run-summary-output-tab) too. It stays `0` and silent if this run's token doesn't
have permission to list other runs under the account. When it *does* detect a sibling run, this
Actor waits a random amount of time (up to 2 minutes, surfaced as `startupJitterMs`) before
sending its first request to `ebay.com` — neither run's own schedule keeps its original trigger
time, but their actual request bursts stop landing at the same instant, which is what the combined
request-rate risk actually comes from. This happens automatically and isn't configurable; a
solo run with nothing else happening never waits at all. Staggering your schedules' fire times
(above) still avoids the wait entirely, so it's worth doing regardless if you find this coming up
often.

The proxy is **not pinned to a specific country**. Pinning (e.g. `countryCode: 'US'`) shrinks
the residential IP pool enough that eBay's detection burns through it noticeably faster than
the full pool, so it's left unpinned in favor of a better success rate. The trade-off is that
eBay auto-localizes price currency based on the proxy's apparent exit country, so consecutive
runs may show the same real price in different currencies (e.g. USD one run, KRW or THB the
next). This is handled by currency normalization (below) rather than by pinning the proxy, so
it doesn't cost any reliability.

### Currency normalization

Because the proxy is unpinned (see above), eBay may display a different currency from run to
run for the same item. Comparing raw prices across a currency change would either misfire or
require suppressing real price changes, so instead every price is converted to a single
`targetCurrency` (default `USD`, configurable) via a free exchange-rate lookup — once per run,
not once per item. The result is stored as `normalizedPrice` alongside the untouched raw
`price`/`currency`, and `normalizedPrice` is what change detection actually compares.

**`normalizedPrice` is only ever used for that internal comparison — it's never displayed to
you as "the price."** Every alert and dataset field showing a price shows the raw, actually-
scraped `price` in its actual `currency`, so what you see always matches what you'll find when
you open the listing. `normalizedPrice` exists purely so a percentage-change alert stays
accurate even when the two runs it's comparing happened to be shown in different currencies.

Two free, keyless sources are queried in parallel and merged: [Frankfurter](https://frankfurter.dev)
(ECB reference rates, the more authoritative source but only ~30 currencies) is primary, and
[ExchangeRate-API's open endpoint](https://www.exchangerate-api.com/docs/free) (much broader
coverage) fills in anything Frankfurter doesn't track, such as CLP. If both fail for a run (rare
— network issue, both APIs down simultaneously), affected items get `normalizedPrice: null` and
the Actor falls back to its previous behavior: comparing raw prices, and leaving
`priceChangePercent` null on a currency mismatch rather than computing a misleading number. This
never crashes the run — currency normalization degrades gracefully, it doesn't block scraping.

### Troubleshooting

**Run fails immediately with a 403 on every URL.** Your Apify plan doesn't have residential
proxy access enabled, or you've exhausted your proxy data allowance for the billing period.
Check Proxy usage under your Apify account's Billing/Usage page.

**`unrecoverableCount` is nonzero on an item's first run.** The item failed to scrape and has
no previous snapshot to fall back on. Run the Actor again — if it keeps failing for the same
URL specifically (not intermittently), double check the URL is a valid, currently-live eBay
item page.

**Webhook never fires even though prices are clearly changing.** Confirm
`priceDropThresholdPercent` isn't set higher than the actual drop you're seeing — smaller drops
are still recorded in the `changes` dataset but don't trigger a webhook alert by design. If
`priceChangePercent` is `null` on an entry where the price obviously changed, check whether
`normalizedPrice` is also `null` on either side — that means the exchange-rate lookup failed
that run and the Actor fell back to raw-price comparison, which nulls the percent on a currency
mismatch rather than guess (see [Currency normalization](#currency-normalization)).

**Some fields (`shippingCost`, `sellerFeedbackScore`, `watchersCount`) are consistently
`null`.** These are sourced from DOM selectors that eBay can change without notice. `null`
`watchersCount` in particular is often correct — many listings simply don't display one.

### Questions or something not covered here?

Use the **Issues** tab on this Actor's Apify Store page — it's public, so you can see exactly how
issues get handled (and how fast) before you even run it.

# Actor input Schema

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

eBay item pages, search result pages, or seller pages. Item pages are tracked directly; search/seller pages are crawled once per run to discover item links, which are then tracked the same way. Mixing all three types in one list is fine. Any eBay country site works (ebay.co.uk, ebay.de, ebay.com.au, etc.), not just ebay.com.

Examples:

- Item page: `https://www.ebay.com/itm/123456789012`
- Search page: `https://www.ebay.com/sch/i.html?_nkw=nintendo+switch+oled`
- Seller page: `https://www.ebay.com/str/examplestore`
- Other regions: `https://www.ebay.co.uk/sch/i.html?_nkw=nintendo+switch`

## `checkIntervalHours` (type: `integer`):

Documentation only — recorded in the run's output for your own reference. It does not schedule anything. To actually run this Actor repeatedly, add an Apify Schedule (Schedules tab in the Apify Console) at whatever interval you choose.

## `priceDropThresholdPercent` (type: `integer`):

Send a webhook alert when a tracked item's price drops by at least this percentage compared to the previous run. Smaller drops are still recorded in the "changes" dataset, just not pushed to the webhook.

## `priceIncreaseThresholdPercent` (type: `integer`):

Send a webhook alert when a tracked item's price rises by at least this percentage compared to the previous run — useful for resellers/dropshippers tracking a supplier or competitor listing where a price increase erodes margin the same way a drop erodes a buyer's deal. Set to 0 (default) to disable; this alert is off unless you opt in.

## `alertWebhookUrl` (type: `string`):

Optional. If set, an alert is POSTed here after each run that has alert-worthy changes. Works directly with Zapier "Catch Hook" / Make.com "Custom Webhook" triggers, a Discord or Slack incoming webhook URL (set "Notification format" below to match), or any endpoint that accepts raw JSON.

## `notificationFormat` (type: `string`):

Shape of the webhook payload. "Raw JSON" is the full structured payload (all change details, for Zapier/Make/your own endpoint). "Discord" and "Slack" send a plain-text summary in the envelope those platforms' incoming webhooks actually expect, so you can paste a Discord/Slack webhook URL directly into "Alert webhook URL" above with no relay in between.

## `alertOnStockZero` (type: `boolean`):

Include items whose available quantity dropped to 0 in the webhook alert payload, even if their price didn't change.

## `alertDigestIntervalHours` (type: `integer`):

0 (default) sends every alert-worthy price change as its own webhook, immediately. Set this to hold price drop/increase alerts and send them together at most once per this many hours instead — useful if you check frequently and don't want a separate notification for every single change. A stock-out alert always sends immediately regardless of this setting, since it's more time-sensitive than a price change.

## `targetCurrency` (type: `string`):

eBay auto-localizes the price currency it displays based on the scraping proxy's exit country, so consecutive runs can show the same item's price in different currencies. Every price is converted to this currency (via a free daily exchange rate) before comparing runs, so price-drop detection stays accurate regardless of which currency eBay happened to display. The raw `price`/`currency` eBay actually showed are still recorded alongside the converted `normalizedPrice` for reference. 3-letter ISO 4217 code, e.g. USD, EUR, GBP.

## `aiEnrichment` (type: `boolean`):

Optional, off by default. When enabled, each newly-scraped or newly-retitled item's title is sent to an LLM to extract structured `aiBrand`/`aiModel`/`aiCategory` fields — useful for filtering or grouping messy titles. Charged as a separate "ai-enrichment" event, once per item actually enriched (unchanged titles on later runs aren't re-charged). Requires that event to be configured with a price in this Actor's monetization setup — leave this off otherwise.

## `enableInsightSignals` (type: `boolean`):

Optional, off by default. Flags three patterns a single-run diff can't see on its own: `consecutiveDropStreak` (price has just dropped for 3 runs in a row — real momentum, not a single blip), `isNewAllTimeLow` (the lowest price ever recorded for this item), and `isUnderpricedVsHistory` (price just dropped 15%+ below its own rolling average). Computed entirely from price history this Actor already tracks — no extra scraping. Charged as a separate "price-insight" event, once per signal actually fired (not per run or per item checked). Requires that event to be configured with a price in this Actor's monetization setup — leave this off otherwise.

## `trackListingVariations` (type: `boolean`):

Optional, off by default. eBay listings with a size/color/etc. picker only ever show one price for the whole listing until a shopper picks a specific combination — without this, that single ambiguous price is tracked, which can be outright wrong (e.g. a placeholder value) rather than just imprecise. Enabling this clicks through every combination (capped at 24 per listing) and tracks each one as its own item, each billed as its own "result"/"checked-unchanged" event — a real cost multiplier for any tracked listing that has variations, not just a data-shape change.

## `stateNamespace` (type: `string`):

Leave empty (default) unless you run more than one schedule/Task of this Actor tracking different items. Apify's named storage (this Actor's price history, carry-forward state, etc.) is scoped to your whole account, not per-schedule — two schedules left at the default would silently share and corrupt each other's history. Set a distinct value per schedule (e.g. "phones", "shoes") to keep them fully isolated. Only letters, numbers, hyphens, and underscores are used; anything else is stripped.

## Actor input object example

```json
{
  "urls": [
    "https://www.ebay.com/sch/i.html?_nkw=usb+cable"
  ],
  "checkIntervalHours": 24,
  "priceDropThresholdPercent": 10,
  "priceIncreaseThresholdPercent": 0,
  "notificationFormat": "raw",
  "alertOnStockZero": true,
  "alertDigestIntervalHours": 0,
  "targetCurrency": "USD",
  "aiEnrichment": false,
  "enableInsightSignals": false,
  "trackListingVariations": false
}
```

# Actor output Schema

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

Every item tracked this run, with full current-state fields — price, currency, normalized price, stock, condition, seller info, and (if enabled) AI-extracted brand/model/category and price insight signals — one row per item.

## `runSummary` (type: `string`):

A plain-language one-line summary of this run — what changed and what's worth a look — plus the underlying counts (freshly-scraped, carried-forward, unrecoverable) and reliability status.

# 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.ebay.com/sch/i.html?_nkw=usb+cable"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("alex1313/ebay-price-inventory-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 = { "urls": ["https://www.ebay.com/sch/i.html?_nkw=usb+cable"] }

# Run the Actor and wait for it to finish
run = client.actor("alex1313/ebay-price-inventory-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 '{
  "urls": [
    "https://www.ebay.com/sch/i.html?_nkw=usb+cable"
  ]
}' |
apify call alex1313/ebay-price-inventory-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,alex1313/ebay-price-inventory-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/ilbBLCPaqFqcfDAfm/builds/yUD8wUKlno0ebimrc/openapi.json
