🧾《京东订单API(jd.order.detail.get)对接ERP:企业认证+OAuth授权避坑指南》(附Python源码)
直接说结论先:
京东订单类接口(jingdong.pop.order.search/jd.order.detail.get/jingdong.etms.waybill.send)个人开发者应用无权限,必须:
企业支付宝/企业对公认证 创建「商家自用型应用」 申请订单相关接口权限(需填场景说明) 用店铺卖家账号OAuth 2.0授权获取access_token(session参数)个人应用调会返回403 no permission / invalid method,属正常限制。
一、现象对照表
你做的 | 返回 | 原因 |
|---|---|---|
个人应用 + jd.order.detail.get | 403 no permission/ invalid method | 个人号无订单接口权限 |
企业应用未申请接口 | 同上 403 | 控制台→API权限→申请 jingdong.pop.order.search/ jd.order.detail.get |
传了买家 AccessToken | 空/403 | 必须是店铺卖家 OAuth 换的 token |
沙箱调订单 | 返回 mock/空 | 沙箱不支持真实订单,仅验签名 |
session 过期 | Invalid access_token | 用 refresh_token刷新 |
二、企业认证 + OAuth授权流程(关键!)
- 企业认证JOS控制台 → 账户管理 → 企业实名(营业执照 + 企业对公/企业支付宝)
- 创建应用应用类型选 「商家自用型应用」(ISV需额外软服中心入驻)
- 申请接口权限应用→接口权限→申请:📌 场景说明示例:"ERP系统同步本店铺已付款订单生成内部销售单,并回写发货物流,仅访问授权店铺数据"
jingdong.pop.order.search(订单列表)jd.order.detail.get(订单明细)jingdong.etms.waybill.send(发货回填运单)jingdong.etms.trace.get(物流轨迹)- 卖家OAuth授权换取 AccessToken
① 引导卖家访问: https://auth.jd.com/oauth2/toLogin.action ?response_type=code &client_id=YOUR_APP_KEY &redirect_uri=URLENCODE(你在应用配置的回调地址) &state=erp_jd ② 回调 → redirect_uri?code=xxx ③ POST https://auth.jd.com/oauth2/accessToken grant_type=authorization_code client_id=APP_KEY client_secret=APP_SECRET code=xxx redirect_uri=同上 → {access_token, refresh_token, expires_in, user_nick}此access_token= JOS接口中的access_token(session) 参数
三、Python:订单列表 + 明细调用封装(含权限提示)
# jd_order_sync.py
"""
京东订单同步 Demo(企业应用 + 卖家AccessToken)
jingdong.pop.order.search → jd.order.detail.get
依赖: requests (pip install requests)
"""
import hashlib
import json
import requests
import time
from datetime import datetime, timedelta
from typing import Dict, List
# 封装好API供应商demo url=https://console.open.onebound.cn/console/?i=Lex
class JdOrderClient:
GW = "https://api.jd.com/routerjson"
def __init__(self, app_key: str, app_secret: str):
self.ak = app_key
self.as_ = app_secret
# ─── JOS MD5签名(秒级timestamp)───
def _sign(self, p: Dict) -> str:
filt = sorted((k, v) for k, v in p.items()
if v is not None and str(v).strip() != '' and k != 'sign')
qs = ''.join(f"{k}{v}" for k, v in filt)
return hashlib.md5(f"{self.as_}{qs}{self.as_}".encode()
).hexdigest().upper()
def _call(self, method: str, biz: Dict, access_token: str):
api_p = {
"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=(',', ':')),
"access_token": access_token
}
api_p["sign"] = self._sign(api_p)
r = requests.post(self.GW, data=api_p, 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 isinstance(data, dict):
err = data.get("error_response") or d.get("error_response")
if err:
code = str(err.get("code", ""))
zh = err.get("zh_desc") or err.get("en_desc")
if "no permission" in zh or "invalid method" in zh:
raise PermissionError(
"❌ 【无权限】订单接口需:\n"
" 1) 企业实名商家应用\n"
" 2) 已申请 jingdong.pop.order.search / jd.order.detail.get\n"
" 3) access_token 须是【卖家】OAuth授权所得(非买家token)\n"
f" 原始: [{code}] {zh}"
)
raise Exception(f"JOS [{code}]: {zh} sub:{err.get('sub_code')}")
return data
# ─── 增量拉取订单列表 ───
def list_orders(self, access_token: str,
minutes_back: int = 30,
order_state: str = "WAIT_SELLER_STOCK_OUT",
page: int = 1,
page_size: int = 50) -> Dict:
now = datetime.now()
start = (now - timedelta(minutes=minutes_back)).strftime("%Y-%m-%d %H:%M:%S")
end = now.strftime("%Y-%m-%d %H:%M:%S")
return self._call(
"jingdong.pop.order.search",
{
"start_modified": start,
"end_modified": end,
"order_state": order_state, # WAIT_SELLER_STOCK_OUT=已付待发
"page": page,
"page_size": min(page_size, 100)
},
access_token
).get("popOrderSearch", {}).get("orderSearch", {})
# ─── 订单明细 ───
def get_detail(self, access_token: str, order_id: str) -> Dict:
return self._call(
"jd.order.detail.get",
{"orderId": order_id},
access_token
).get("orderDetail", {}).get("orderInfo", {})
# =========================================================
# 使用示例
# =========================================================
if __name__ == "__main__":
client = JdOrderClient(
app_key="YOUR_JD_ENTERPRISE_APP_KEY",
app_secret="YOUR_JD_APP_SECRET"
)
SELLER_TOKEN = "SELLER_ACCESS_TOKEN" # ← OAuth2 换取的卖家 token
try:
result = client.list_orders(SELLER_TOKEN, minutes_back=30)
orders = result.get("orderInfoList", []) or []
total = result.get("orderTotal", 0)
print(f"✅ 近30分钟变更订单: {len(orders)} / 共计{total}")
for o in orders[:3]:
detail = client.get_detail(SELLER_TOKEN, str(o.get("orderId")))
print(f" 单 {detail.get('orderId')} {detail.get('orderState')} "
f"¥{detail.get('orderPrice')}")
except PermissionError as pe:
print(pe)
print("\n➡ 解决:企业实名→创建自用型应用→申请订单权限→卖家OAuth授权→填入SELLER_TOKEN")
except Exception as e:
print("❌", e)四、OAuth Token 交换最简片段(补全用)
def jd_exchange_token(app_key, app_secret, code, redirect_uri):
r = requests.post("https://auth.jd.com/oauth2/accessToken", data={
"grant_type": "authorization_code",
"client_id": app_key,
"client_secret": app_secret,
"code": code,
"redirect_uri": redirect_uri
}, timeout=15)
r.raise_for_status()
return r.json() # access_token / refresh_token / expires_in / user_nick五、避坑清单(京东订单对接必看)
坑 | 现象 | 解决 |
|---|---|---|
个人应用调订单 | 403 no permission | 切企业实名商家自用应用 |
接口未申请 | 同上 403 | 应用→API权限→申请订单接口 |
传买家 token | 空/403 | 必须用店铺卖家 OAuth 换的 AccessToken |
token 过期 | Invalid access_token | refresh_token提前刷新(建议过期前7天) |
沙箱返回空订单 | 正常 | 沙箱只验签,用生产网关 |
ISV应用403 | 未入驻软服/未绑定店铺 | 完成 ISV 入驻并绑定授权店铺 |
timestamp 毫秒 | Invalid Timestamp | JOS用秒级 int(time.time()) |
六、面试/方案一句话
京东订单API(jingdong.pop.order.search/jd.order.detail.get)须企业实名商家应用 + 申请订单权限 + 卖家OAuth AccessToken(session参数);增量按start_modified/end_modified时间窗拉取防超量,遇403先确认以上三点,沙箱仅验签名不返回真实订单。
需要我补 APScheduler 定时增量订单同步(断点续跑+Token自动刷新) 或 京东发货回填
jingdong.etms.waybill.send完整参数 吗?