京东开放平台里“京东联盟API”和“商家JOS接口”是完全两套独立的体系,很多人容易把它们的额度、权限和网关搞混,导致调用超限或拿不到想要的数据。核心区别一句话先给:联盟API走选品推广路线(免店铺授权、量大但库存只有状态),商家JOS走ERP履约路线(需店铺授权、量小但拿真实库存)。
一、两套额度与定位全景对照
维度 | 京东联盟API (jd.union.open.*) | 京东商家JOS (jingdong.ware/read/get 等) |
|---|---|---|
核心用途 | 选品比价、CPS推广、价格/佣金监控 | ERP订单同步、真实库存管理、发货回写、区域仓存 |
授权要求 | 联盟实名应用(无需店铺授权) | 企业认证 + 卖家OAuth2 AccessToken |
免费额度 | 备案后通常 月100万次 级别(日约数万~十万) | 企业应用通常 3000次/天 左右基础免费(订单/库存类更低,超量按¥0.02~0.10/百次) |
默认QPS | 5~10/s(可申请提) | 个人1~2/s,企业5~10/s(需买包提频) |
库存数据 | 仅返回 有货/无货/预售状态,无具体 stockNum | 返回 真实可售库存、锁定库存、在途库存 |
价格数据 | 促销价、券后价、佣金比例 | 商家后台基准价、协议价 |
网关 | 同JOS网关 https://api.jd.com/routerjson(签名逻辑一致) | 同上(但 access_token类型不同) |
⚠️ 致命误区:用联盟API去拿自己店铺的真实库存数 → 拿不到;用商家JOS去批量爬竞品比价 → 额度瞬间耗尽且涉嫌越权。
二、Python源码:双通道Client区分调用(防混用)
下面给你一个统一网关、分离Client的示例,明确区分联盟应用与商家应用的调用逻辑,避免把3000次/天的商家额度浪费在选品上。
# jd_dual_api_client.py
"""
京东双通道API Client
1. UnionClient -> 京东联盟API (jd.union.open.*) 月百万次级,免店铺授权
2. JosMerchantClient -> 京东商家JOS (jingdong.ware.read.get等) 日3000次级,需卖家Token
共用:MD5签名、秒级timestamp、routerjson网关
"""
import hashlib
import json
import time
import requests
from typing import Dict, List
封装好API供应商demo url=https://console.open.onebound.cn/console/?i=Lex
class _BaseJdClient:
GW = "https://api.jd.com/routerjson"
def __init__(self, app_key: str, app_secret: str):
self.ak = app_key
self.ask = app_secret
def _sign(self, params: Dict) -> str:
"""JOS通用MD5签名:AppSecret + 排序KV + AppSecret -> 大写"""
filtered = sorted(
(k, v) for k, v in params.items()
if k != "sign" and v is not None and str(v).strip() != ""
)
qs = "".join(f"{k}{v}" for k, v in filtered)
raw = f"{self.ask}{qs}{self.ask}"
return hashlib.md5(raw.encode("utf-8")).hexdigest().upper()
def _post(self, method: str, biz: Dict, token: str = None) -> Dict:
params = {
"app_key": self.ak,
"method": method,
"timestamp": str(int(time.time())), # 京东统一秒级
"format": "json",
"v": "2.0",
"sign_method": "md5",
"360buy_param_json": json.dumps(biz, ensure_ascii=False, separators=(',', ':'))
}
if token:
params["access_token"] = token
params["sign"] = self._sign(params)
r = requests.post(self.GW, data=params, timeout=15)
r.raise_for_status()
d = r.json()
# 提取响应
resp_key = method.replace(".", "_") + "_response"
if resp_key not in d:
for k in d:
if k.endswith("_response"):
resp_key = k
break
data = d.get(resp_key, d)
if "error_response" in str(data):
err = d.get(resp_key, {}).get("error_response") or d.get("error_response")
if err:
raise Exception(f"JOS[{err.get('code')}]: {err.get('zh_desc') or err.get('en_desc')}")
return data
## ================= 联盟通道(免店铺授权) =================
class JdUnionClient(_BaseJdClient):
"""
适用:jd.union.open.goods.detail.query / goods.query / promotion.goodsByPid
额度:月100万次级免费,QPS~10
数据:有货/无货状态、券后价、佣金
"""
def get_goods_detail(self, sku_ids: List[int], fields: str = None) -> Dict:
fields = fields or (
"skuId,productName,price,promotionPrice,couponInfo,"
"stockState,commissionInfo,salesCount,shopName"
)
biz = {"skuIds": sku_ids, "fields": fields}
return self._post("jd.union.open.goods.detail.query", biz)
def parse_stock_status(self, item: Dict) -> str:
"""联盟仅返回库存状态:1有货 0无货"""
st = item.get("stockState", item.get("skuList", [{}])[0].get("stock"))
if st == 1 or str(st) == "1":
return "有货(状态值1)"
return "无货/未知(状态值0)"
## ================= 商家JOS通道(需卖家Token) =================
class JosMerchantClient(_BaseJdClient):
"""
适用:jingdong.ware.read.get / jingdong.stock.get / pop.order.search
额度:企业约3000次/天基础免费(订单类更低),超量计费
数据:真实stockNum、订单明细、区域库存
"""
def get_ware_with_real_stock(self, ware_id: str, seller_token: str) -> Dict:
"""查自己店铺商品,必须传卖家AccessToken"""
fields = (
"ware_id,title,jd_price,stock_num,"
"skus_json,outer_id,approve_status"
)
biz = {"wareId": ware_id, "fields": fields}
return self._post("jingdong.ware.read.get", biz, token=seller_token).get("ware", {})
def get_orders_incremental(self, seller_token: str, start: str, end: str, page: int = 1) -> Dict:
"""订单增量拉取(消耗商家额度,慎用)"""
return self._post("jingdong.pop.order.search", {
"start_modified": start,
"end_modified": end,
"order_state": "WAIT_SELLER_STOCK_OUT,FINISHED",
"page": page,
"page_size": 50
}, token=seller_token)
# =========================================================
# 使用示例:明确分流,别把商家额度用在联盟场景上
# =========================================================
if __name__ == "__main__":
# ---- 联盟应用(无店铺也能跑)----
union_cli = JdUnionClient("UNION_APP_KEY", "UNION_APP_SECRET")
try:
detail = union_cli.get_goods_detail([100012345678])
data_list = detail.get("data", detail.get("jd_union_open_goods_detail_query_response", {}).get("data", []))
if isinstance(data_list, dict):
data_list = data_list.get("data") or []
for it in (data_list or []):
print(f"[联盟] SKU:{it.get('skuId')} 价:{it.get('promotionPrice')} "
f"库存:{union_cli.parse_stock_status(it)}")
except Exception as e:
print("联盟API异常:", e)
# ---- 商家应用(必须卖家Token)----
merchant_cli = JosMerchantClient("MERCHANT_APP_KEY", "MERCHANT_APP_SECRET")
SELLER_TOKEN = "YOUR_SELLER_ACCESS_TOKEN"
try:
ware = merchant_cli.get_ware_with_real_stock("100012345678", SELLER_TOKEN)
print(f"[商家] 标题:{ware.get('title')} 真实库存:{ware.get('stock_num')}")
except Exception as e:
print("商家JOS异常(可能额度耗尽/无权限):", e)三、避坑与选型建议
- 额度隔离使用:选品、价格监控、竞品爬取 无脑用联盟API(月100万次免费,不占商家额度);订单同步、真实库存、发货回写必须用商家JOS(日3000次精打细算,超了按量扣费)。
- 库存认知差:联盟返回的
stockState=1只是“前端显示有货”,可能是区域有货或虚拟库存;商家JOS的stockNum才是可售实物库存,做ERP防超卖只能信后者。 - Token别混传:联盟接口不传店铺
seller_access_token(传了也没额外数据);商家接口必须传,且Token过期(通常24h)要用RefreshToken刷新,否则直接403。 - QPS防护:联盟虽然额度大但默认QPS约10,批量跑时加令牌桶;商家接口免费QPS极低(2~5),高频调用前务必确认是否已购QPS资源包,否则频繁429会导致订单漏拉。
一句话记死:联盟管“看”(免费大方、浅数据),商家管“干”(额度抠门、深数据),两套Key两套逻辑,千万别串线。
要不要我帮你在这个双通道Client基础上,补一个每日调用计数器(分别统计联盟/商家额度消耗),并在接近商家3000次上限时自动熔断非核心同步任务的脚本?