京东开放平台里“京东联盟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(需买包提频) |
库存数据 | 仅返回有货/无货/预售状态,无具体 | 返回真实可售库存、锁定库存、在途库存 |
价格数据 | 促销价、券后价、佣金比例 | 商家后台基准价、协议价 |
网关 | 同JOS网关 | 同上(但 |
⚠️致命误区:用联盟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次上限时自动熔断非核心同步任务的脚本?