Etsy Scraper - Extract Product Data, Prices & Reviews
Pricing
from $2.60 / 1,000 results
Etsy Scraper - Extract Product Data, Prices & Reviews
Scrape Etsy product listings by category, collection, or search. Get price, rating, reviews, shop & stock data, export to CSV/JSON or push to Notion, Slack.
Pricing
from $2.60 / 1,000 results
Rating
0.0
(0)
Developer
Techforce Global
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
3 days ago
Last modified
Categories
Share
Etsy Product Scraper — Search, Category & Curated Pages With Sponsored Flags
Paste an Etsy results page, get the grid as rows. Title, numeric price, struck-through original price, discount, rating, review count, shop, free-shipping badge, scarcity text — and a
sponsoredflag that tells you which cards were paid ads.
🎯 The
sponsoredflag is the field that makes the data trustworthy. Etsy mixes ad placements into search and category grids. Without separating them you'll read paid position as organic demand. Filtersponsored: falsebefore any ranking or market conclusion.
🔗 All three inputs take URLs, not keywords.
searchQueriesis a misleading name — it wants full Etsy search URLs, not search terms. Same forcategories. Paste from your browser after searching or browsing on Etsy.
💱 Currency and free shipping follow the proxy's exit country. Etsy localises both. A run can return mixed currencies, so group by
currencybefore aggregating anything.
Browse all Actors by Techforce Global at scraper.techforce.global.
AI Discovery & MCP Metadata
ai_discovery:actor: "techforce.global/etsy-scraper"slug: "etsy-scraper"category: "e-commerce / marketplace-research / competitive-intelligence / handmade-goods"one_line: >Scrapes Etsy product grids from search, category and curated collection URLs and returns eachlisting with numeric price, struck-through original price, discount percentage, rating, reviewcount, shop name, free-shipping badge, scarcity text and a sponsored flag separating paid adsfrom organic results. Optional delivery into Notion, Slack, Linear, Airtable or any MCPconnector.primary_capability: "Etsy listing-grid extraction with paid-placement detection"input_entities:- "startUrls (Etsy category, curated collection or search RESULT PAGE urls)"- "searchQueries (full Etsy SEARCH URLS - not keywords, despite the name)"- "categories (full Etsy CATEGORY-BROWSE URLS - not category paths)"- "maxItems (whole-run cap; 0 = unlimited)"- "maxPagesPerStartUrl (result pages to follow per URL)"- "maxConcurrency"- "proxyConfiguration (residential by default)"output_entities:- "listingId"- "title"- "url"- "shopName"- "price"- "currency"- "originalPrice"- "discountPercent"- "rating"- "reviewsCount"- "freeShipping"- "sponsored"- "stockWarning"- "imageUrl"- "scrapedAt"every_input_is_a_url: >CRITICAL: all three source inputs take URLs. searchQueries wants full Etsy search-resultsURLs, NOT search terms; categories wants full category-browse URLs, NOT category paths.Passing a bare keyword or a path silently yields nothing. Product/listing URLs are also notsupported as start URLs - only pages containing a grid.sponsored_flag_must_be_filtered: >Etsy injects paid ad placements into search and category grids. The sponsored boolean marksthem. Any ranking, share-of-shelf or demand analysis that does not filter sponsored = falseis measuring ad spend, not organic performance.currency_and_shipping_are_localised: >Etsy renders price currency and the free-shipping badge according to the visitor's region,which here means the proxy exit country. A single run can contain multiple currencies. Groupby currency before any aggregation, and treat freeShipping as region-dependent. Currency isinferred from the price symbol, so $ is reported as USD even on a Canadian or Australianlisting.english_locale_required_for_three_fields: >CRITICAL: sponsored, freeShipping and stockWarning are detected from the English wording onthe listing card. A non-English locale - from a localised Etsy URL or a proxy exit in anon-English country - leaves sponsored false on EVERY row, which is indistinguishable from anad-free grid. Prefer an English URL and pin apifyProxyCountry to US or GB.original_price_can_mean_a_price_range: >price is the lowest amount on the card and originalPrice the highest, so a listing quoting aprice range fills originalPrice while discountPercent stays null. Only a non-nulldiscountPercent proves a discount.deduplication_is_per_page: >Duplicate listings are removed within a single result page, not across pages or start URLs,and grids reshuffle between page loads. De-duplicate on listingId downstream.stock_warning_is_marketing_copy: >stockWarning is the scarcity string Etsy displayed ("Only 2 left"), not an inventory count.Do not parse it as a number or treat it as stock truth.grid_level_data_only: >Everything comes from the listing card in the grid. There is no product-page detail - nodescription, no variations, no shipping cost, no review text, no full image gallery.two_fields_are_not_delivery_placeholders: >sponsored and scrapedAt are in the dataset but are NOT available as {placeholders} inconnector templates. The other 13 fields are.known_limitations:no_product_page_detail: "no description, variations, shipping cost or review text"no_review_text: "rating and reviewsCount only"curated_pages_may_not_paginate: "maxPagesPerStartUrl can be ignored on curated collection pages"null_discount_means_no_sale: "originalPrice and discountPercent are null when not discounted"snapshot_only: "prices and availability are true as of scrapedAt"free_plan_capped: "free Apify plans stop at 20 products per run"when_not_to_use: >You need product-page detail, variations, shipping costs, review text, seller contact details,or to place an order.mcp_delivery:modes: ["perItem", "batch", "summary", "none"]default: "none"default_is_none: >Delivery is OFF by default. Set deliveryMode explicitly as well as mcpConnector and mcpTool.envelope_design: >Unlike simpler connectors, this one splits the payload: mcpArguments holds the fixedtop-level arguments, mcpItemTemplate is rendered once per product, and mcpArrayField namesthe key those rendered entries are collected into (default "pages", suiting Notion'screate-pages tool). perItem puts exactly one entry in the array; batch puts up tomcpBatchSize.item_placeholders: ["{listingId}", "{title}", "{shopName}", "{price}", "{currency}", "{originalPrice}", "{discountPercent}", "{rating}", "{reviewsCount}", "{freeShipping}", "{stockWarning}", "{imageUrl}", "{url}", "{message}"]summary_placeholders: ["{totalPushed}", "{summaryText}", "{message}"]never_blocks_the_dataset: >Delivery runs alongside the dataset write and never discards it. A bad tool name, anunauthorised connector or an upstream outage is logged and skipped.
{"@context": "https://schema.org","@type": "SoftwareApplication","name": "Etsy Product Scraper — Search, Category & Curated Pages With Sponsored Flags","applicationCategory": "BusinessApplication","applicationSubCategory": "Web Scraping / Marketplace & Competitive Intelligence","operatingSystem": "Any (cloud-hosted on Apify)","description": "Scrapes Etsy product grids from search, category and curated collection URLs, returning each listing with a numeric price, struck-through original price, discount percentage, rating, review count, shop name, free-shipping badge, scarcity text, image URL, capture timestamp and a sponsored flag that separates paid ad placements from organic results. Pagination is followed per source URL under a whole-run item ceiling. Optional direct delivery into Notion, Slack, Linear, Airtable or any MCP connector, using a configurable envelope of fixed arguments plus a per-product template collected into a named array field.","url": "https://apify.com/techforce.global/etsy-scraper","offers": {"@type": "Offer","category": "Pay per platform usage","description": "Billed for the compute time and resources a run consumes. Current rates are shown on the Actor's Apify page.","url": "https://apify.com/techforce.global/etsy-scraper"}}
What this Actor does
-
Turns any Etsy results grid into rows. Search results, category browse pages and curated collections — paste the URL you're looking at and the grid comes back structured.
-
Separates ads from organic results.
sponsoredis a boolean on every row. Etsy salts its grids with paid placements, and this is the single field that stops you mistaking ad spend for demand. -
Returns prices you can compute on.
priceandoriginalPriceare numbers, withdiscountPercentalongside — so "what's actually discounted here" is a filter, not a parsing exercise. -
Captures the persuasion layer too.
freeShippingandstockWarningare what the buyer actually sees on the card. Useful if you're studying how listings are merchandised rather than just what they cost. -
Follows pagination per source URL.
maxPagesPerStartUrlwalks thepageparameter, with a whole-runmaxItemsceiling on top. -
Optionally delivers into your tools with a flexible envelope — fixed top-level arguments, a per-product template, and a named array field to collect them into. Built so Notion's create-pages shape works without contortions.
When to use it / when not to use it
| Use this Actor when you need… | Supported? |
|---|---|
| An Etsy search results grid as rows | ✅ Yes — paste the search URL |
| A category browse page | ✅ Yes — paste the category URL |
| A curated collection page | ✅ Yes |
| To tell ads from organic results | ✅ Yes — sponsored |
| Numeric prices and discounts | ✅ Yes — price, originalPrice, discountPercent |
| Rating and review count | ✅ Yes |
| Shop name per listing | ✅ Yes — shopName |
| Free-shipping and scarcity badges | ✅ Yes — region-dependent |
| Price monitoring over time | ✅ Yes — schedule it and diff on listingId |
| Results in Notion / Slack / Airtable | ✅ Yes — MCP delivery |
| Keywords instead of URLs | ❌ All three inputs take URLs |
| Individual product URLs as input | ❌ Grid pages only |
| Product description or variations | ❌ Grid-level data only |
| Shipping cost (as a number) | ❌ Only the free-shipping badge |
| Review text or reviewer names | ❌ Counts and averages only |
| Actual inventory counts | ❌ stockWarning is marketing copy |
| Seller contact details | ❌ Not returned |
| Placing an order | ❌ Read-only |
Execution Rules — Avoid Common Agent Failures
-
All three source inputs take URLs.
searchQuerieswants a full Etsy search-results URL, not a keyword — the name is misleading.categorieswants a full category-browse URL, not a path likejewelry/necklaces. Paste from your browser after searching or browsing. A bare keyword yields nothing, with no error. -
Product URLs are not valid start URLs. Only pages containing a grid — search, category or curated. A single listing URL won't work.
-
Supply at least one of the three. None is marked required, so an empty input passes validation and scrapes nothing.
-
Filter
sponsored: falsebefore any ranking claim. Etsy mixes paid placements into grids. A "top 20 by position" that includes sponsored rows is a measure of ad spend. This is the most consequential mistake you can make with this dataset. -
currencycan vary within one run. Etsy localises pricing by visitor region, which here follows the proxy exit country. Group bycurrencybefore aggregating — and if you need one currency, pinapifyProxyCountryinproxyConfiguration. Pin an English-speaking country (US,GB) — see the next rule for why. -
The grid must come back in English, or three fields go quiet.
sponsored,freeShippingandstockWarningare all read from the wording on the card. A non-English locale — from a localised Etsy URL or a proxy exit in a non-English country — leavessponsoredfalseon every row, which reads exactly like an ad-free grid. If that column is uniformlyfalse, check the locale before you believe it. -
freeShippingis region-dependent too. It's what the card showed for that request, not a universal property of the listing. -
stockWarningis marketing copy, not inventory. "Only 2 left" is a string Etsy chose to display. Don't parse a number out of it or treat it as stock truth. -
discountPercentis what proves a sale — notoriginalPrice. Null discount fields mean not on sale: a signal, not missing data. But the reverse does not hold.priceis the lowest amount on the card andoriginalPricethe highest, so a listing quoting a price range fills inoriginalPricewithdiscountPercentstill null. Filter on non-nulldiscountPercentto get actual discounts. -
maxItemsis a whole-run ceiling;maxPagesPerStartUrlis per URL. Three URLs at 3 pages each is 9 page loads, butmaxItemscan stop it early. Set both deliberately. -
Curated collection pages may ignore pagination. Getting one page from a curated URL despite
maxPagesPerStartUrl: 3is expected, not a bug. -
Everything is grid-level. No descriptions, variations, shipping costs, review text or image galleries. Don't build a pipeline expecting more.
-
deliveryModedefaults tonone. Delivery is off until you set it explicitly and providemcpConnectorandmcpTool. -
sponsoredandscrapedAtare not delivery placeholders. They're in the dataset; the other 13 fields are available in templates. -
Free Apify plans stop at 20 products per run. The run succeeds and says so.
Input Contract
Sources — all three take URLs
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
startUrls | array | see rule 3 | — | Etsy category, curated or search result page URLs. Product URLs are not supported. |
searchQueries | array<string> | see rule 3 | — | Full Etsy search URLs — e.g. https://www.etsy.com/search?q=statement+necklace. Not keywords. |
categories | array<string> | see rule 3 | — | Full Etsy category URLs — e.g. https://www.etsy.com/c/jewelry/necklaces. Not paths. |
Limits and runtime
| Parameter | Type | Default | Description |
|---|---|---|---|
maxItems | integer | 100 | Whole-run product cap. 0 = unlimited. |
maxPagesPerStartUrl | integer | 3 | Result pages to follow per URL. 1 = first page only. Curated pages may ignore it. |
maxConcurrency | integer | 5 | Parallel requests. Lower if you're being blocked. |
proxyConfiguration | object | Apify Proxy, residential | Leave enabled. Pin apifyProxyCountry if you need one currency. |
Delivery (optional)
| Parameter | Type | Default | Description |
|---|---|---|---|
mcpConnector | string | — | MCP connector to deliver into. The Console offers a picker of connectors you've authorised. |
mcpTool | string | "" | Tool to call — e.g. notion-create-pages, send_message. Required when a connector is set. |
deliveryMode | string | "none" | perItem, batch, summary, none. Off by default. |
mcpArguments | object | {} | Fixed top-level arguments — everything except the product list. |
mcpItemTemplate | object | {} | Shape of one product entry, rendered once per product. |
mcpArrayField | string | "pages" | Key in mcpArguments that receives the rendered entries. "pages" suits Notion's create-pages tool. |
mcpBatchSize | integer | 10 | batch mode only — products per connector call. |
mcpMessageTemplate | string | "" | Text rendered into {message}. |
The envelope split is unusual and worth understanding:
mcpArgumentsis the wrapper,mcpItemTemplateis the repeated element, andmcpArrayFieldnames where the elements go. InperItemthe array holds exactly one entry per call; inbatchit holds up tomcpBatchSize.
Delivery placeholders
| Scope | Placeholders |
|---|---|
Per product (mcpItemTemplate) | {listingId} {title} {shopName} {price} {currency} {originalPrice} {discountPercent} {rating} {reviewsCount} {freeShipping} {stockWarning} {imageUrl} {url} {message} |
| Summary mode | {totalPushed} {summaryText} {message} |
sponsoredandscrapedAtare not available as placeholders.
Output Contract
One dataset item per listing card.
| Field | Type | Nullable | Meaning |
|---|---|---|---|
listingId | string | yes | Etsy's listing id. The stable key for joining runs. |
title | string | yes | Listing title from the card. |
url | string | yes | Canonical listing URL, tracking parameters stripped. |
shopName | string | yes | Selling shop. Often a sole trader — see Compliance. |
price | number | yes | Current price, numeric. On a price-range card this is the lowest amount shown. |
currency | string | yes | Currency as rendered. Can vary within a run — Etsy localises. |
originalPrice | number | yes | Struck-through pre-sale price, or the top of a price range. Null when the card showed a single price. |
discountPercent | number | yes | Discount as displayed. Null when not discounted — this, not originalPrice, is the proof of a sale. |
rating | number | yes | Average stars out of 5. Null when unreviewed. |
reviewsCount | integer | yes | Review count. Null or 0 for a new listing. |
freeShipping | boolean | yes | Free-shipping badge. Region-dependent. |
sponsored | boolean | yes | true = a paid ad placement. Filter before ranking analysis. Read from the English card text — all-false means the locale, not a clean grid. |
stockWarning | string | yes | Scarcity text, e.g. "Only 2 left". Marketing copy, not inventory. |
imageUrl | string | yes | Primary listing image. Third-party copyrighted media. |
scrapedAt | string | yes | ISO-8601 UTC capture time. The row's shelf life. |
Missing values are genuine
null, not"N/A". Null discount fields mean no sale.
Example output object
{"listingId": "1234567890","title": "Hammered Brass Statement Necklace, Handmade Minimalist Jewellery","url": "https://www.etsy.com/listing/1234567890/hammered-brass-statement-necklace","shopName": "NorthLightMetalwork","price": 42.0,"currency": "USD","originalPrice": 60.0,"discountPercent": 30,"rating": 4.9,"reviewsCount": 1284,"freeShipping": true,"sponsored": false,"stockWarning": "Only 3 left","imageUrl": "https://i.etsystatic.com/<image-path>.jpg","scrapedAt": "2026-08-25T14:02:11.884213Z"}
Values illustrate shape and formatting — the listing and shop are invented. Note
sponsored: false: a real grid will contain a mix, and that field is what lets you tell.
What a successful run looks like
- Status:
SUCCEEDED - Dataset: one item per listing card, as each result page is parsed
- Log: the resolved sources, a per-page count as pagination proceeds, and a final total
- Zero results is also a success. A keyword passed where a URL was expected, a product URL
used as a start URL, a blocked request, or a genuinely empty grid all return an empty dataset
with
SUCCEEDED. Check that your inputs are URLs first — that's the most common cause.
Dataset views
| View | Shows |
|---|---|
| Products | Image, title, price, discount, rating, shop, badges |
| Organic vs Sponsored | The sponsored column first — use this before any ranking analysis |
| Deals & Discounts | Discount-first; null means not on sale |
| Shops & Capture | Shop, listing id and capture time, for joining scheduled runs |
Invocation & Integration
Apify Console
Open the Actor page, paste an Etsy search or category URL, leave the proxy at its default, click Start.
MCP — add as an agent tool
$claude mcp add apify --transport http https://mcp.apify.com/?actors=techforce.global/etsy-scraper
When an agent calls this Actor, it must supply URLs, not keywords. If a user asks for "statement necklaces on Etsy", the agent needs to construct
https://www.etsy.com/search?q=statement+necklacefirst.
Python
from apify_client import ApifyClientclient = ApifyClient("<YOUR_APIFY_TOKEN>")run = client.actor("techforce.global/etsy-scraper").call(run_input={# A full search URL — NOT the keyword "statement necklace"."searchQueries": ["https://www.etsy.com/search?q=statement+necklace"],"maxItems": 200,"maxPagesPerStartUrl": 3,"proxyConfiguration": {"useApifyProxy": True,"apifyProxyGroups": ["RESIDENTIAL"],"apifyProxyCountry": "US", # pin the country to pin the currency},})rows = list(client.dataset(run["defaultDatasetId"]).iterate_items())# Ads first — never analyse ranking without separating them.organic = [r for r in rows if not r.get("sponsored")]print(f"{len(organic)}/{len(rows)} organic ({len(rows) - len(organic)} sponsored)")# Currency can vary in one run — group before you aggregate.from collections import defaultdictby_cur = defaultdict(list)for r in organic:if r.get("price") is not None:by_cur[r.get("currency") or "?"].append(r["price"])for cur, prices in by_cur.items():prices.sort()print(f"{cur} n={len(prices):<4} median {prices[len(prices) // 2]:.2f}")
JavaScript / TypeScript
import { ApifyClient } from 'apify-client';const client = new ApifyClient({ token: '<YOUR_APIFY_TOKEN>' });const run = await client.actor('techforce.global/etsy-scraper').call({categories: ['https://www.etsy.com/c/jewelry/necklaces'],maxItems: 300,maxPagesPerStartUrl: 5,proxyConfiguration: { useApifyProxy: true, apifyProxyGroups: ['RESIDENTIAL'] },});const { items } = await client.dataset(run.defaultDatasetId).listItems();// Genuine discounts only — null discount means "not on sale".const onSale = items.filter((i) => !i.sponsored && i.discountPercent !== null).sort((a, b) => b.discountPercent - a.discountPercent);console.table(onSale.slice(0, 10).map(({ title, price, originalPrice, currency, discountPercent, shopName }) =>({ title: title?.slice(0, 40), was: `${currency} ${originalPrice}`, now: `${currency} ${price}`, off: `${discountPercent}%`, shopName })));
cURL — synchronous (waits, returns the dataset)
curl -X POST "https://api.apify.com/v2/acts/techforce.global~etsy-scraper/run-sync-get-dataset-items?token=<YOUR_APIFY_TOKEN>" \-H 'Content-Type: application/json' \-d '{"searchQueries": ["https://www.etsy.com/search?q=ceramic+mug"],"maxItems": 50,"maxPagesPerStartUrl": 2,"proxyConfiguration": { "useApifyProxy": true, "apifyProxyGroups": ["RESIDENTIAL"] }}'
cURL — asynchronous with a spend cap
curl -X POST "https://api.apify.com/v2/acts/techforce.global~etsy-scraper/runs?token=<YOUR_APIFY_TOKEN>&maxTotalChargeUsd=3" \-H 'Content-Type: application/json' \-d '{ "categories": ["https://www.etsy.com/c/jewelry"], "maxItems": 0, "maxPagesPerStartUrl": 10 }'
maxItems: 0 is unlimited — maxTotalChargeUsd is then your only ceiling.
Competitive analysis — the configuration that answers the question
{"searchQueries": ["https://www.etsy.com/search?q=personalised+dog+collar"],"maxItems": 300,"maxPagesPerStartUrl": 5,"proxyConfiguration": {"useApifyProxy": true,"apifyProxyGroups": ["RESIDENTIAL"],"apifyProxyCountry": "US"}}
Pinning the country pins the currency, so prices are comparable. Then filter sponsored: false
and group by shopName to see who actually owns the organic grid.
With MCP delivery — a Notion database of listings
{"searchQueries": ["https://www.etsy.com/search?q=ceramic+mug"],"maxItems": 100,"proxyConfiguration": { "useApifyProxy": true, "apifyProxyGroups": ["RESIDENTIAL"] },"mcpConnector": "notion","mcpTool": "notion-create-pages","deliveryMode": "batch","mcpBatchSize": 10,"mcpArrayField": "pages","mcpArguments": {"parent": { "database_id": "<YOUR_NOTION_DATABASE_ID>" }},"mcpItemTemplate": {"properties": {"Name": "{title}","Shop": "{shopName}","Price": "{price}","Rating": "{rating}","Link": "{url}"}}}
mcpArguments is the wrapper, mcpItemTemplate is one row, mcpArrayField collects them.
batch with mcpBatchSize: 10 is ten products per call — far fewer calls than perItem.
Scheduling for price monitoring
Put a fixed input on a schedule and join runs on
listingId. Because originalPrice is captured too, you can tell a genuine price drop from a
changed "was" price — and because sponsored is captured, you can track whether a competitor
started buying ads.
Autonomous Agent Error Handling & Resolution Matrix
Each row is a condition observable from the run's status, log or dataset.
| Condition | What you see | Cause | Deterministic action |
|---|---|---|---|
NO_SOURCES | SUCCEEDED, empty dataset | All three source inputs empty | Modify Input: supply at least one URL. |
KEYWORD_PASSED_AS_URL | SUCCEEDED, empty dataset | A bare keyword given to searchQueries, or a path to categories | Modify Input: use a full Etsy URL. This is the most common cause of an empty run. |
PRODUCT_URL_AS_START_URL | Nothing from that URL | Listing URLs aren't supported | Modify Input: use a search, category or curated grid page. |
ZERO_RESULTS | SUCCEEDED, empty dataset | Any of the above, blocking, or a genuinely empty grid | Check your inputs are URLs first, then enable/upgrade the proxy and retry once. |
MIXED_CURRENCIES | currency differs across rows | Etsy localises by region | Expected. Group by currency, or pin apifyProxyCountry. |
SPONSORED_SKEWING_ANALYSIS | Rankings look implausible | Paid placements counted as organic | Fix Consumer: filter sponsored: false. |
NULL_DISCOUNT_FIELDS | originalPrice / discountPercent null | The listing isn't on sale | Expected — a signal. Filter non-null discountPercent for deals. |
PRICE_RANGE_LOOKS_LIKE_A_SALE | originalPrice set, discountPercent null | The card quoted a price range, not a discount | Fix Consumer: require non-null discountPercent before calling anything a sale. |
SPONSORED_ALWAYS_FALSE | Every row has sponsored: false | The grid came back in a non-English locale | Modify Input: use an English Etsy URL, or pin apifyProxyCountry to US/GB. freeShipping and stockWarning are affected too. |
DUPLICATE_LISTINGS | The same listingId twice | De-duplication is per page, and grids reshuffle between page loads | Fix Consumer: de-duplicate on listingId. |
START_URLS_UNVISITED | Later source URLs never appear in the log | The run's request budget is derived from maxItems | Modify Input: raise maxItems, or split the URLs across runs. |
NULL_PRICE | price is null | The card showed no price | Expected. Null-check before arithmetic. |
FEWER_PAGES_THAN_REQUESTED | Fewer pages walked than maxPagesPerStartUrl | Grid ran out, or a curated page ignored pagination | Expected. Not a failure. |
MAX_ITEMS_TRUNCATED | Fewer rows than pages × cards | maxItems bound first | Modify Input: raise maxItems. |
STOCK_WARNING_NOT_A_NUMBER | stockWarning won't parse | It's marketing copy by design | Fix Expectation: treat it as a string. |
LIKELY_BLOCKED | Empty or thin dataset with the proxy on | Concurrency too high, or transient block | Modify Input: maxConcurrency: 2 and retry once. |
FREE_PLAN_CAPPED | 20 products; log says so | Free Apify plan | Upgrade the plan. |
CONNECTOR_NOT_DELIVERING | Connector set, nothing delivered, no error | deliveryMode still none (the default) | Modify Input: set it explicitly. |
CONNECTOR_ARRAY_FIELD_WRONG | Connector rejects the payload | mcpArrayField doesn't match the tool's expected key | Modify Input: set it to the key the tool wants (pages for Notion create-pages). |
CONNECTOR_TOOL_NOT_FOUND | Log: tool not available, followed by the available tools | mcpTool name is wrong | Modify Input: copy an exact name from that log line. Dataset unaffected. |
CONNECTOR_UNREACHABLE | Log: could not reach the connector | Connector not authorised, or running outside the Apify platform | Authorise it in Apify, or set deliveryMode: "none". Data already saved. |
Verify any platform-level HTTP status behaviour against the Apify API reference before relying on it in automation — the rows above describe this Actor's observable behaviour, not Apify's transport layer.
Natural Language → Actor Mapping
Every row below assumes the caller builds the Etsy URL first.
| A user says… | Intent | Input |
|---|---|---|
| "Statement necklaces on Etsy" | Search grid | {"searchQueries":["https://www.etsy.com/search?q=statement+necklace"]} |
| "Everything in the necklaces category" | Category | {"categories":["https://www.etsy.com/c/jewelry/necklaces"]} |
| "Scrape this Etsy page" | Paste-through | {"startUrls":[{"url":"<the-url>"}]} |
| "Who ranks organically for X?" | Ranking | Search URL + maxItems: 300, then filter sponsored: false |
| "How much do ceramic mugs sell for?" | Price benchmark | Search URL + pin apifyProxyCountry, then group by currency |
| "Best discounts right now" | Deals | Search URL + maxItems: 300, then sort discountPercent |
| "Which shops dominate this niche?" | Shop share | Search URL, filter organic, group by shopName |
| "Are competitors running ads here?" | Ad presence | Search URL, then count sponsored: true |
| "Track this category's prices weekly" | Scheduled monitor | Category URL on a schedule; join on listingId |
| "Just a quick sample" | Cheap probe | {"searchQueries":["<url>"],"maxItems":10,"maxPagesPerStartUrl":1} |
| "It returned nothing" | Wrong input type | Check you passed a URL, not a keyword |
| "Put these listings in Notion" | Scrape + deliver | Above, plus the delivery envelope |
Should NOT route here: bare keywords — build the search URL first · individual product URLs · product descriptions or variations · shipping costs as numbers · review text or reviewer names · real inventory counts · seller contact details · placing an order · Amazon or other marketplaces (see the Amazon and Flipkart Actors).
Agent Execution & Routing Logic
Does the user want ETSY listing data?├── NO → not this Actor.│ Amazon → Amazon Product Scraper│ Flipkart → Flipkart Search Scraper│ descriptions/variations → not returned (grid data only)│ placing an order → read-only Actor│└── YES│├── FIRST: do you have a URL, or a keyword?│ ├── keyword → BUILD THE URL:│ │ https://www.etsy.com/search?q=<url-encoded terms>│ │ then pass it in searchQueries│ ├── category → build https://www.etsy.com/c/<path>, pass in categories│ ├── a pasted grid URL → startUrls│ └── a PRODUCT url → NOT SUPPORTED. Find its category or search instead.│├── ALWAYS keep proxyConfiguration at residential│ └── need one currency? → pin apifyProxyCountry│├── How much?│ ├── a probe → maxItems 10, maxPagesPerStartUrl 1│ ├── normal → maxItems 100-300, pages 3-5│ └── deep → maxItems 0 + ALWAYS a spend cap│├── What is the actual question?│ ├── organic ranking → FILTER sponsored == false ← non-negotiable│ ├── price benchmark → pin country, GROUP BY currency│ ├── deals → filter discountPercent != null, sort desc│ ├── shop share → filter organic, group by shopName│ ├── ad presence → count sponsored == true│ └── change over time → SCHEDULE it; join on listingId│└── Should results land in another app?├── yes → deliveryMode (DEFAULT none — set it!) + the envelope:│ mcpArguments = fixed wrapper│ mcpItemTemplate = one product│ mcpArrayField = where entries go ("pages" for Notion)│ batch + mcpBatchSize → far fewer calls than perItem└── no → leave mcpConnector empty; dataset only
Cost Control & Pricing Transparency
This Actor bills for platform usage — the compute time and resources a run consumes — not per row. Current rates are on the Actor's Apify page, and the Console shows the live total while a run is in progress.
| Choice | Effect on cost |
|---|---|
maxItems | The direct ceiling on how much gets collected. |
maxPagesPerStartUrl × number of URLs | The number of page loads — the real driver of run time. |
maxConcurrency | Higher finishes sooner if you're not blocked. Being blocked makes it slower. |
| Passing keywords instead of URLs | Pure waste — the run costs time and returns nothing. Check this first. |
| Being blocked | Retries cost run time and return nothing. Keep the residential proxy on. |
Keeping runs predictable:
- First run: one URL,
maxItems: 10,maxPagesPerStartUrl: 1. Confirm your input really is a URL and the grid parses. - Verify the input type before scaling. The single most common wasted run here is a keyword passed where a URL belongs — it fails silently.
- Pin
apifyProxyCountryif you'll aggregate prices. Otherwise you may pay for a dataset you can't safely average. - Always set a spend cap on unattended runs, mandatory with
maxItems: 0. - Free Apify plans get 20 products per run — a connectivity check.
Companion machine-readable files
| File | What it defines |
|---|---|
| .actor/input_schema.json | Every input, its type, default and bounds |
| .actor/dataset_schema.json | All 15 output fields with types, and the four Console views |
| .actor/output_schema.json | Where the run's output is exposed |
| .actor/actor.json | Actor identity and storage wiring |
If this page and the schema files disagree, the schema files win. They are what the platform validates against at run time; this page is documentation and can lag a release.
Troubleshooting
| # | Symptom you see | Most likely cause | Fix |
|---|---|---|---|
| 1 | SUCCEEDED, empty dataset | A bare keyword was passed to searchQueries, or a path to categories | Pass a full Etsy URL. All three source inputs take URLs, and a keyword fails silently. Check this first — it is the most common cause by a wide margin. |
| 2 | One source URL returned nothing | It was a product/listing URL | Only grid pages work — search, category or curated. Use the listing's category instead. |
| 3 | SUCCEEDED, empty dataset, no sources in the log | All three source inputs were left empty | None is marked required, so an empty input passes validation. Supply at least one. |
| 4 | Empty or very thin dataset, 403s in the log | Etsy rejects datacenter IPs, usually on the first request | Keep proxyConfiguration on the default residential group. Residential proxies need a paid Apify plan — on a free plan this is the likely blocker. |
| 5 | sponsored is false on every single row | The grid was served in a non-English locale | Paid placements are recognised from the English card text. Use an English Etsy URL (/en-us/…) or pin apifyProxyCountry to US or GB. A uniformly false column is the tell — real grids always contain a mix. |
| 6 | freeShipping always false, stockWarning always null | Same cause as row 5 | These badges are read from English card text too. Fix the locale and they return. |
| 7 | originalPrice is set but discountPercent is null | The card showed a price range, not a discount | This is a range, not a sale: price is the lowest amount on the card and originalPrice the highest. Treat a discount as real only when discountPercent is non-null. |
| 8 | currency says USD for a listing you know is CAD or AUD | Currency is inferred from the symbol, and $ is assumed to be USD | Pin apifyProxyCountry so you know which $ you are looking at, and don't mix $ rows from different runs. |
| 9 | currency differs between rows in one run | Etsy localises pricing by visitor region | Expected. Group by currency before aggregating, or pin apifyProxyCountry (see rows 5 and 8 before choosing one). |
| 10 | The same listing appears twice | Duplicates are removed within a page, not across pages or start URLs | De-duplicate on listingId — the same product legitimately appears on page 1 and page 2 as the grid reshuffles. |
| 11 | Fewer pages than maxPagesPerStartUrl | The grid ran out, a curated page ignored pagination, or a page returned zero cards | Expected — pagination stops at the first page with no cards. Not a failure. |
| 12 | Fewer rows than pages x cards | maxItems bound first | Raise maxItems, or set it to 0 for unlimited and set a spend cap. |
| 13 | Exactly 20 products | Free Apify plan cap | The run still succeeds and says so in the log. Upgrade for unlimited results. |
| 14 | Later start URLs were never visited | The whole-run request budget is derived from maxItems | A small maxItems with many source URLs exhausts it early. Raise maxItems, or split the URLs across runs. |
| 15 | price is null | The card quoted no price | Expected. Null-check before any arithmetic — null never means free. |
| 16 | rating and reviewsCount are null | The listing has no reviews yet | Expected on new listings. Treat null as "unrated", not as zero stars. |
| 17 | Connector configured, nothing delivered | deliveryMode is still none — the default | Set it explicitly, and set mcpTool as well. |
| 18 | Connector rejects the payload | mcpArrayField does not match the key the tool expects | Set it to the tool's key — pages for Notion's create-pages. |
| 19 | Log: tool not available, followed by a list | mcpTool is misspelled | Copy an exact name from that log line. The dataset is unaffected. |
| 20 | Log: could not reach the connector | Not authorised, or running locally rather than on the platform | Authorise it in Apify and run on the platform. Your data is already saved either way. |
Diagnostic checklist before opening an issue
- Confirm every source value is a URL, not a keyword or a path. This single check explains most empty runs, and it produces no error message of any kind.
- Check the proxy. Etsy blocks datacenter IPs almost immediately; residential access requires a paid plan.
- Probe cheap:
{"searchQueries":["<one search URL>"],"maxItems":10,"maxPagesPerStartUrl":1}. If that returns rows, the pipeline works and your inputs are the problem. - Scan the
sponsoredcolumn before trusting any analysis. All-falsemeans the locale defeated the detection, not that the grid was ad-free. - Re-read
scrapedAtbefore reporting a price as wrong — this is a snapshot of a marketplace that moves continuously.
If it survives all five, open an Issues ticket with the run ID, the exact input JSON, and what you expected.
FAQ
Setup & scope
Do I need an Etsy account or API key?
No. The Actor reads public listing grids. You need an Apify account and API token only.
Is this an official Etsy API?
No. This is an independent Actor and is not affiliated with, endorsed by, or sponsored by Etsy. Etsy operates its own Open API for sanctioned access.
Do I need a paid Apify plan?
In practice, yes. Etsy rejects datacenter IPs almost immediately, so the Actor defaults to the residential proxy group — and residential proxies require a paid plan. Free plans are also capped at 20 products per run.
Can I plug it into Claude, Cursor, or a LangChain agent?
Yes — it is a native MCP tool:
claude mcp add apify --transport http https://mcp.apify.com/?actors=techforce.global/etsy-scraper
Input
Why does searchQueries not accept search terms?
Because the name is misleading — it wants a full Etsy search-results URL. Auto-building search URLs from keywords produced pages Etsy blocked, so the Actor now takes the URL you actually browsed. Search on Etsy, then copy the address bar.
What happens if I pass a keyword anyway?
Nothing visible. The run succeeds with an empty dataset and no error. It is the single most common cause of a confusing result here.
Can I scrape an individual product page?
No. Only pages containing a grid — search, category or curated collection.
Is maxItems per URL or per run?
Per run. maxPagesPerStartUrl is the per-URL control. Three URLs at three pages each is nine page loads, but maxItems can stop the run before they finish.
Why did later start URLs never get visited?
The run's total request budget is derived from maxItems, so a small cap with many source URLs can exhaust it before the last URL is reached. Raise maxItems or split the URLs across runs.
Output & data
What does the sponsored flag actually do for me?
It separates paid ad placements from organic results. Etsy salts its grids with ads, so any ranking, share-of-shelf or demand conclusion drawn without filtering sponsored: false is measuring ad spend rather than performance.
Why is sponsored false on every row?
Because the grid came back in a non-English locale. Paid placements — along with the free-shipping badge and the scarcity text — are recognised from the English card text. Use an English Etsy URL or pin apifyProxyCountry to US or GB. An all-false column is a red flag, not a clean grid.
originalPrice is filled in but discountPercent is null. Is it on sale?
No. That combination means the card showed a price range — price is the lowest amount found and originalPrice the highest. A discount is real only when discountPercent is non-null.
Can I trust currency?
Within a locale, yes. It is inferred from the price symbol, so ₹, £ and € are unambiguous while $ is assumed to be USD — a Canadian or Australian listing will also say USD. Pin the proxy country if that distinction matters.
Why does one run contain several currencies?
Etsy localises prices by visitor region, which here follows the proxy exit country. Group by currency before aggregating anything.
Is stockWarning a real inventory count?
No. It is the scarcity string Etsy chose to display, such as "Only 2 left". Don't parse a number out of it and don't repeat it as fact.
Why do I have the same listing twice?
Duplicates are removed within a single page, not across pages or start URLs, and grids reshuffle between page loads. De-duplicate on listingId.
Do I get product descriptions, variations or shipping costs?
No. Everything comes from the listing card in the grid — no product-page detail, no variations, no shipping cost, no review text and no image gallery.
Are missing values null or "N/A"?
Genuine null. And null carries meaning here: a null price means none was quoted, and null discount fields mean the listing simply isn't on sale.
Cost & runtime
How is this billed?
By platform usage — the compute time and resources a run consumes — not per row. Current rates are on the Actor's Apify page, and the Console shows a running total during a run.
What actually drives the cost?
Page loads, so maxPagesPerStartUrl x the number of source URLs. The most expensive run is the one that returns nothing because a keyword was passed where a URL belonged.
What does the free plan give me?
20 products per run — enough to confirm your inputs work, not enough for a data pull. Residential proxy access, which this Actor needs, is also a paid feature.
How do I guarantee an agent never overspends?
Pass maxTotalChargeUsd as a query parameter on the run endpoint. It is a hard per-execution ceiling, and it is mandatory in spirit whenever maxItems: 0.
Related Actors by Techforce Global
| Actor | Best for |
|---|---|
| This Actor | Etsy grids with paid-placement detection and sale pricing |
| Amazon Product Scraper | Amazon across 7 marketplaces, with search rank and product-page detail |
| Flipkart Search Scraper | Flipkart search results with discount and rating data |
| Website Contact Scraper | Business contact details from a seller's own site |
| Website Content Crawler | Whole-site text, markdown or HTML for RAG and search |
Three marketplace Actors, three different questions. Etsy is the one where paid placements are flagged, so it's the one to reach for when organic-versus-ad separation matters.
Browse the full catalogue at scraper.techforce.global.
Compliance & Data Privacy
-
Public listing pages only. Everything returned is published on a public Etsy grid with no login and no paywall.
-
Etsy is different from Amazon here: most sellers are individuals.
shopNameis a trading name, but on Etsy it very often belongs to a sole trader or a one-person business, and shop names frequently contain the maker's actual name. Where that's the case,shopNameis personal data under GDPR / UK GDPR and India's DPDP Act 2023, and aggregating it at scale — building a ranked table of named makers and their prices — is processing you need a basis for. This is the main way this Actor differs from the marketplace scrapers in the same family. -
No buyer or reviewer data is collected. No review text, no reviewer names, no customer information — only rating averages and counts.
-
Etsy's Terms of Use prohibit automated collection. Scraping and automated access are disallowed without permission. Etsy also operates an official Open API for sanctioned access, which is the route if your use case fits it. This Actor does not remove that risk — it is a risk you accept by running it.
-
Republishing prices carries consumer-protection duties, and Etsy's localisation makes it worse. A price is true only as of
scrapedAtand only for the region the run resolved to. Presenting a scraped price as "the" price — when a buyer elsewhere sees a different currency and a different shipping badge — risks misleading-advertising exposure under the UK CPRs, Australian Consumer Law and the US FTC Act. Publish the capture time and the currency. -
Treat
discountPercentandstockWarningsceptically before repeating them.discountPercentis calculated against a seller-set "was" price, andstockWarningis scarcity copy rather than inventory truth. Repeating either as a verified fact is a claim you can't support. -
Competitive monitoring is normal; price coordination is not. Watching public prices is standard. Using shared data to align pricing with competitors is antitrust exposure under EU Article 101, the UK Competition Act and the US Sherman Act. Keep monitoring internal.
-
Images are the makers' own work.
imageUrlpoints at a photograph a small seller took of something they made. A URL is not a licence — and on a handmade marketplace, reusing listing photography is a live copyright and design-appropriation issue rather than a theoretical one. -
Attribution over mirroring.
urlis on every row. Link back rather than mirroring listings, and don't present another maker's products as your own inventory. -
Respect the load you generate. Keep
maxItems,maxPagesPerStartUrlandmaxConcurrencyto what your question needs.
🆘 Support & Custom Pipeline Engineering
Need automated multi-city scheduled feeds, webhook enrichment, or enterprise data warehouse syncs?
- Email: bhavin.shah@techforceglobal.com
- Custom Enterprise Integrations: Book a 15-Minute Technical Consultation
- Maintained by: Techforce Global — Specialists in High-Performance Web Scrapers and Agentic Workflows.
🏷️ Structured data for search & AI discovery
SoftwareApplication describes the Actor; FAQPage wraps the FAQ above so those answers are
eligible for rich results, matching it question-for-question.
{"@context": "https://schema.org","@type": "SoftwareApplication","name": "Etsy Product Scraper — Search, Category & Curated Pages With Sponsored Flags","applicationCategory": "BusinessApplication","applicationSubCategory": "Web Scraping / Marketplace & Competitive Intelligence","operatingSystem": "Any (cloud-hosted on Apify)","url": "https://apify.com/techforce.global/etsy-scraper","description": "Scrapes Etsy product grids from search, category and curated collection URLs, returning each listing with a numeric price, struck-through original price, discount percentage, rating, review count, shop name, free-shipping badge, scarcity text, image URL, capture timestamp and a sponsored flag that separates paid ad placements from organic results. Pagination is followed per source URL under a whole-run item ceiling. Optional direct delivery into Notion, Slack, Linear, Airtable or any MCP connector, using a configurable envelope of fixed arguments plus a per-product template collected into a named array field.","author": {"@type": "Organization","name": "Techforce Global","url": "https://www.techforceglobal.com"},"provider": {"@type": "Organization","name": "Apify","url": "https://apify.com"},"offers": {"@type": "Offer","category": "Pay per platform usage","description": "Billed for the compute time and resources a run consumes. Current rates are shown on the Actor's Apify page.","url": "https://apify.com/techforce.global/etsy-scraper"},"featureList": ["Search, category and curated collection grids from pasted URLs","Sponsored flag separating paid ad placements from organic results","Numeric price plus struck-through original price and discount percentage","Rating and review count per listing","Free-shipping badge and scarcity text as the buyer sees them","Shop name per listing for seller-level aggregation","Per-row capture timestamp for auditable, diffable snapshots","Pagination per source URL under a whole-run item ceiling","Flexible MCP delivery envelope with per-product templates and batching","Four Console dataset views including organic-versus-sponsored"],"isAccessibleForFree": false,"softwareRequirements": "Apify account and API token; a paid plan for residential proxies","keywords": "Etsy scraper, Etsy product data, handmade marketplace, sponsored listings, ad detection, price monitoring, competitive intelligence, discount tracking, shop analysis, MCP connector"}
{"@context": "https://schema.org","@type": "FAQPage","mainEntity": [{"@type": "Question","name": "Do I need an Etsy account or API key?","acceptedAnswer": {"@type": "Answer","text": "No. The Actor reads public listing grids. You need an Apify account and API token only."}},{"@type": "Question","name": "Is this an official Etsy API?","acceptedAnswer": {"@type": "Answer","text": "No. This is an independent Actor and is not affiliated with, endorsed by, or sponsored by Etsy. Etsy operates its own Open API for sanctioned access."}},{"@type": "Question","name": "Do I need a paid Apify plan?","acceptedAnswer": {"@type": "Answer","text": "In practice, yes. Etsy rejects datacenter IPs almost immediately, so the Actor defaults to the residential proxy group - and residential proxies require a paid plan. Free plans are also capped at 20 products per run."}},{"@type": "Question","name": "Can I plug it into Claude, Cursor, or a LangChain agent?","acceptedAnswer": {"@type": "Answer","text": "Yes - it is a native MCP tool: claude mcp add apify --transport http https://mcp.apify.com/?actors=techforce.global/etsy-scraper"}},{"@type": "Question","name": "Why does searchQueries not accept search terms?","acceptedAnswer": {"@type": "Answer","text": "Because the name is misleading - it wants a full Etsy search-results URL. Auto-building search URLs from keywords produced pages Etsy blocked, so the Actor now takes the URL you actually browsed. Search on Etsy, then copy the address bar."}},{"@type": "Question","name": "What happens if I pass a keyword anyway?","acceptedAnswer": {"@type": "Answer","text": "Nothing visible. The run succeeds with an empty dataset and no error. It is the single most common cause of a confusing result here."}},{"@type": "Question","name": "Can I scrape an individual product page?","acceptedAnswer": {"@type": "Answer","text": "No. Only pages containing a grid - search, category or curated collection."}},{"@type": "Question","name": "Is maxItems per URL or per run?","acceptedAnswer": {"@type": "Answer","text": "Per run. maxPagesPerStartUrl is the per-URL control. Three URLs at three pages each is nine page loads, but maxItems can stop the run before they finish."}},{"@type": "Question","name": "Why did later start URLs never get visited?","acceptedAnswer": {"@type": "Answer","text": "The run's total request budget is derived from maxItems, so a small cap with many source URLs can exhaust it before the last URL is reached. Raise maxItems or split the URLs across runs."}},{"@type": "Question","name": "What does the sponsored flag actually do for me?","acceptedAnswer": {"@type": "Answer","text": "It separates paid ad placements from organic results. Etsy salts its grids with ads, so any ranking, share-of-shelf or demand conclusion drawn without filtering sponsored: false is measuring ad spend rather than performance."}},{"@type": "Question","name": "Why is sponsored false on every row?","acceptedAnswer": {"@type": "Answer","text": "Because the grid came back in a non-English locale. Paid placements - along with the free-shipping badge and the scarcity text - are recognised from the English card text. Use an English Etsy URL or pin apifyProxyCountry to US or GB. An all-false column is a red flag, not a clean grid."}},{"@type": "Question","name": "originalPrice is filled in but discountPercent is null. Is it on sale?","acceptedAnswer": {"@type": "Answer","text": "No. That combination means the card showed a price range - price is the lowest amount found and originalPrice the highest. A discount is real only when discountPercent is non-null."}},{"@type": "Question","name": "Can I trust currency?","acceptedAnswer": {"@type": "Answer","text": "Within a locale, yes. It is inferred from the price symbol, so , and are unambiguous while $ is assumed to be USD - a Canadian or Australian listing will also say USD. Pin the proxy country if that distinction matters."}},{"@type": "Question","name": "Why does one run contain several currencies?","acceptedAnswer": {"@type": "Answer","text": "Etsy localises prices by visitor region, which here follows the proxy exit country. Group by currency before aggregating anything."}},{"@type": "Question","name": "Is stockWarning a real inventory count?","acceptedAnswer": {"@type": "Answer","text": "No. It is the scarcity string Etsy chose to display, such as \"Only 2 left\". Don't parse a number out of it and don't repeat it as fact."}},{"@type": "Question","name": "Why do I have the same listing twice?","acceptedAnswer": {"@type": "Answer","text": "Duplicates are removed within a single page, not across pages or start URLs, and grids reshuffle between page loads. De-duplicate on listingId."}},{"@type": "Question","name": "Do I get product descriptions, variations or shipping costs?","acceptedAnswer": {"@type": "Answer","text": "No. Everything comes from the listing card in the grid - no product-page detail, no variations, no shipping cost, no review text and no image gallery."}},{"@type": "Question","name": "Are missing values null or \"N/A\"?","acceptedAnswer": {"@type": "Answer","text": "Genuine null. And null carries meaning here: a null price means none was quoted, and null discount fields mean the listing simply isn't on sale."}},{"@type": "Question","name": "How is this billed?","acceptedAnswer": {"@type": "Answer","text": "By platform usage - the compute time and resources a run consumes - not per row. Current rates are on the Actor's Apify page, and the Console shows a running total during a run."}},{"@type": "Question","name": "What actually drives the cost?","acceptedAnswer": {"@type": "Answer","text": "Page loads, so maxPagesPerStartUrl x the number of source URLs. The most expensive run is the one that returns nothing because a keyword was passed where a URL belonged."}},{"@type": "Question","name": "What does the free plan give me?","acceptedAnswer": {"@type": "Answer","text": "20 products per run - enough to confirm your inputs work, not enough for a data pull. Residential proxy access, which this Actor needs, is also a paid feature."}},{"@type": "Question","name": "How do I guarantee an agent never overspends?","acceptedAnswer": {"@type": "Answer","text": "Pass maxTotalChargeUsd as a query parameter on the run endpoint. It is a hard per-execution ceiling, and it is mandatory in spirit whenever maxItems: 0."}}]}