JD.com Product Search
Purpose
Given a keyword (and optional brand / price-range / sort filters), return a structured list of matching products from JD.com (京东 / Jingdong, China's largest electronics retailer): product title, current price in CNY (¥), JD-self-operated flag (自营), star rating, review count, stock status, and the canonical https://item.jd.com/<sku>.html URL. Read-only — never adds to cart, never checks out, never edits an account.
When to Use
- Price-monitoring an iPhone / appliance / electronics SKU on JD's primary Chinese-domestic catalog.
- Comparing JD-self-operated (
京东自营) vs. third-party-seller listings for the same keyword. - Bulk extraction of search results across multiple sort orders (sales-descending, price-ascending) for category research.
- Anywhere the user explicitly asks for jd.com product data and a Joybuy / global.jd.com substitution would be wrong (different SKU catalog, China-export-filtered inventory, separate pricing).
Workflow
Read this section in full before touching a session — JD.com has a hard anti-bot wall for foreign IPs that the rest of this section is built around. Skipping the prerequisites guarantees a login redirect with zero useful data.
Prerequisite — one of these MUST be true, or the skill cannot return data
JD.com fingerprints the request IP and forces every search.jd.com, list.jd.com, item.jd.com, so.m.jd.com, wq.jd.com, and search.jd.hk request from a non-China IP through https://cfe.m.jd.com/privatedomain/risk_handler/03101900/, which immediately issues a 302 to https://passport.jd.com/new/login.aspx. There is no JS challenge to wait out; the redirect fires instantly. Verified 2026-05-20 across us-west-2 and ap-southeast-1 Browserbase regions, with and without --verified --proxies, and with proxies: [{type: "browserbase", geolocation: {country: "CN"}}] (which silently fell back to a US Oregon AWS IP — Browserbase's residential proxy pool does not include China exit nodes).
You need exactly one of:
- A persisted JD authenticated context — a Browserbase Context (
--context-id <id> --persist) that already holds the cookies for a logged-in JD account (pin,thor,pt_key,pt_pin,unick,flash). The QR code on the login page binds to the JD mobile app, so the human bootstrap is: scan the QR in--debug-urlonce, save the context, reuse it for every future run. This is the only path the host marketplace currently exposes for JD skills. - A China-resident residential or datacenter proxy wired through
proxies: [{type: "external", server: "<host:port>", username: "<u>", password: "<p>"}]in the session-create body. This bypasses both the foreign-IP wall and the login requirement for unauthenticated browsing of search/category/item pages. Browserbase's built-in proxy does NOT count — confirmed-tested CN-geolocation hint via the body returned a US AWS IP.
If neither is available, stop and emit {"success": false, "reason": "auth_required"} — do not navigate search.jd.com and pretend to parse a result; the page is the login form and any "extracted" data is hallucinated.
1. Stealth + proxy session
SID=$(browse cloud sessions create --keep-alive --proxies --verified \
--context-id "$JD_CONTEXT_ID" --persist \
--region us-west-2 \
| node -pe "JSON.parse(require('fs').readFileSync(0,'utf8')).id")
export BROWSE_SESSION="$SID"
--verified (humanlike fingerprint) + --proxies is mandatory even with a logged-in context — JD also fingerprints UA/canvas/WebGL and a bare Chromium gets booted on first navigation. If using an external CN proxy, swap --proxies for --body '{"proxies":[{"type":"external","server":"...","username":"...","password":"..."}]}'.
2. Warm the session on the consumer homepage
browse open "https://www.jd.com/?gtm_test=us" --remote
browse wait load --remote
browse wait timeout 2000 --remote
The ?gtm_test=us query string is required from a non-China IP. A bare https://www.jd.com/ 302-redirects to corporate.jd.com (the English investor-relations site, a completely different page with zero products). The gtm_test=us parameter keeps the routing on the Chinese consumer site (京东(JD.COM)-正品低价、品质保障...). Verified — this trick still gives the consumer page even on a fresh foreign-IP session before any cookies are set.
After this load, the session has the cookies JD's edge expects: __jdv, __jdu, areaId, ipLoc-djd, 3AB9D23F7A4B3C9B, wlfstk_smdl, shshshfpa, shshshfpb, sdtoken. These alone are NOT sufficient to bypass the search-page wall — they only get you past the homepage-level edge check. The login cookies from step 0's context are what actually open search.jd.com.
3. Build the search URL
https://search.jd.com/Search
?keyword=<URL-encoded-keyword>
&enc=utf-8
&psort=<sort-code> # see table below; omit for relevance (default)
&ev=<filter-expr> # see filter syntax below; omit for unfiltered
&page=<2*pageNumber - 1> # JD uses odd-numbered pages (1, 3, 5, ...)
&s=<startIndex> # 1 for page 1, 31 for page 2, 61 for page 3
psort (sort order) — JD's psort codes (verified against the desktop search UI):
| psort | Sort |
|---|---|
omitted or 0 | Comprehensive / relevance (综合) — JD's default ranking |
3 | Sales descending (销量) |
4 | Price ascending (价格升序) |
2 | Price descending (价格降序) |
5 | Reviews / rating descending (评论数) |
6 | Newest (新品) |
ev (filter expression) — multiple filters are concatenated with %5E (URL-encoded ^), and each filter has the shape <facet>_<value>:
- Price range:
exprice_<min>-<max>(e.g.exprice_3000-5000). Use0for an open lower bound. - Brand:
exbrand_<brandName>—brandNameis JD's canonical brand string in Chinese OR English, depending on the brand. Apple isApple/苹果; Xiaomi isXiaomi/小米; Huawei isHUAWEI/华为. Brand names with spaces must be URL-encoded. Multiple brands: chain with another^exbrand_. - JD-self-operated only:
4_4(the facet4, value4— verified by clicking the "仅看京东自营" toggle).
Example with iphone keyword, sort by sales, Apple brand, ¥3000–5000:
https://search.jd.com/Search?keyword=iphone&enc=utf-8&psort=3&ev=exbrand_Apple%5Eexprice_3000-5000&page=1&s=1
4. Navigate and wait for the grid
browse open "$SEARCH_URL" --remote
browse wait load --remote
browse wait timeout 3000 --remote # JD lazy-loads price/stock via XHR after DOMContentLoaded
Verify you actually landed on the search page, not the login wall:
URL_AFTER=$(browse get url --remote)
case "$URL_AFTER" in
*passport.jd.com*|*cfe.m.jd.com/privatedomain/risk_handler*)
echo "AUTH_REQUIRED: context cookies expired or proxy IP flagged" >&2
exit 2
;;
esac
If you see passport.jd.com/new/login.aspx — the context cookies expired (JD's thor token has a ~14d TTL); a human needs to re-QR-scan to refresh the context, and this skill should return {"success": false, "reason": "auth_required"}.
5. Extract the product grid
The search results live under #J_goodsList ul.gl-warp.clearfix > li.gl-item, each with a data-sku attribute on the <li> carrying the SKU id. Per-item extraction (DOM selectors — run via browse eval):
Array.from(document.querySelectorAll('#J_goodsList li.gl-item')).map(li => ({
sku: li.getAttribute('data-sku'),
url: `https://item.jd.com/${li.getAttribute('data-sku')}.html`,
title: li.querySelector('.p-name em')?.innerText.trim()
?? li.querySelector('.p-name')?.innerText.trim(),
price_cny: parseFloat(li.querySelector('.p-price i')?.innerText) || null,
self_operated: !!li.querySelector('.p-icons .goods-icons.J-picon-tips[data-tips*="自营"]')
|| !!li.querySelector('.p-icons em:has-text("自营")'),
review_count: (() => {
const txt = li.querySelector('.p-commit strong a')?.innerText || '';
const m = txt.match(/^([\d.]+)([万+]?)/);
if (!m) return null;
const n = parseFloat(m[1]);
return m[2].includes('万') ? Math.round(n * 10000) : n;
})(),
shop: li.querySelector('.p-shop a')?.innerText.trim(),
stock_status: li.querySelector('.p-stock')?.innerText.trim() || 'in_stock'
}));
Rating is not in the search grid. JD only shows review count and a percentage-good label ("99%好评率") on the grid; the numeric star rating (1.0–5.0) is exposed only on the item-detail page (item.jd.com/<sku>.html → #summary-comment .comment-percent). If the caller wants a rating, do a follow-up fetch per SKU (expensive — N+1) or return the percentage-good as a proxy.
6. Paginate
JD uses an odd-numbered page parameter (1, 3, 5, ... not 1, 2, 3, ...) because each "page" is two halves of a 60-item AJAX-paginated unit. For page N of results: page=2*N-1, s=30*(N-1)+1. Total result count is in #J_resCount on first load.
7. Release the session
browse cloud sessions update "$SID" --status REQUEST_RELEASE
Site-Specific Gotchas
- Foreign IPs are hard-walled. Every product-browsing subdomain (
search.jd.com,list.jd.com,item.jd.com,so.m.jd.com,wq.jd.com,search.jd.hk) issues a 302 tohttps://cfe.m.jd.com/privatedomain/risk_handler/03101900/?returnurl=...&evapi=hold_search_phpwhich immediately bounces tohttps://passport.jd.com/new/login.aspxfrom any non-China IP, with or without proxies/stealth. Verified acrossus-west-2andap-southeast-1regions. Without authenticated cookies or a real CN-IP, the skill is non-functional. Do not "guess" at output shapes. https://www.jd.com/redirects tocorporate.jd.comfrom non-China IPs — use?gtm_test=usto land on the Chinese consumer homepage. This is the ONE page that renders without authentication. It's useful for warming cookies and visually confirming the IP is not banned outright, but the search box on it still POSTs tosearch.jd.comand bounces to login on submit.- Browserbase's built-in proxy is NOT a China IP.
proxies: [{type: "browserbase", geolocation: {country: "CN"}}]silently routes to a US AWS Oregon IP (verified52.41.230.44→ ipinfo: Boardman, Oregon, US). To get a real CN exit node, you need an external proxy viaproxies: [{type: "external", server: "...", ...}]. - The mobile JSON API
https://api.m.jd.com/?functionId=...requires a signedbodyparameter (uuid,bodySHA-256 sig,client,clientVersion,area,eid,fp,appid=jd_app_android). Unsigned calls return{"code":"1","echo":"no access"}. Recovering the sign function requires reverse-engineering JD'st1algorithm from the mobile app — out of scope for a marketplace skill. Don't waste time onapi.m.jd.com. - The AJAX scroll endpoint
https://search.jd.com/s_new.php?keyword=...&page=2&s=26&scrolling=yreturns a polite Chinese error from foreign IPs:{"code":"0","message":"success","body":{"errorCode":"601","errorReason":"大促异常火爆,已优先为您接入快速通道,稍安勿躁,请返回上一页重新尝试下~~~~"}}. Translation: "promotion-traffic excuse → please go back". It's anti-bot, not actual traffic. Don't retry — it won't clear. global.jd.comandjoybuy.co.ukare different catalogs.global.jd.com(京东全球版) and Joybuy (jd.com's international shopping arm, redirects tojoybuy.co.ukfor US IPs) carry a heavily-curated subset of the Chinese SKU catalog filtered for cross-border export, with different prices and SKU IDs. If the user asked forjd.com, do NOT silently fall back to these — returnauth_requiredinstead. The skill marketplace has separate listings for global.jd.com if/when that's the target.pageis odd-numbered (1, 3, 5, ...),sis the start-index (1, 31, 61, ...). A naivepage=2returns the same 30 items aspage=1— confusing but consistent with how JD's AJAX paginator increments.- Rating (1.0–5.0 stars) is not in the search grid. Only the review count (
.p-commit) and a percentage-good label (99%好评率) appear. The numeric rating requires a per-SKU fetch ofitem.jd.com/<sku>.html. If the caller asked for rating, either do the N+1 fetches (each requires the same auth wall) or return the percentage-good as a 0–100 proxy. - Self-operated flag. The "京东自营" badge is on
<li>via.p-icons .goods-icons[data-tips*="自营"]and/or a textual<em>自营</em>inside.p-icons. Some JD-self-operated listings still ship from a partner warehouse — the自营badge is the canonical signal regardless. Third-party shops show.p-shop atext linking to amall.jd.com/index-<merchantId>.htmlURL. - Prices update lazily via XHR after DOMContentLoaded. The initial HTML often shows
¥0.00placeholders; wait 2–3s afterloadbefore reading.p-price i, or some prices will be missing. Pre-sale (预售) and out-of-stock (无货) items may legitimately show no price — preservenull, don't fabricate. - Brand-name canonicalization.
ev=exbrand_<brand>requires JD's exact canonical brand string. For mixed-language brands, the canonical is usually the English (Apple,Xiaomi,HUAWEI,Samsung) but some are Chinese-only (格力,美的,海尔). Verify by clicking a brand checkbox in the UI once and reading the resulting URLevsegment. Wrong brand string → no filter applied, results returned silently as if unfiltered. - Captcha / "滑动验证" (slide-puzzle). Even with valid cookies, JD periodically interposes a slide-to-verify captcha (
https://iv.jd.com/...). Browserbase--verifieddoes NOT auto-solve these; the only options are (a)--solve-captchas(additional cost), (b) human intervention via--debug-url, or (c) waiting ~5 min and retrying with a new session. Capture the captcha screenshot and surface it as{"success": false, "reason": "captcha_required"}. - Read-only. Never click
.btn-addtocart, never navigate tocart.jd.com, never submitpassport.jd.comcredentials programmatically. Stop at the search-result extraction.
Expected Output
Three distinct outcome shapes:
// Success — results returned
{
"success": true,
"keyword": "iphone",
"sort": "sales_desc",
"filters": { "brand": "Apple", "price_min": 3000, "price_max": 5000, "self_operated_only": false },
"total_results": 8742,
"page": 1,
"products": [
{
"sku": "100012043978",
"title": "Apple iPhone 15 (A3092) 128GB 黑色 支持移动联通电信5G 双卡双待手机",
"price_cny": 4499.00,
"url": "https://item.jd.com/100012043978.html",
"self_operated": true,
"shop": "Apple产品京东自营旗舰店",
"review_count": 1200000,
"good_review_percent": 99,
"rating": null,
"stock_status": "in_stock"
}
]
}
// Authentication required — context expired or no CN-IP available
{
"success": false,
"reason": "auth_required",
"redirected_to": "https://passport.jd.com/new/login.aspx?ReturnUrl=...",
"hint": "Refresh the Browserbase Context by QR-scanning login at JD app, or supply a China-resident external proxy."
}
// Captcha interposed mid-session
{
"success": false,
"reason": "captcha_required",
"captcha_type": "slide_puzzle",
"captcha_url": "https://iv.jd.com/...",
"hint": "Use --solve-captchas or human-via-debug-url; current session has cookies that may still be valid after solve."
}
Marketplace status. This skill is shipped as a candidate — it documents the optimal honest path but cannot be validated end-to-end from Browserbase's default infrastructure (no China exit node, no shared JD credentials). Functional validation requires either (a) a tenant-supplied authenticated JD context, or (b) a tenant-supplied CN-residential proxy. Iteration log: 1 iter, 2026-05-20, converged on documenting the auth wall after exhausting search/list/item/wq/m-mobile/hk/global/api.m subdomains and Browserbase's CN-geolocation-hinted proxy (all confirmed-blocked from foreign IPs).