Korean e-commerce is one of the richest, least-scraped data sources on the web. K-beauty and K-fashion drive billions in global demand, but almost every tutorial you find scrapes Amazon or Shopify — because the Korean platforms sit behind a double wall: the language, and some genuinely annoying anti-bot setups. I spent a while getting reliable, structured data out of the three that matter most — Olive Young (K-beauty), Musinsa (K-fashion), and Naver Place (local businesses + reviews). Here's what actually worked, the parts that fought back, and the rules I set for myself. Rule #1: never parse the HTML if the site talks JSON to itself Every one of these is a JavaScript SPA. If you GET the page and run Cheerio over it, you get a shell. The trick is the same everywhere: open the network tab, reload, and watch what the frontend calls to fill itself in. That call is almost always a clean JSON endpoint — the same data the page renders, minus the parsing pain and minus 90% of the breakage when they reskin the site. Musinsa was the friendliest. One endpoint powers both category rankings and keyword search: GET https://api.musinsa.com/api2/dp/v1/plp/goods ?gf=A&sortCode=POPULAR&caller=CATEGORY&category=001&page=1&size=60 Swap caller=CATEGORY&category=001 for caller=SEARCH&keyword= and you've got search. The response hands you goodsNo , brandName , normalPrice , finalPrice , saleRate , reviewCount , reviewScore — everything, already structured. No auth. (Their reviewScore is 0–100, so divide by 20 if you want a 0–5 rating.) Olive Young was two services once I switched to their global storefront ( global.oliveyoung.com ) — which is actually better for most people, because it returns English product names and USD prices instead of Korean + KRW: GET product-ranking-service.oliveyoung.com/v1/pages/ranking/sales/products # best sellers POST cbe-external-api.oliveyoung.com/display/v1/search/products/unified-search # search The domestic site ( www.oliveyoung.co.kr ) 403'd my datacenter IP immediately; the global one didn't. Worth checking both when a Korean site blocks you — the international storefront is often more permissive. Naver Place was the stubborn one. The clean data lives in the Apollo cache embedded in the search results HTML: GET https://search.naver.com/search.naver?where=nexearch&query=강남역 카페 Pull the APOLLO_STATE blob out of the page, and you get PlaceListBusinessesItem nodes with name, category, address, phone, rating, review count — plus a few review snippets linked from each place. The deeper per-place review API ( pcmap-api.place.naver.com/graphql ) exists, but it CAPTCHA-walls datacenter IPs with an HTTP 405. More on that below. The parts that fought back Datacenter IPs get filtered. All three tolerate a light touch from a normal IP but clamp down on cloud ranges fast. The fix is boring: residential proxies (Korea-geo for Naver especially). What matters in code is that a hung proxy tunnel shouldn't take down the whole run — wrap the fetch so a proxy timeout falls back to a direct connection instead of throwing. That one change turned a lot of silent zero-result runs into successful ones. "Succeeded with 0 results" is a lie you tell yourself. My first Naver cloud run reported success and returned nothing, with no error in the logs — because the code swallowed a proxy timeout and exited cleanly. If a run collects nothing, throw. A paid data tool that silently returns empty is worse than one that fails loudly. Pagination isn't always real pagination. Naver's search page repeats results after ~15 businesses; true depth requires the GraphQL route (and a residential IP). Know where your easy path ends so you don't ship something that looks paginated but isn't. Rule #2: public, non-personal data only This is the line I don't cross, and I'd encourage anyone scraping reviews to hold it too: I never collect reviewer identity. Review objects carry text , rating , date , and keywords — never nicknames, profile URLs, or user IDs. Business phone numbers and addresses are fine (they're public business info); a person's name attached to a review is not. It keeps the data useful for sentiment/market analysis without turning into a privacy problem. The output that makes it usable Whatever the source shape, everything normalizes to one flat, snake_case contract: { "source" : "oliveyoung" , "product_id" : "GA230518746" , "name" : "SKIN1004 Madagascar Centella Hyalu-Cica Water-Fit Sun Serum" , "brand" : "SKIN1004" , "price_usd" : 28.0 , "sale_price_usd" : 22.4 , "rating" : 4.9 , "review_count" : 9958 , "rank" : 1 , "url" : "https://global.oliveyoung.com/product/detail?prdtNo=GA230518746" , "scraped_at" : "2026-07-07T09:35:33+09:00" } Consistent field names and KST timestamps across all three sources mean you can dump them into the same table and diff prices over time without writing per-site glue. If you'd rather not maintain any of this I packaged all three as ready-to-run scrapers on the Apify Store — they handle the proxying, the fallbacks, and the JSON normalization, and they're priced per result so you only pay for what you pull: Olive Young (K-beauty, USD prices): https://apify.com/kdatafactory/oliveyoung-scraper Musinsa (K-fashion): https://apify.com/kdatafactory/musinsa-scraper Naver Place (businesses + reviews): https://apify.com/kdatafactory/naver-place-scraper But honestly, the endpoints above are the whole trick — if you just need a one-off pull, the network tab will get you most of the way. Happy to answer questions on any of the three in the comments.