DEV Community

Devil Scrapes
Devil Scrapes

Posted on

Fashionphile Scraper: Its Own Search Page Hides Real Prices Behind a Null Block

Quick answer

Fashionphile's own /shop search page ships a JSON block with price and sku set to null — a placeholder, not the real listing data. The real, fully-resolved inventory lives one layer down, in the Algolia search index that actually powers Fashionphile's on-site search. The Fashionphile Handbag Listings Scraper queries that index directly and returns price, condition, SKU, images, and retail-savings percent per handbag, at $3.05 per 1,000 rows.

Why does the /shop page show null prices? 🕳️

Load fashionphile.com/shop, view source, and find the embedded product JSON. The fields are there — price, sku, compare_at_price — but they're null. Not missing, not malformed: explicitly null. If you parse that block expecting real numbers, you get a dataset full of empty prices and no error to tell you why.

That block isn't where Fashionphile's search actually gets its data from. The site's own search-as-you-type box calls Algolia — the hosted search service — directly from the browser, using a public, search-only application key that ships to every visitor. That call returns the real shopify_products index: full pricing, condition grade, SKU, images, category tags, description, and inventory count, all fully resolved. This Actor makes that same class of call server-side instead of relying on the page's own placeholder block.

What a search actually returns

{
  "searchTerms": ["chanel", "hermes birkin"],
  "maxItemsPerSearch": 5,
  "proxyConfiguration": { "useApifyProxy": true }
}
Enter fullscreen mode Exit fullscreen mode
from apify_client import ApifyClient

client = ApifyClient("APIFY_TOKEN")
run = client.actor("DevilScrapes/fashionphile-handbag-listings-scraper").call(
    run_input={"searchTerms": ["chanel classic flap"], "maxItemsPerSearch": 100}
)
for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(item["title"], item["price"], item["condition"])
Enter fullscreen mode Exit fullscreen mode

And one resolved row:

{
  "object_id": "shopify_products_7841234567890",
  "sku": "FP-CHN-00042",
  "title": "Chanel Classic Medium Double Flap Black Caviar",
  "url": "https://www.fashionphile.com/products/chanel-classic-medium-double-flap-black-caviar-00042",
  "price": 5095.0,
  "compare_at_price": 6200.0,
  "retail_savings_percent": 17.8,
  "condition": "Excellent",
  "images": ["https://cdn.shopify.com/s/files/1/fashionphile/00042-front.jpg"],
  "categories": ["Handbags", "Chanel", "Flap Bags"],
  "inventory_quantity": 1,
  "search_term": "chanel",
  "scraped_at": "2026-08-24T12:00:00.000Z"
}
Enter fullscreen mode Exit fullscreen mode

retail_savings_percent and condition come from a nested meta.custom block on the hit, not a top-level field — the kind of detail that's easy to miss if you're mapping the response by hand, and easy to get silently wrong if you're not checking it against a live response.

What we handle 🛡️

  • Browser fingerprint rotation. Every request goes out through curl-cffi, impersonating a real Chrome, Firefox, or Safari TLS handshake, rotated on every retry — not a bare Python client signature.
  • Retries with backoff, on the right codes. 408 / 429 / 503 and network errors trigger up to 5 attempts, 2 seconds doubling to a 30-second cap, honoring Retry-After when it's sent. Other 4xx responses are treated as final, not retried into a request loop.
  • Session rotation on pushback. A retry doesn't just wait — it also rotates to the next browser profile and, where available, a fresh proxy session, so a run doesn't keep hammering with the exact signature that got throttled.
  • Per-search-term fault isolation. A malformed or missing field on one listing gets logged and skipped — it doesn't take the rest of that search term's results down with it. A search term that runs and genuinely matches nothing finishes as a clean zero-row success, not a failed run.
  • Typed, validated rows. Every field is Pydantic-checked before it reaches your dataset — no float where a null belongs, no silently dropped keys.

Does this scrape each listing's detail page too?

No — and that's deliberate. The Algolia hit already carries every field this Actor emits (price, condition, SKU, images, description, inventory count), so there's no second per-item HTTP request. A 500-listing search is one search request plus pagination, not 500 follow-up fetches to individual product pages.

What people build with this 💡

  • Price-arbitrage sourcing — scan brand and keyword queries for underpriced authenticated pieces before the general public finds them through Fashionphile's own storefront search.
  • Authentication and resale comps — pull condition grade, SKU, and retail-savings percent to benchmark a specific bag's resale value against current live listings.
  • Inventory monitoring — track how deep a brand or model's catalog runs on Fashionphile and at what price band, run over run.
  • Luxury resale market research — build a keyword-level snapshot of supply, condition mix, and pricing across brands for a dataset or report.

Frequently asked questions

What does 1,000 rows cost?
$3.05 — a $0.05 run-start charge plus $0.003 per unique listing. No data, no charge beyond the run-start fee.

Do I need a Fashionphile account or API key?
No. This queries Fashionphile's own public, search-only index — the same one its on-site search box calls — no login required.

Why not just parse the /shop page's JSON block directly?
Because that block's price and sku fields are null placeholders on Fashionphile's own page, not real data. Reading it as-is gets you a dataset of empty prices.

What happens if a search term matches nothing?
That term finishes as a clean, zero-row success. The run only fails if every search term could not be reached at all — a genuine no-match isn't treated as a failure.

Does this filter to handbags only?
Not strictly — a broad brand query can return non-bag accessories from the wider catalog alongside handbags. Scope your searchTerms narrowly if you need a bags-only set.

Try it

Live on the Apify Store: Fashionphile Handbag Listings Scraper. Apify gives every new account $5 of free credit, no card required to try it.


Built by Devil Scrapes — we go looking for the real data behind the placeholder block, so your dataset doesn't ship full of nulls.

Top comments (0)