Tokopedia Scraper — Products & Suppliers, One for All avatar

Tokopedia Scraper — Products & Suppliers, One for All

Pricing

from $5.00 / 1,000 results

Go to Apify Store
Tokopedia Scraper — Products & Suppliers, One for All

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

Muhammad Sajid Izzulhaq

Maintained by Community

Actor 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

InputTypeDefaultDescription
searchStringslist 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.
maxItemsinteger100Limit 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.
orderByenum"23""23" = relevance (Tokopedia default), "5" = highest rating.
proxyproxy—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.
minPriceinteger—Drop products priced below this number (Rupiah). Products without a price are dropped too. Empty = no filtering.
maxPriceinteger—Drop products priced above this number (Rupiah). Products without a price are dropped too. Empty = no filtering.
minRatingnumber—Drop products rated below this number (0-5). Products without reviews are dropped too. Empty = no filtering.
debugDumpbooleanfalseSave 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:

  1. The proxy input 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.
  2. SCRAPER_PROXY — an environment variable holding your own rented proxy in http://user:pass@host:port format. Used when the Proxy component is empty.
  3. Apify residential proxy — used automatically when both are empty and the runtime provides APIFY_PROXY_PASSWORD. Built with group RESIDENTIAL and country-ID so 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) and Toko (13 columns, aggregated) plus a Ringkasan sheet. Download it through the Storage button. The spreadsheet shows a subset of the 19 dataset columns; the rest (oldID, long url, 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

  • price has no divisor. price.number is identical to the number shown on the page (98254 = Rp98,254). There is no /100 or /1000 anywhere in the codebase.
  • discountPercent is computed from the promo label, not from discountPercentage in the response — that field measured 0 on 100/100 products. When the ri_ribbon label is absent, the value is null, not 0.
  • countSold can be null. "250+ terjual" means at least 250, and "7rb+ terjual" means at least 7,000. null means the seller shows no label, not zero.
  • shopTier is the only official-status marker: 1 = Tokopedia Mall, 2 = Power Merchant, 3 = regular seller. The badge.url filename is not a marker — official_store/badge_os.png also shows up for tier 3 sellers, so an "Official" column is deliberately absent.
  • rating can be null for products with no reviews, and it can be a tiered string (5.0, 4.9, 4.8) — that is the real value, not rounding.
  • imageUrl is signed and carries x-expires. Don't store it as an asset.
  • oldID is the dedup key, not url, 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.json
CRAWLEE_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.