Tokopedia Scraper — Products & Suppliers, One for All
Pricing
from $5.00 / 1,000 results
Tokopedia Scraper — Products & Suppliers, One for All
Search Tokopedia products over pure HTTP — no browser, no cookie, no login — and turn the results into a supplier list you can compare. Built for price research and supplier discovery in Indonesia.
Pricing
from $5.00 / 1,000 results
Rating
0.0
(0)
Developer
Muhammad Sajid Izzulhaq
Maintained by CommunityActor stats
0
Bookmarked
3
Total users
2
Monthly active users
2 days ago
Last modified
Categories
Share
Tokopedia Product Search
Search products on Tokopedia over pure HTTP and group their sellers into a comparable supplier list. The whole pipeline is pure HTTP — no headless browser.
From a residential IP (e.g. your home machine when running locally) no proxy is needed at all. From a datacenter IP — including the Apify runtime — Tokopedia silently drops the request at the Akamai edge (status 0 on every transport, not a 403), so a residential proxy is required. See Proxy below.
The endpoint used is SearchProductV5 in Tokopedia's internal GraphQL. The query document was taken from the frontend JS bundle, then cleaned of @client-marked fields the server never receives. Only three headers are required: content-type, user-agent, accept. Without accept, the server hangs until timeout — it does not reject — so no error can be caught.
Input
| Input | Type | Default | Description |
|---|---|---|---|
searchStrings | list of string | ["laptop"] | One or more product keywords. All are combined in a single run so the Toko sheet immediately sees every seller that appears under any keyword. |
maxItems | integer | 100 | Limit of unique products for the whole run, split evenly across keywords. The server accepts at most 200 rows per request, so a value above 200 means more than one request. Maximum 600. |
orderBy | enum | "23" | "23" = relevance (Tokopedia default), "5" = highest rating. |
proxy | proxy | — | Proxy Configuration component. Pick a residential proxy from your own account, or paste a custom proxy URL. If left empty, the actor falls back to the runtime's residential proxy. See Proxy below. |
minPrice | integer | — | Drop products priced below this number (Rupiah). Products without a price are dropped too. Empty = no filtering. |
maxPrice | integer | — | Drop products priced above this number (Rupiah). Products without a price are dropped too. Empty = no filtering. |
minRating | number | — | Drop products rated below this number (0-5). Products without reviews are dropped too. Empty = no filtering. |
debugDump | boolean | false | Save the raw GraphQL payload to storage. Turn on only when results look wrong. |
The hard maxItems cap lives in tokopedia_core.MAX_PLAUSIBLE_LIMIT, shared by the input schema and the code, so the number in the UI and in Python can never drift apart.
The minPrice, maxPrice, and minRating filters are client-side: applied after results are fetched, not sent to the server (Tokopedia's server is not confirmed to accept pmin/pmax/rt). That is why the number of products returned can be lower than maxItems — raise maxItems if you want more results to survive the filter. How many were dropped is logged and recorded in SUMMARY (filtered).
Proxy
Three paths, tried in order:
- The
proxyinput component — highest priority. The user picks a residential proxy from their own account, or pastes a custom proxy URL. Credentials belong to the account that runs the run, so the quota consumed is the runner's, not the actor owner's. SCRAPER_PROXY— an environment variable holding your own rented proxy inhttp://user:pass@host:portformat. Used when the Proxy component is empty.- Apify residential proxy — used automatically when both are empty and the runtime provides
APIFY_PROXY_PASSWORD. Built with groupRESIDENTIALandcountry-IDso the egress is Indonesian residential.
If none of the three exist, the actor still runs without a proxy and will fail from a datacenter IP. That is not a transport configuration bug — use a residential proxy.
Apify's default proxy group is datacenter and won't get through: it has been tested and gives the exact same status 0 as the direct path. It must be
RESIDENTIAL.
Output
One dataset row = one product, 19 columns:
no, query, page, oldID, name, url, price, priceOriginal, discountPercent, countSold, rating, shopId, shopName, shopUrl, shopCity, shopTier, categoryName, categoryBreadcrumb, imageUrl
Two views in the Output tab:
- Produk (Product) — the natural reading order when comparing suppliers: what is sold, its price, sales and satisfaction proof, then who the seller is.
- All columns — every field, raw.
Three artifacts also land in the key-value store:
products.xlsx— two sheets:Produk(15 columns, reading order) andToko(13 columns, aggregated) plus aRingkasansheet. Download it through the Storage button. The spreadsheet shows a subset of the 19 dataset columns; the rest (oldID, longurl,imageUrl, breadcrumb) are intentionally left out so the table stays readable.SUMMARY— run statistics: number of requests, duplicates, empty-response retries, and whether the server ran out of results.
What to know when reading the columns
pricehas no divisor.price.numberis identical to the number shown on the page (98254= Rp98,254). There is no /100 or /1000 anywhere in the codebase.discountPercentis computed from the promo label, not fromdiscountPercentagein the response — that field measured 0 on 100/100 products. When theri_ribbonlabel is absent, the value isnull, not0.countSoldcan benull."250+ terjual"means at least 250, and"7rb+ terjual"means at least 7,000.nullmeans the seller shows no label, not zero.shopTieris the only official-status marker: 1 = Tokopedia Mall, 2 = Power Merchant, 3 = regular seller. Thebadge.urlfilename is not a marker —official_store/badge_os.pngalso shows up for tier 3 sellers, so an "Official" column is deliberately absent.ratingcan benullfor products with no reviews, and it can be a tiered string (5.0,4.9,4.8) — that is the real value, not rounding.imageUrlis signed and carriesx-expires. Don't store it as an asset.oldIDis the dedup key, noturl, because the URL carries campaign parameters that change every session.
Pagination and server limits
maxItems is a total, not per page. The server silently truncates to 200 rows per request; asking for 300 or 500 still returns 200. Offset moves by start = (page - 1) * rows — not by page. Duplicates across windows are real (the search index shifts between two requests), so dedup keeps running.
Responses are sometimes empty without an error (HTTP 200, products: []), so every page is retried before it's considered finished. If the next page adds no new products, the scraper stops on its own — further requests would just waste quota.
The pause between requests is 1.5–3 seconds with jitter, and exponential backoff for 429/503. All of that lives in tokopedia_core.TokopediaSession, not in the actor.
Running locally
mkdir -p .tmp-actor-run/storage/key_value_stores/default# write input to .tmp-actor-run/storage/key_value_stores/default/INPUT.jsonCRAWLEE_STORAGE_DIR=.tmp-actor-run/storage \PYTHONPATH="actors/tokopedia-search;packages/tokopedia-core/src" \.venv/Scripts/python -m src
The actor's build context is the repo root (dockerContextDir: "../../.."), which is why packages/tokopedia-core must be copied along. Don't change it to COPY . ..
Tests
$pytest packages/tokopedia-core/tests -q
Parsing regressions use raw payloads in data/scraped/_debug/tokopedia-*.json as fixtures, so no internet connection is needed.