Overview
You send a Shopee product (shop_id, item_id, region) and get a job_id back immediately. Our infrastructure collects the product in parallel with other jobs, and you poll the job until it is completed. The finished job carries the product data under raw.
Asynchronous by design
No request holds a connection open while Shopee is scraped, so throughput grows with the number of jobs you send in parallel.
Typical completion time
Measured median from submit to result: 10–20 s for Thailand, Indonesia and the Philippines; 25–50 s for Singapore, Vietnam, Malaysia and Brazil. Taiwan usually finishes within 1–3 minutes.
Charged only for answers
A job is charged only when it returns a definitive answer within its TTL. Our own failures and expired jobs are never charged.
Anti-bot handled
Sessions, proxies and blocking are managed on our side. You call a plain REST API with one key.
Quickstart
POST the product to /jobs. The response returns a job_id straight away.
GET /jobs/{job_id} every 3–5 seconds while status is pending or processing.
When status is completed, the product data is under raw. result_status tells you whether the product is live, out of stock or unavailable.
# 1) submit curl -s -X POST https://shopee-multi-region.fastscraping.com/jobs \ -H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" \ -d '{"shop_id":83276818,"item_id":23743605234,"region":"th","ttl":600}' # 2) poll with the job_id from step 1 curl -s https://shopee-multi-region.fastscraping.com/jobs/JOB_ID -H "X-API-Key: YOUR_API_KEY"
import time, requests BASE = "https://shopee-multi-region.fastscraping.com" HEAD = {"X-API-Key": "YOUR_API_KEY"} def fetch_product(shop_id, item_id, region, ttl=600): job = requests.post(f"{BASE}/jobs", headers=HEAD, timeout=30, json={ "shop_id": shop_id, "item_id": item_id, "region": region, "ttl": ttl}).json() deadline = time.time() + ttl + 30 # TTL plus the 20 s delivery grace while time.time() < deadline: r = requests.get(f"{BASE}/jobs/{job['job_id']}", headers=HEAD, timeout=30).json() if r["status"] not in ("pending", "processing"): return r # completed / failed / expired / not_found time.sleep(4) return None r = fetch_product(83276818, 23743605234, "th") print(r["result_status"], list(r.get("raw", {})))
const BASE = "https://shopee-multi-region.fastscraping.com"; const HEAD = { "X-API-Key": "YOUR_API_KEY", "Content-Type": "application/json" }; const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); async function fetchProduct(shopId, itemId, region, ttl = 600) { const job = await (await fetch(`${BASE}/jobs`, { method: "POST", headers: HEAD, body: JSON.stringify({ shop_id: shopId, item_id: itemId, region, ttl }), })).json(); const deadline = Date.now() + (ttl + 30) * 1000; // TTL plus the 20 s delivery grace while (Date.now() < deadline) { const r = await (await fetch(`${BASE}/jobs/${job.job_id}`, { headers: HEAD })).json(); if (!["pending", "processing"].includes(r.status)) return r; await sleep(4000); } return null; }
Authentication
Every request needs your API key in the X-API-Key header. Authorization: Bearer YOUR_API_KEY is accepted as well.
X-API-Key: YOUR_API_KEY
A missing key returns 401 Missing API key; an unknown or disabled key returns 401 Invalid or inactive API key. Jobs are private to the key that created them.
Regions
Pass one of these codes as region. The blocks inside raw depend on the market; see Response data.
| Code | Market | Currency | Blocks in raw |
|---|---|---|---|
tw | Taiwan | TWD | get_pc — full product page payload |
br | Brazil | BRL | get_pc — full product page payload |
th | Thailand | THB | basic_pdp · get_pc · shop |
id | Indonesia | IDR | basic_pdp · get_pc · shop |
my | Malaysia | MYR | basic_pdp · get_pc · shop |
ph | Philippines | PHP | basic_pdp · get_pc · shop |
vn | Vietnam | VND | basic_pdp · get_pc · shop |
sg | Singapore | SGD | basic_pdp · get_pc · shop |
7386000 in Brazil is R$ 73.86; 999000000 in Thailand is ฿ 9,990.Submit a job
Creates one scrape job for one product and returns immediately, normally with status: "pending". Each call creates a new, separately charged job and runs a fresh scrape. One exception: a product we found delisted in the last few hours is answered at once as product_not_available, and the POST response then already shows status: "completed".
Request body
| Field | Type | Description |
|---|---|---|
shop_idrequired | integer | Shopee shop ID, the first number in shopee.xx/product/{shop_id}/{item_id}. |
item_idrequired | integer | Shopee item ID. |
regionrequired | string | Lowercase market code: tw, br, th, id, my, ph, vn or sg. Always send it: a request without region is not rejected but runs as Thailand (th). Upper-case codes such as "TH" return 400. |
ttloptional | integer | Seconds the job has to finish. Range 300–3600 (5 minutes to 1 hour): lower values become 300 and higher values become 3600. Omitted or 0 means the default, 3600. The effective value is echoed in the response note. See TTL & billing. |
all_voucheroptional | boolean or string | Taiwan. true, or one of "yes", "true", "1", "y", "on", also fetches the shop's vouchers into raw.get_pc.data.promotion_drawer.shop_vouchers. Other regions accept the flag without an error, but vouchers are not guaranteed there. |
POST /jobs
Content-Type: application/json
X-API-Key: YOUR_API_KEY
{
"shop_id": 1665239494,
"item_id": 58256703066,
"region": "br",
"ttl": 600
}
Response · 200
{
"job_id": "8aabc85bf7724eceaec27e5f375ad905",
"status": "pending",
"shop_id": 1665239494,
"item_id": 58256703066,
"note": "Poll GET /jobs/8aabc85bf7724eceaec27e5f375ad905 for your result. This job expires in 600s if not fetched in time."
}Possible errors: 400 unsupported region, 401 key problem, 422 a body that does not match the schema (for example a missing or non-integer shop_id), 429 rate, concurrency or quota limit. See Errors.
Get a job
Returns the job's state. While it runs you get the envelope without data; once it completes, the product data is added under raw.
Response fields
| Field | Type | Description |
|---|---|---|
job_id | string | The job you asked for. |
status | string | pending, processing, completed, failed, expired or not_found. See Job lifecycle. |
shop_id, item_id | integer | The product you submitted. |
result_status | string · null | What we found: success, out_of_stock, item_unavailable, product_not_available, shop_unavailable, failed, expired or not_found. null while the job runs. |
billable | boolean · null | Whether this job is charged. null while the job runs. |
retriable | boolean | true when submitting the same product again is worthwhile: the job was not charged, or (rarely) a completed job whose data failed our integrity check and was withheld. |
note | string · null | Free-text context, such as an internal retry, an expiry reason, or a notice that the data is no longer available. Informational only; it can be set in any status, so do not branch on it. |
voucher_source | string · null | For all_voucher jobs: live, cache or negative_cache (the shop has no vouchers). |
raw | object | The product data. Present on completed jobs while the data is retained (see TTL & billing); absent once it has aged out or when our integrity check withheld it, and can be empty for product_not_available. See Response data. |
Completed jobs can also carry diagnostic fields such as http_status; they are safe to ignore.
{
"job_id": "8aabc85bf7724eceaec27e5f375ad905",
"status": "completed",
"shop_id": 1665239494,
"item_id": 58256703066,
"result_status": "success",
"billable": true,
"retriable": false,
"note": "",
"voucher_source": null,
"raw": {
"get_pc": {
"bff_meta": null, "error": 0, "error_msg": null,
"data": {
"item": {
"item_id": 58256703066, "shop_id": 1665239494,
"title": "Kit Mel Cola 1k Óleo Nutritivo Mel Natural 60 ml Trihair",
"currency": "BRL",
"price": 7386000, "price_before_discount": 7774000,
"stock": 409, "total_stock": 1213,
"historical_sold": 2609,
"tier_variations": [ { "name": "Quantidade", "options": ["Kit Mel Cola 1kg + Óleo 60ml"], … } ],
"models": [ { "model_id": 239439288108, "name": "Kit Mel Cola 1kg + Óleo 60ml", "stock": 1213, … } ],
…
},
"product_price": {
"price": { "single_value": 7386000, "range_min": -1, "range_max": -1 },
"price_before_discount": { "single_value": 7774000, … },
"discount": 5, …
},
"price_breakdown": {
"discount_breakdown": [ { "price_source": "Product Discount", "discount_amount": 388000, … } ], …
},
"product_review": {
"rating_star": 4.983, "rating_count": [1239, 1, 0, 5, 7, 1226],
"historical_sold": 2609, "liked_count": 2494, …
},
"shop_detailed": { "shopid": 1665239494, "name": "Cades Cosméticos - Loja Oficial", "rating_star": 4.886, … },
"product_images": { … }, "product_attributes": { … },
"product_description": { … }, "promotion_info": { … }
}
}
}
}
{
"job_id": "8aabc85bf7724eceaec27e5f375ad905",
"status": "pending",
"shop_id": 1665239494,
"item_id": 58256703066,
"result_status": null,
"billable": null,
"retriable": false,
"note": ""
}
{
"job_id": "3f1e0a9b27c84d0e9a5b1f6c2d7e8a90",
"status": "failed",
"shop_id": 1665239494,
"item_id": 58256703066,
"result_status": "shop_unavailable",
"billable": false,
"retriable": true,
"note": ""
}
Possible errors: 404 job not found, 403 not your job (the job belongs to another key), 401 key problem.
Latest result for a product
Returns the latest cached data for a product without a job_id. It works only for products your key already has a completed job for, and it reads the same product cache as GET /jobs/{job_id}: data is kept for 2 hours after the most recent completed scrape of that product. It is not charged.
Query parameters
| Parameter | Type | Description |
|---|---|---|
shop_idrequired | integer | Shopee shop ID. |
item_idrequired | integer | Shopee item ID. |
regionrequired | string | Lowercase market code, the same one you used when you submitted the job. If omitted, Thailand (th) is assumed. |
Response fields
| Field | Type | Description |
|---|---|---|
shop_id, item_id | integer | The product. |
status | string | The result for this product: success, out_of_stock, item_unavailable or product_not_available (the same values as result_status on a job). |
voucher_source | string · null | For all_voucher results: live, cache or negative_cache. |
raw | object | The product data, in the same format as a completed job. See Response data. |
curl -s "https://shopee-multi-region.fastscraping.com/result?shop_id=83276818&item_id=23743605234®ion=th" \ -H "X-API-Key: YOUR_API_KEY" { "shop_id": 83276818, "item_id": 23743605234, "status": "success", "voucher_source": null, "raw": { "basic_pdp": { … }, "get_pc": { … }, "shop": { … } } }
Errors: 404 no completed job for this product on your key when your key has not requested this product yet; 404 no result yet (pending/failed/expired) when the cached data has aged out (submit a new job); 401 key problem; 422 missing or non-integer shop_id/item_id. Completed results may also carry diagnostic fields such as http_status; they are safe to ignore.
Job lifecycle
A job normally moves from pending to processing and ends in one of four final states. It can go back from processing to pending while we retry it internally, and it can end without reaching processing (expired in the queue, or answered at once for a recently delisted product). Poll until status is no longer pending or processing.
status | result_status | Meaning | Charged | Retry |
|---|---|---|---|---|
completed | success | Live product; full data in raw. | yes | no |
completed | out_of_stock | The product exists but has no stock. Data is still returned. | yes | no |
completed | item_unavailable | The listing exists but cannot be bought right now. | yes | no |
completed | product_not_available | Delisted, banned or deleted on Shopee. raw can be partial or empty. | yes | no |
failed | shop_unavailable, failed | We could not complete the scrape. | no | yes |
expired | expired | The job did not finish in time: its TTL ran out, or it could not be processed after repeated internal retries. | no | yes |
not_found | not_found | Shopee does not recognise this shop and item pair. | no | no |
Use the billable and retriable flags in your code rather than hard-coding this table; they are computed per job and always reflect the final decision.
Response data
Where to find the common data points in raw. Field names follow Shopee's format. A value that is not available for a product comes back as 0, an empty string or null; treat those as not available. Numbers inside basic_pdp can arrive as JSON strings.
Taiwan and Brazil · raw.get_pc.data
| Data point | Path |
|---|---|
| Title | item.title |
| Current price | product_price.price.single_value, also item.price |
| Price before discount | product_price.price_before_discount.single_value |
| Discount % | product_price.discount |
| Discount breakdown | price_breakdown.discount_breakdown[] |
| Stock | item.total_stock (sum across all variants). item.stock can be a smaller promotion allocation. |
| Variants and SKUs | item.models[] (model_id, name, stock), item.tier_variations[]. models[].price is not always the price a buyer pays; use product_price for the current price. |
| Sold count | product_review.historical_sold, product_review.global_sold |
| Rating | product_review.rating_star; product_review.rating_count = [total, 1★, 2★, 3★, 4★, 5★] |
| Likes | product_review.liked_count |
| Images and video | product_images |
| Categories and attributes | item.categories[] (category IDs; display_name is empty), product_attributes.attrs (can be empty) |
| Description | product_description.paragraph_list |
| Shop | shop_detailed (shopid, name, rating_star, is_official_shop, is_shopee_verified). Counters such as follower_count and item_count are not filled in this block. |
Shop vouchers (Taiwan, all_voucher) | promotion_drawer.shop_vouchers[] (voucher_code, min_spend, start_time, end_time …) |
Thailand, Indonesia, Malaysia, Philippines, Vietnam, Singapore
| Data point | Path |
|---|---|
| Title | raw.basic_pdp.name |
| Current price | raw.get_pc.data.item.price (range: price_min, price_max), including running promotions. raw.basic_pdp.price_min/price_max leave promotions out and can be higher. |
| Price before discount | raw.get_pc.data.item.price_before_discount; also raw.basic_pdp.normal_price_min, normal_price_max |
| Discount % | raw.get_pc.data.item.show_discount |
| Stock | raw.basic_pdp.total_stock (a string, e.g. "311") |
| Variants and SKUs | raw.basic_pdp.models[] (model_id, name, price, normal_price, stock, sold), tier_variations[] |
| Sold count | raw.basic_pdp.sold |
| Rating | raw.basic_pdp.rating.rating_star; rating.rating_counts = [0, 1★, 2★, 3★, 4★, 5★] (index 0 is not the total) |
| Likes and comments | raw.basic_pdp.liked_count, cmt_count |
| Images and description | raw.basic_pdp.images (Shopee image IDs). Plain-text description: raw.get_pc.data.item.description; raw.basic_pdp.description is a JSON-encoded list of text and image segments. |
| Page item summary | raw.get_pc.data.item (title, categories, item_rating, tier_variations …) |
| Shop | raw.shop (name, rating_star, follower_count, response_rate, shop_location, item_count …) |
{
"basic_pdp": {
"item_id": "23743605234", "shop_id": "83276818", "currency": "THB",
"price_min": "1089000000", "price_max": "1089000000",
"total_stock": "311", "sold": 4266,
"rating": { "rating_star": 4.71, "rating_counts": [0, 52, 20, 55, 119, 1477] },
"models": [ { "model_id": "186771150146", "price": "1089000000", "normal_price": "1749000000", "stock": 311, "sold": 4266, … } ],
…
},
"get_pc": { "data": { "item": { "price": 999000000, "price_before_discount": 1749000000, "show_discount": 43, … } } },
"shop": { "shopid": 83276818, "name": "samsung_thailand", "rating_star": 4.89, "follower_count": 1628625, "response_rate": 97, … }
}TTL & billing
- TTL is the time a job has to finish: 300 to 3600 seconds, default 3600.
- A job is charged once when it completes with
billable: truewithin its TTL or the 20-second grace after it. One job is one charge, whatever the size of the product data. - A job that has not completed by its TTL plus the 20-second grace becomes
expiredand is not charged. Keep polling until about TTL + 30 seconds before giving up. - Product data is kept for 2 hours after the most recent completed scrape of that product. It is stored per product, so if the same product is scraped again in that time, an earlier job returns the newer data under
raw. After that the envelope still shows the final status and billing, with a note that the data is no longer available. - Submitting the same product again creates a new job, charged separately (see Submit a job for the delisted-product exception).
Rate limits
Each API key has its own limits, checked when you submit a job (POST /jobs). Polling and the /me endpoints are not limited. When a limit is reached, POST /jobs returns 429 with a message naming it.
| Limit | What it counts | 429 message |
|---|---|---|
| Concurrency | Your jobs currently pending or processing | Concurrency limit reached (N in-flight jobs) |
| Per minute, hour, day | Charged results in the last minute, the last hour, or since 00:00 UTC | Rate limit exceeded (per-minute), (per-hour), (per-day) |
| Total quota | Charged results over the key's lifetime (trial keys) | Quota exceeded (lifetime) |
GET /me/usage always shows the limits on your key. The per-minute, per-hour, per-day and quota limits compare results already charged, so jobs you have in flight still run and are charged; the charged total can pass a limit by up to your in-flight jobs. Counters refresh every few seconds.
Usage
Your key's limits, remaining credits, job counts and charged usage per region. interval (1–365, default 30) sets how many days daily covers; values outside that range return 422. Day buckets in totals, daily and monthly_breakdown follow the server's reporting day (Central European Time).
In monthly_breakdown, a month with no usage is null and a day with no usage is 0 instead of an object. jobs.total also counts processing and expired jobs.
{
"owner": "your-company",
"timezone": "UTC",
"key": { "is_active": true, "created_at": "2026-09-25T10:20:20" },
"limits": { "per_minute": 120, "per_hour": 1200, "per_day": 1000, "concurrency": 100 },
"credits": { "total": 1000, "used": 37, "remaining": 963, "unlimited": false },
"jobs": { "total": 41, "completed": 37, "failed": 1, "pending": 3, "not_found": 0, "billable": 37 },
"totals": {
"today": { "all": 12, "th": 9, "tw": 3 },
"yesterday": { "all": 25, "br": 25 },
… day_before_yesterday, last_7_days, last_30_days, this_month, last_month, this_year, lifetime
},
"daily": [ { "date": "2026-09-25", "th": 9, "tw": 3, "all": 12 }, … ],
"monthly_breakdown": { "2026-09": { "24": { "th": 9, "all": 9 }, "25": 0, …, "total": 37 }, "2026-08": null }
}Job history
Your jobs, newest first. limit is 1–500 (default 50); use offset to page. Out-of-range values return 422. created_ts is a Unix timestamp in seconds.
{
"limit": 2, "offset": 0,
"items": [
{ "job_id": "a32d719e27744a1393722737e9209506", "region": "tw", "shop_id": 5597991, "item_id": 74412812,
"status": "completed", "result_status": "success", "billable": true, "created_ts": 1790228814.34 },
{ "job_id": "8daf6d5a0e834572b0a0221e0089bbe8", "region": "tw", "shop_id": 37004578, "item_id": 7775080780,
"status": "completed", "result_status": "success", "billable": true, "created_ts": 1790227317.52 }
]
}Health
Returns HTTP 200 with "status": "ok" while the API is up. Use it for uptime monitoring.
Errors
Errors return a JSON body with a detail field.
HTTP/1.1 429 Too Many Requests
{ "detail": "Concurrency limit reached (100 in-flight jobs)" }| Code | detail | What to do |
|---|---|---|
| 400 | Unsupported region 'xx'. Supported: [...] | Use one of the region codes. |
| 401 | Missing API key, Invalid or inactive API key | Send a valid key in X-API-Key. |
| 403 | not your job | Read the job with the key that created it. |
| 404 | job not found | Check the job_id. |
| 404 | no completed job for this product on your key | GET /result only serves products your key has a completed job for. Submit a job first. |
| 422 | Validation details (a list) | The request did not match the schema: a missing or non-integer shop_id/item_id, a non-integer ttl, a body that is not JSON, or a query parameter out of range on /me/usage or /me/jobs. |
| 429 | Rate, concurrency or quota message | Wait for in-flight jobs to finish or slow down. See Rate limits. |
Best practices
- Send jobs in parallel. The system is built for volume: 20–50 jobs at once per region finish sooner overall than one-by-one submission. Keep in-flight jobs under your concurrency limit.
- Poll every 3–5 seconds. Faster polling does not speed a job up.
- Choose a TTL that fits your use. A short TTL gives you a quick, uncharged
expiredwhen a job cannot finish in time; the 1-hour default favours completion. - Retry only when
retriableis true. Charged answers such asproduct_not_availableare final.