JD.com API in Python: Build a Product Research Pipeline
Use the JD.com API in Python to search products, normalize details, estimate call costs, and add review evidence without trusting unstable upstream fields.

A product-search response looks ready for an agent until the next endpoint returns a completely different shape. That happened in my November 1 test of the new JD.com APIs: search exposed a compact products list, while product detail returned a deep mobile-commerce payload and a masked price string. The useful engineering task is therefore not “call JD.com.” It is to build a small normalization boundary that an agent can trust.
This guide is for developers building catalog research, price-watch, assortment, or buyer-feedback tools. It uses the five JD.com endpoints now listed on SandBase, a dated live search and detail call, defensive Python, and an explicit cost model. It does not treat one marketplace snapshot as inventory, demand, or product-quality proof.
Key takeaway (TL;DR)
- SandBase lists five JD.com endpoints released on October 29, 2026: search, detail, description images, shop products, and comments.
- Use
POST /v1/api/jd/...; completed calls place the business payload atoutputs[0].data.- Search cost $0.0208 in the live endpoint page; detail and comments cost $0.0278 each on November 1, 2026.
- Normalize IDs, titles, prices, shops, and provenance into your own schema. The live detail sample returned a masked price, so a missing numeric value must remain unknown.
- Reviews are evidence to inspect, not a score to blindly average. This test did not retain a completed response for the documentation sample item, so the comments payload remains unvalidated here.
What is available
The JD.com API catalog on SandBase exposes five synchronous POST endpoints:
| Endpoint | Purpose | Live page price on 2026-11-01 |
|---|---|---|
/v1/api/jd/search/items | Keyword search with page pagination | $0.0208 |
/v1/api/jd/item/detail | Product, price, promotion, brand, shop and images | $0.0278 |
/v1/api/jd/item/description-images | Description images and attributes | $0.0208 |
/v1/api/jd/shop/items | Page through a known shop catalog | $0.0278 |
/v1/api/jd/item/comments | Page through customer comments | $0.0278 |
The JD.com catalog showed five available endpoints on November 1, 2026. This screenshot supports endpoint coverage, not the freshness or completeness of any marketplace listing.
The provider layer and the SandBase layer are different. JD.com supplies the underlying public marketplace data. SandBase exposes each capability through one authentication and billing surface. The business payload remains the platform’s original structure, so SandBase’s common envelope does not make every vendor field stable.
The workflow: search, normalize, then enrich
Start with a narrow question. “Find Bluetooth headphones” is too broad for a purchasing decision, but it is useful for discovering candidate IDs and shops. A practical pipeline has four stages:
- Search a keyword and save the request, page, timestamp, and returned item IDs.
- Normalize only the fields your application needs.
- Enrich shortlisted items with detail, description images, and comments.
- Keep unknown or masked values as
None; never ask an LLM to infer them.
The endpoint reference documents only keyword and optional string page for search. Do not invent price, sort, or category filters that are not in this contract.
The search page documents keyword plus optional page and a production route of POST /v1/api/jd/search/items. Captured November 1, 2026; current pricing should be checked on the live page.
Here is a defensive client. It reads only the documented completion envelope and fails loudly when the call is incomplete.
import os
import requests
BASE = "https://api.sandbase.ai/v1/api"
HEADERS = {
"Authorization": f"Bearer {os.environ['SANDBASE_API_KEY']}",
"Content-Type": "application/json",
}
def call_jd(path: str, payload: dict) -> dict:
response = requests.post(
f"{BASE}/jd/{path}", headers=HEADERS, json=payload, timeout=60
)
response.raise_for_status()
body = response.json()
outputs = body.get("outputs") or []
if body.get("status") != "completed" or not outputs:
raise RuntimeError(
f"JD call did not complete: {body.get('status')} {body.get('error')}"
)
data = outputs[0].get("data")
if not isinstance(data, dict):
raise RuntimeError("JD call completed without an object payload")
return {"run_id": body.get("id"), "data": data}
result = call_jd("search/items", {"keyword": "蓝牙耳机", "page": "1"})The payload field names below are from calls run on November 1, 2026 (UTC+8). They are illustrative, observed-only fields, not documented guarantees. Search run f392e6eb-1430-4e90-912c-a3dfa49a38ee returned this trimmed structure:
{
"currentPage": 1,
"totalCount": 7598,
"products": [
{
"id": "100196304872",
"title": "SANAG...Z60S 蓝牙耳机...",
"price": "223.00",
"shopName": "SANAG影音京东自营旗舰店"
}
]
}totalCount describes that response, not a stable market size. Search indexes change, and the endpoint returned many additional fields. Save the raw result for audit, but expose a smaller internal object to downstream code:
from decimal import Decimal, InvalidOperation
def money_or_none(value):
try:
return Decimal(str(value))
except (InvalidOperation, TypeError, ValueError):
return None
def normalize_search_item(item: dict, run_id: str) -> dict:
return {
"marketplace": "jd",
"item_id": item.get("id"),
"title": item.get("title") or item.get("shortTitle"),
"price_cny": money_or_none(item.get("price")),
"shop_id": item.get("shopId"),
"shop_name": item.get("shopName"),
"observed_run_id": run_id,
"observed_at": "2026-11-01",
}Why detail needs a separate adapter
The detail endpoint accepts one required itemId. Its description promises title, price, promotion price, brand, shop, and images, but the original payload is much deeper than search.
The detail reference uses one required itemId and cost $0.0278 per completed call when captured on November 1, 2026.
In run fd1ac766-af8c-427e-828f-e8a7e78e7f95, useful product fields were nested under product, while a price object appeared under priceFloor. The observed product had skuId, skuName, brandName, and shopId; the observed price string was masked as 1??.
That is the moment to stop and resist a tempting shortcut. Do not strip the question marks, substitute the search price, or ask a model to “repair” the number. Search and detail are two observations that may refer to different moments or variants. Preserve both with provenance.
def normalize_detail(data: dict, run_id: str) -> dict:
product = data.get("product") or {}
price_floor = data.get("priceFloor") or {}
return {
"marketplace": "jd",
"item_id": product.get("skuId"),
"title": product.get("skuName"),
"brand": product.get("brandName"),
"shop_id": product.get("shopId"),
"detail_price_cny": money_or_none(price_floor.get("price")),
"raw_price": price_floor.get("price"),
"observed_run_id": run_id,
}Add reviews without manufacturing sentiment
The comments endpoint uses itemId and optional string page. It is useful for collecting buyer language, recurring complaints, and questions for manual review. It is not a quality oracle.
The documentation sample request produced no usable recorded response: there is no completed envelope, HTTP status, or run ID in the retained evidence. I therefore cannot classify it as a valid empty result or claim that the comments payload was verified. Production code must distinguish transport or execution failure, a completed no_data response, and a completed response with zero negative comments; a safe research job records all three states.
For text analysis, remove direct identifiers, keep comment dates and product variants when present, deduplicate repeated content, and make the sample window visible. An LLM may cluster observed themes, but the article or dashboard should show sample size and collection date rather than announce that “buyers love” a product.
Cost a real research job
Suppose a daily job searches 20 keywords, enriches 50 shortlisted products, and requests one comment page for 20 finalists:
| Stage | Calls/day | Unit price | Daily cost |
|---|---|---|---|
| Search | 20 | $0.0208 | $0.416 |
| Detail | 50 | $0.0278 | $1.390 |
| Comments | 20 | $0.0278 | $0.556 |
| Total | 90 | $2.362 |
At 30 identical runs, the arithmetic is $70.86 per month. That is a scenario estimate using prices observed on November 1, not a quote or guarantee. You can reduce spend by caching details by item ID, enriching only items that survive deterministic filters, and refreshing volatile fields more often than static images or attributes.
Architecture choices that matter
- Keep raw and normalized layers. Raw JSON enables later debugging; normalized rows protect the rest of the application from upstream changes.
- Make provenance a field. Store run ID, request hash, page, item ID, and observation time with every record.
- Separate discovery from monitoring. Search finds candidates; a monitor follows a fixed ID list. Re-running search alone can silently change the cohort.
- Use deterministic gates before an LLM. Validate IDs, parse numeric prices, check missing fields, and deduplicate first. Send only a bounded evidence bundle to the model.
- Treat pagination as a state machine. The endpoint documents page-based pagination. Save the requested page and stop on empty results; do not invent a cursor.
Limits
Marketplace results are time-bound snapshots. Prices, promotions, stock, sellers, and reviews can change. A listing does not prove authenticity or fulfillment quality. Search counts are not demand estimates. Comment text can be biased, duplicated, promotional, or variant-specific. Finally, the original JD payload is broad and may change independently of the common SandBase envelope.
If your real problem is wholesale sourcing and MOQ rather than retail catalog research, the 1688 Product Finder guide is the closer workflow. For multi-market offer comparison, see the shopping price comparison tool.
FAQ
Do I need a JD.com developer key?
The endpoint pages use SandBase bearer authentication. You need a SandBase API key for these routes; the page does not require a separate JD.com developer key.
Can I use /v1/run?
Use the production Platform API route shown on each endpoint page: POST /v1/api/jd/<path>. The catalog may display older generated snippets elsewhere, but platform-data tutorials should use the documented POST route and read only outputs[0].data.
Can an agent compare prices automatically?
Yes, after deterministic parsing. Keep masked or missing prices as unknown, record currency and observation time, and avoid comparing different variants as if they were identical SKUs.
How often should I refresh data?
There is no universal interval. Search daily for discovery, refresh shortlisted detail records according to the business decision, and cache slow-changing assets. Record the chosen schedule so a dashboard never presents old observations as live stock.
Bottom line
The new JD.com endpoints cover the stages of a useful retail-research pipeline: search for candidates, enrich known IDs, inspect description assets, walk shop catalogs, and request comment evidence. Search and detail were validated in this test; the comments response was not. The hard part is not the HTTP request. It is preserving uncertainty when evidence is masked, missing, unvalidated, or structurally different.
Build one adapter per endpoint, attach provenance to every normalized record, and let an agent reason only over that bounded layer. That turns a volatile marketplace snapshot into an auditable research input without pretending it is a purchasing decision.