Exact per-listing sold counts for Shopee Vietnam. Submit a job with 300 to 1,000 products, collect Shopee's own item data when it is done: lifetime sold of the listing, Shopee's merged sold figure, 30-day sold, price, stock and rating. You pay only for the items that come back.
The API works with jobs. You send a list of Shopee products as shop_id + item_id pairs, 300 to 1,000 per job, and get a job_id back at once. Jobs are processed in order, in the background. You poll the job until its status is completed; the job then carries Shopee's own item card for each product found: one object per product, about 90 fields each, with Shopee's field names and values. Every job is completed within 5 minutes of submission. A completed 1,000-item job is about 6 MB of JSON, about 1 MB with gzip (Response size).
historical_sold is the exact number of units sold by that listing alone, not a rounded label such as "10k+" and not merged with other listings.
Submit 300 to 1,000 products in one call. The price is per item, so how you group your lists into jobs does not change the cost.
$3 per 1,000 items that come back. Products that are not found, failed items, errors, job submissions and status checks are free.
The job body is compatible with Shopee's item/get_list body: bff_meta and source are accepted and ignored.
Base URL: https://shopee-multi-region.fastscraping.com. Replace YOUR_API_KEY with your API key. Each sample reads 300 to 1,000 shop_id/item_id pairs from a file, submits a job, waits estimated_wait_s, polls every 20 seconds until the job is completed and prints the sold counts.
# job.json holds 300 to 1,000 distinct pairs: # {"region":"vn","shop_item_ids":[{"shop_id":88201679,"item_id":27041370670}, ...]} # 1. submit the job: the answer carries job_id and estimated_wait_s curl -s -X POST "https://shopee-multi-region.fastscraping.com/v1/get_list/jobs" \ -H "X-API-Key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d @job.json # 2. poll until "status" is "completed"; the items are then in the answer. # --compressed: about 1 MB on the wire instead of about 6 MB for 1,000 items curl -s --compressed "https://shopee-multi-region.fastscraping.com/v1/get_list/jobs/JOB_ID" \ -H "X-API-Key: YOUR_API_KEY" -o result.json
import csv import time import requests BASE = "https://shopee-multi-region.fastscraping.com" HEAD = {"X-API-Key": "YOUR_API_KEY"} # items.csv: one "shop_id,item_id" per line, 300 to 1,000 lines, e.g. 88201679,27041370670 with open("items.csv", newline="") as f: pairs = [{"shop_id": int(s), "item_id": int(i)} for s, i in csv.reader(f)] r = requests.post(f"{BASE}/v1/get_list/jobs", headers=HEAD, json={"region": "vn", "shop_item_ids": pairs}, timeout=30) r.raise_for_status() job_id = r.json()["job_id"] time.sleep(r.json()["estimated_wait_s"]) while True: # requests asks for gzip and decompresses it for you r = requests.get(f"{BASE}/v1/get_list/jobs/{job_id}", headers=HEAD, timeout=60) r.raise_for_status() job = r.json() if job["status"] == "completed": break time.sleep(20) for it in job["items"]: print(it["itemid"], it["historical_sold"], it["global_sold_count"], it["sold"]) print("billed items:", job["items_billed"], "missing:", job["missing_item_ids"])
// Node.js 18+ (built-in fetch). Save as get_list.mjs and run: node get_list.mjs import { readFileSync } from "node:fs"; const BASE = "https://shopee-multi-region.fastscraping.com"; const HEAD = { "X-API-Key": "YOUR_API_KEY", "Content-Type": "application/json" }; const sleep = (sec) => new Promise((r) => setTimeout(r, sec * 1000)); // items.csv: one "shop_id,item_id" per line, 300 to 1,000 lines const shop_item_ids = readFileSync("items.csv", "utf8").trim().split(/\r?\n/).map((line) => { const [shop_id, item_id] = line.split(",").map(Number); return { shop_id, item_id }; }); let res = await fetch(`${BASE}/v1/get_list/jobs`, { method: "POST", headers: HEAD, body: JSON.stringify({ region: "vn", shop_item_ids }), }); if (!res.ok) throw new Error(`${res.status} ${await res.text()}`); const { job_id, estimated_wait_s } = await res.json(); await sleep(estimated_wait_s); let job; for (;;) { // fetch asks for gzip and decompresses it for you res = await fetch(`${BASE}/v1/get_list/jobs/${job_id}`, { headers: HEAD, signal: AbortSignal.timeout(60000) }); if (!res.ok) throw new Error(`${res.status} ${await res.text()}`); job = await res.json(); if (job.status === "completed") break; await sleep(20); } for (const it of job.items) console.log(it.itemid, it.historical_sold, it.global_sold_count, it.sold); console.log("billed items:", job.items_billed, "missing:", job.missing_item_ids);
Every request needs your API key in the X-API-Key header (Authorization: Bearer YOUR_API_KEY works too).
X-API-Key: YOUR_API_KEY
A missing, unknown or disabled key returns 401. Jobs belong to the key that created them: another key gets 404 for them. Keep the key on your server; do not put it in client-side code.
Creates a job for 300 to 1,000 distinct products and answers at once with 202 and the job ID. Submitting is free.
| Field | Type | Description |
|---|---|---|
shop_item_idsrequired | array | 300 to 1,000 distinct shop_id/item_id pairs, each an object {"shop_id": …, "item_id": …}. Duplicate pairs are merged before counting. Fewer than 300 or more than 1,000 distinct pairs returns 400; group small lists until they reach 300, split bigger lists into several jobs. |
shop_item_ids[].shop_idrequired | integer | Shopee shop ID, the first number in shopee.vn/product/{shop_id}/{item_id}. Numeric strings are accepted. |
shop_item_ids[].item_idrequired | integer | Shopee item ID, the second number in the same URL. |
regionoptional | string | Lowercase market code. Default "vn" (Vietnam). Other Shopee markets are available on request; until a market is enabled, any other value returns 400. |
bff_meta, sourceoptional | any | Accepted and ignored, so a body written for Shopee's item/get_list call works unchanged. |
POST /v1/get_list/jobs
Content-Type: application/json
X-API-Key: YOUR_API_KEY
{
"bff_meta": null,
"shop_item_ids": [
{ "item_id": 27041370670, "shop_id": 88201679 },
{ "item_id": 42568221380, "shop_id": 308461157 },
… 300 to 1,000 distinct pairs in total …
],
"source": "microsite_individual_product"
}
HTTP/1.1 202 Accepted
Content-Type: application/json
{
"job_id": "8c2f0e7d4b1a4e6f9a3c5d7e9f1b2c4d",
"status": "queued",
"region": "vn",
"items_requested": 1000,
"duplicates_removed": 0,
"created_at": "2026-10-05T10:23:16.338+00:00",
"deadline_at": "2026-10-05T10:28:16.338+00:00",
"estimated_wait_s": 240
}| Field | Type | Meaning |
|---|---|---|
job_id | string | 32 hex characters. Use it to poll the job, and quote it when you contact support. |
status | string | Always "queued" here. See Job lifecycle. |
items_requested | integer | Distinct pairs in the job, after duplicates are removed. |
duplicates_removed | integer | How many duplicate pairs were dropped from your list. |
deadline_at | string | The job is completed at this time at the latest: 5 minutes after created_at. |
estimated_wait_s | integer | Rough estimate, in seconds, of when the job will be done, based on the work queued ahead of it (never more than 300). A guide for your first poll. |
Returns the job's status and counters. Once the status is completed, the answer also carries the items, in the order of your request, plus the IDs that did not come back. Polling is free.
| Parameter | Type | Description |
|---|---|---|
partialoptional | integer | 1 = include the items collected so far while the job is still queued or running. Default 0: items only once the job is completed. |
{
"job_id": "8c2f0e7d4b1a4e6f9a3c5d7e9f1b2c4d",
"status": "completed",
"region": "vn",
"created_at": "2026-10-05T10:23:16.338+00:00",
"started_at": "2026-10-05T10:23:21.540+00:00",
"completed_at": "2026-10-05T10:27:41.912+00:00",
"deadline_at": "2026-10-05T10:28:16.338+00:00",
"items_requested": 1000,
"items_done": 1000,
"items_pending": 0,
"items_returned": 997,
"items_missing": 3,
"items_failed": 0,
"items_billed": 997,
"missing_item_ids": ["23456789012", "24567890123", "25678901234"],
"failed_item_ids": [],
"items": [
{
"itemid": 27041370670,
"shopid": 88201679,
"name": "Điện thoại Apple iPhone 17 Pro Max 256GB",
"historical_sold": 28975,
"global_sold_count": 28975,
"sold": 3223,
"stock": 5450,
"price": 3499000000000,
"price_min": 3499000000000,
"price_max": 3559000000000,
"price_before_discount": 3799000000000,
"currency": "VND",
"item_rating": { "rating_star": 4.944232820356544, "rating_count": [6566, 55, 8, 29, 64, 6410], "...": "more fields" },
"liked_count": 11289,
"cmt_count": 6566,
"shop_location": "Tỉnh Bắc Ninh",
"cb_option": 0,
"ctime": 1757498693,
"catid": 100013,
"global_cat": { "catid": [100013, 100073], "l0_category": 2 },
"shop_rating": 4.922738,
"item_status": "normal",
"...": "more fields"
},
{
"itemid": 42568221380,
"shopid": 308461157,
"name": "Apple iPhone 17 Pro Max 256GB Chính hãng ZP/A",
"historical_sold": 38423,
"global_sold_count": 38424,
"sold": 18512,
"...": "more fields"
},
… 995 more items …
]
}While the job is queued or running, the answer has the same counters but no items, missing_item_ids or failed_item_ids (unless you pass partial=1). An unknown or expired job_id, or a job of another key, returns 404.
A completed job carries every item it found, about 6 KB of JSON per item, so its answer is large. Status checks while the job is queued or running are small (under 1 KB).
| Completed job | JSON | With gzip |
|---|---|---|
| 300 items | ≈ 1.9 MB | ≈ 0.3 MB |
| 1,000 items | ≈ 6.3 MB | ≈ 1.0 MB |
Accept-Encoding: gzip. Python requests and Node.js fetch do this and decompress for you; with curl, add --compressed.GET of a completed job sends all its items again. Save the answer when the status turns completed and stop polling that job.| Field | Type | Meaning |
|---|---|---|
status | string | queued, running or completed. See Job lifecycle. |
created_at, started_at, completed_at | string | ISO 8601 times in UTC. started_at and completed_at are null until they happen. |
deadline_at | string | 5 minutes after created_at. The job is completed by then at the latest. |
items_requested | integer | Distinct pairs in the job. |
items_done, items_pending | integer | Pairs with a final outcome, and pairs still waiting. items_done = items_returned + items_missing + items_failed. |
items_returned | integer | Products found; each one is an object in items. |
items_missing, missing_item_ids | integer, array | Products Shopee did not return: deleted, banned or a wrong shop_id/item_id pair. Not billed. |
items_failed, failed_item_ids | integer, array | Products we could not fetch before deadline_at, even after automatic retries. Not billed; submit them again in a new job. |
items_billed | integer | Items charged for this job. Always equal to items_returned. |
items | array | One object per product found, in the order of your request, with Shopee's field names and values. See Response fields. |
Your key's most recent jobs, newest first, with the same counters as Get a job and without items. limit is 1 to 100, default 20. Useful to find a job_id you lost.
{
"jobs": [
{ "job_id": "8c2f0e7d4b1a4e6f9a3c5d7e9f1b2c4d", "status": "completed", "items_requested": 1000, "items_returned": 997, "...": "more fields" }
]
}Billed items and the amount for your key, by period (UTC days and months), plus the items still pending in your unfinished jobs.
{
"timezone": "UTC",
"billing": "per returned item",
"price_per_1000_items_usd": 3.0,
"today": { "items": 1840, "amount_usd": 5.52 },
"yesterday": { "items": 2310, "amount_usd": 6.93 },
"this_month": { "items": 4150, "amount_usd": 12.45 },
"last_month": { "items": 0, "amount_usd": 0.0 },
"lifetime": { "items": 4150, "amount_usd": 12.45 },
"items_pending": 300
}| Status | Meaning |
|---|---|
queued | Accepted and waiting. No item has been processed yet. |
running | Some items are done, others are still waiting. Large jobs are processed in several parts. |
completed | Every item has a final outcome: returned, missing or failed. items is in the answer. Reached at deadline_at (5 minutes after submission) at the latest. |
running can last a while.deadline_at at the latest. Start polling after estimated_wait_s, then every 15–30 seconds.deadline_at are marked failed and are not billed. Submit them again in a new job.GET returns 404. Store the results on your side.Each object in items is Shopee's item card for one listing. These are the fields most clients use, plus a few bonus fields. Every object has about 90 fields; all of them keep Shopee's names and values.
| Field | Type | Meaning |
|---|---|---|
itemid, shopid | integer | The product and shop IDs. |
name | string | Listing title as shown on Shopee, in the local language. |
historical_sold | integer | Exact lifetime units sold by this listing. Counts this listing only. |
global_sold_count | integer | Shopee's merged sold figure. It can include sales of similar listings that Shopee groups with this one and, for cross-border items, sales in all markets. Never lower than historical_sold, and can be far higher. |
sold | integer | Units sold in the last 30 days. |
stock | integer | Units currently available for the listing, all variants together. |
price, price_min, price_max | integer | Current price, and the lowest and highest variant price. Shopee integers: divide by 100,000. 19800000000 = 198,000 VND; 3499000000000 = 34,990,000 VND. |
price_before_discount | integer | Price before discount, same ×100,000 scale. Read it together with price on discounted listings; for a listing without a discount, use price. |
currency | string | "VND" for Vietnam. |
item_rating.rating_star | number | Average rating, 0 to 5. item_rating.rating_count is [total, 1★, 2★, 3★, 4★, 5★]. |
liked_count | integer | Popularity counter Shopee shows on the product page. |
cmt_count | integer | Review count shown for the listing. |
shop_location | string | Seller's province or city, as Shopee displays it. |
cb_option | integer | 1 = cross-border listing (shipped from abroad), 0 = local seller. |
ctime | integer | Listing creation time, Unix seconds. |
catid | integer | Top-level category ID. |
shop_rating | number | The seller's shop rating, 0 to 5. |
item_status | string | Listing status as Shopee reports it, for example "normal" for a live listing. |
global_cat | object | bonusFull category path. global_cat.catid lists the category IDs from the top level down, for example [100013, 100073]; the first one equals catid. |
global_brand | object | bonusShopee brand ID of the listing, in global_brand.brand_id. |
is_on_flash_sale, flash_sale_stock, flash_sale_, flash_sale_infos | various | bonusFlash-sale info: Shopee's flash-sale flag, the flash-sale stock and sold counters, and, when the listing has flash-sale slots, their start and end times (Unix seconds) in flash_sale_infos. |
| other fields | various | For example shop_name, discount, images, tier_variations (variant names and options), is_official_shop. Shopee's names and values, unchanged. |
historical_sold or global_sold_count? For the sales of one specific listing, use historical_sold. The two are equal for most listings, but the merged figure can be many times larger: on one popular Vietnam listing we checked, historical_sold was 102,625 while global_sold_count was 11,829,433. The gap shows up on local and cross-border listings alike, so check every listing rather than relying on cb_option.
$3 / 1,000
items returned ($0.003 per item). A job of 1,000 products that all come back costs $3.00.
per item
Each product that comes back in a job is charged once, at the moment it is stored for that job. items_billed always equals items_returned.
$0
Missing and failed items, job submissions, status checks, job lists, usage checks, and every error (4xx and 5xx).
GET /v1/get_list/usage shows your billed items and the amount for today, yesterday, this month, last month and lifetime.region: "vn") today. Other Shopee markets are available on request.| Limit | Default | Notes |
|---|---|---|
| Items per job | 300–1,000 | Distinct shop_id/item_id pairs; duplicates are merged before counting. Fewer than 300 or more than 1,000 returns 400. |
| Job time limit | 5 minutes | Every job is completed by deadline_at. Items not fetched by then are failed and free. |
| Response size | ≈ 6 MB / 1,000 items | About 1 MB with gzip. See Response size. |
| Job submissions | 60 / minute per key | Above the limit you get 429. Group items into bigger jobs instead of sending many tiny ones. |
| Unfinished items | 20,000 per key | Items in your jobs that are not done yet. A new job that would go above it gets 429; submit it when earlier jobs finish. |
| Daily items | set per key | Billed items per UTC day, plus your pending items. Above it you get 429 with a detail starting Daily limit reached. Adjustable on request. |
| Trial quota | 1,000 | Items returned. When it is used up you get 429 with a detail starting Quota exceeded; retrying won't help, contact support. |
| Polling | every 15–30 s | Status checks are free; there is no benefit in polling faster. |
| Result retention | 3 days | After completion. Later, the job returns 404. |
Errors return a JSON body with a detail field. No error is billed. The detail text is meant for people and may change, so branch on the HTTP status code.
HTTP/1.1 400 Bad Request
{ "detail": "min 300 distinct shop_item_ids per job (got 120 after removing duplicates)" }| Code | Meaning | What to do |
|---|---|---|
| 400 | Invalid body, fewer than 300 or more than 1,000 distinct items, or unsupported region. | Fix the request. Retrying unchanged returns the same error. |
| 401 | Missing or invalid API key. | Send a valid key in X-API-Key. |
| 404 | Unknown or expired job_id, or a job created with another key. | Check the ID; list your jobs with GET /v1/get_list/jobs. |
| 429 | Too many job submissions, too many unfinished items, the queue is full, or a daily limit or quota is reached (detail says which). | Retry later with backoff, for example 30 s, 60 s, 120 s. For Quota exceeded, retrying won't help: contact support. |
| 500 | Internal error. | Retry. If it persists, contact support and quote the job_id. |
| 503 | Temporarily unavailable. | Retry after 30–60 seconds. |
requests and fetch handle it for you; with curl, add --compressed.estimated_wait_s, then poll every 15–30 seconds until the job is completed; it never runs past deadline_at. Use the counters to show progress.GET of a completed job sends all its items again. Jobs are kept for 3 days, then removed.missing_item_ids are usually deleted, banned or mistyped. Sending them again seldom changes the result and is free.failed_item_ids were not billed; put them in a new job.itemid/shopid from each object to be safe.job_id. It lets us trace any job you ask about.