方式五:API 接口对接
适合有自研订单系统、或前四种方式满足不了的定制需求。和平台授权 / ERP 授权的区别只有一条:这条路要写代码。 只是单量大、不想开发的话,先看方式二:批量导入;订单在平台或第三方 ERP 里,看方式三~四:自动进单。
什么时候才值得走 API
| 你的订单在哪 | 建议走哪条 |
|---|---|
| Amazon、Shopify、乐天等销售平台 | 平台授权同步,不用开发 |
| 在用的第三方 ERP | ERP 授权同步,不用开发 |
| 只是量大,人工录不过来 | 批量导入,不用开发 |
| 自研订单系统,或要定制字段、定制触发时机 | API 接口对接 |
对接前要拿到什么
| 东西 | 从哪来 |
|---|---|
appKey / appSecret | OMS 右上角头像 → API信息(/userCenter/apiKeys) |
| 接口文档 | https://apidoc-oms.xlwms.com/ |
| 接口基础地址 | https://api.xlwms.com/openapi/ |
这个页面是一张表:App Key / App Secret / 备注 / 创建时间 / 创建人,密钥默认打码显示。右上角还有个开发验签工具按钮,直达官方验签页面。
appSecret 是签名密钥,只能放在你自己的服务端,不要写进前端代码或 App。请求怎么签名
每个请求都要带签名 authcode,算法是 HmacSHA256:
- 把业务参数
data里的字段按**字典序(不区分大小写)**排序,再序列化成 JSON - 按
appKey+ 排序后的data+reqTime的顺序拼成一个字符串,中间没有任何分隔符 - 以
appSecret为密钥对该字符串做 HmacSHA256,结果转成十六进制字符串,就是authcode
传参方式也要注意:authcode 走 URL 查询参数,appKey / data / reqTime 走 POST 请求体。
| 参数 | 位置 | 说明 |
|---|---|---|
authcode | URL query | 上面算出来的签名 |
appKey | POST body | OMS 后台给的 key |
reqTime | POST body | 10 位 UNIX 秒级时间戳 |
data | POST body | 业务参数,创建接口是数组 |
reqTime 超过 5 分钟的请求会被服务端拒绝。 别用缓存下来的签名重放,也别让服务器时钟漂太多。签名对不上就用官方验签工具
文档站提供了在线验签工具,把你的 appKey、data、reqTime 贴进去比对,能立刻看出是排序、序列化还是拼接出了问题。
签名示例代码
按文档实现的最小可运行版本,联调前请先用官方验签工具核对一次:
js
// Node.js 18+
import crypto from 'node:crypto'
function sign({ appKey, appSecret, data, reqTime }) {
// 1. data 内字段按字典序(不区分大小写)排序后序列化
const sortedData = JSON.stringify(sortKeys(data))
// 2. 三段直接拼接,没有分隔符
const raw = `${appKey}${sortedData}${reqTime}`
// 3. HmacSHA256 后转十六进制
return crypto.createHmac('sha256', appSecret).update(raw).digest('hex')
}
function sortKeys(value) {
if (Array.isArray(value)) return value.map(sortKeys)
if (value && typeof value === 'object') {
return Object.fromEntries(
Object.keys(value)
.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()))
.map((k) => [k, sortKeys(value[k])])
)
}
return value
}
const appKey = process.env.OMS_APP_KEY
const appSecret = process.env.OMS_APP_SECRET
const reqTime = String(Math.floor(Date.now() / 1000))
const data = [
{
whCode: 'TEST',
thirdOrderNo: 'SO-20260903-0001',
subOrderType: 1,
logisticsChannel: 'T_YAMATO_BIN',
receiver: '佐藤 健太',
telephone: '09012345678',
countryRegionCode: 'JP',
provinceName: '東京都',
cityName: '渋谷区',
postCode: '150-0002',
addressOne: '渋谷 3-15-7',
isSubmit: 1,
itemList: [{ sku: 'APP-TSHIRT-006', quantity: 1 }]
}
]
const authcode = sign({ appKey, appSecret, data, reqTime })
const res = await fetch(
`https://api.xlwms.com/openapi/v1/outboundOrder/create?authcode=${authcode}`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ appKey, reqTime, data })
}
)
console.log(await res.json())python
# Python 3
import hashlib, hmac, json, os, time, requests
def sort_keys(value):
if isinstance(value, list):
return [sort_keys(v) for v in value]
if isinstance(value, dict):
return {k: sort_keys(value[k]) for k in sorted(value, key=str.lower)}
return value
def sign(app_key, app_secret, data, req_time):
sorted_data = json.dumps(sort_keys(data), ensure_ascii=False, separators=(',', ':'))
raw = f'{app_key}{sorted_data}{req_time}'
return hmac.new(app_secret.encode(), raw.encode(), hashlib.sha256).hexdigest()
app_key, app_secret = os.environ['OMS_APP_KEY'], os.environ['OMS_APP_SECRET']
req_time = str(int(time.time()))
data = [{
'whCode': 'TEST',
'thirdOrderNo': 'SO-20260903-0001',
'subOrderType': 1,
'logisticsChannel': 'T_YAMATO_BIN',
'receiver': '佐藤 健太',
'telephone': '09012345678',
'countryRegionCode': 'JP',
'provinceName': '東京都',
'cityName': '渋谷区',
'postCode': '150-0002',
'addressOne': '渋谷 3-15-7',
'isSubmit': 1,
'itemList': [{'sku': 'APP-TSHIRT-006', 'quantity': 1}],
}]
authcode = sign(app_key, app_secret, data, req_time)
r = requests.post(
'https://api.xlwms.com/openapi/v1/outboundOrder/create',
params={'authcode': authcode},
json={'appKey': app_key, 'reqTime': req_time, 'data': data},
)
print(r.json())创建一件代发出库单
POST /v1/outboundOrder/create,单次最多 100 单。
主要字段(完整字段以官方文档为准):
| 字段 | 必填 | 对应界面上的 | 说明 |
|---|---|---|---|
whCode | ✳︎ | 仓库 | 仓库代码,最长 30 |
thirdOrderNo | ✳︎ | —— | 外部单号,全局唯一,最长 100 |
subOrderType | ✳︎ | —— | 1 = 按产品,2 = 按箱 |
logisticsChannel | ✳︎ | 物流渠道 | 渠道代码,如 T_YAMATO_BIN |
referOrderNo | 参考单号 | 你自己系统的编号 | |
platformOrderNo | 平台单号 | 平台上的原始订单号 | |
isSubmit | —— | 0 = 存草稿(默认),1 = 直接提交 | |
remark | 备注 | 接口限 255,但承运商侧只接受 32 个全角 | |
receiver | ✳︎ | 收件人 | |
telephone | 电话 | 接口非必填,但黑猫 / 佐川必须填 | |
countryRegionCode | ✳︎ | 国家/地区 | 日本固定 JP |
provinceName | ✳︎ | 省/州 | 日本填都道府县,如 東京都 |
provinceCode | ✳︎ | 省/州 | 美国、加拿大、澳大利亚必填 |
cityName | ✳︎ | 城市名称 | 日本填市区町村 |
postCode | ✳︎ | 邮编 | |
addressOne | ✳︎ | 地址-1 | 接口限 255,但承运商侧只接受 16 个全角 |
addressTwo | 地址-2 | 同上,16 个全角 | |
district | 区/县 | 发日本留空 | |
itemList[].sku | ✳︎ | SKU | |
itemList[].quantity | ✳︎ | 出库数量 |
isSubmit 决定单据落在哪:填 1 直接提交,之后的走向仍按渠道分叉——黑猫直接进仓库处理中,佐川停在草稿的「待提交」。见第 6 步。建完之后还有这些接口
| 用途 | 接口 |
|---|---|
| 分页查询出库单列表 | getParcelOutboundOrders |
| 查询出库单详情(回读状态、跟踪号) | getOutboundOrder |
| 批量取消出库单(异步) | batchCancelOutboundOrder |
| 查询出库单聚合状态(确认取消结果) | selectBizStatus |
| 更新跟踪号或面单(用自有面单时) | updateTrackNoAndLabel |
| 出库单加留言和附件 | replyBigOutboundMessage |
三个必须避开的坑
重要 HTTP 500 不等于创建失败。 官方文档写明网络异常时不能直接当作失败——盲目重试会重复建单。正确做法是用查询详情接口按thirdOrderNo 核对,确认没建成再重试。注意 code: 200 只代表这次请求成功,不代表每一单都建成了。逐单结果看返回的 data[].success 和 data[].msg。注意 thirdOrderNo 全局唯一,拿它当幂等键——重复提交同一个外部单号不会重复建单,这是你重试逻辑的安全网。备货中转(ToB)走另一组接口
本页讲的都是一件代发(小包出库单)。备货中转出库(ToB)是另一组接口:createBigOutboundOrder(按产品)和 createBoxStockOutboundOrder(按箱),查询和取消接口也是独立的一套。
接口文档以官方为准
本页只讲对接一件代发出库单的主干流程和容易踩的坑。完整字段、错误码、以及其他业务的接口,看官方文档:https://apidoc-oms.xlwms.com/