OrangeShip 开放接口:订单履约 API
渠道方通过本接口把待发货订单交给 OrangeShip 履约,并取回运单号与物流状态。
01概述
OrangeShip 是订单履约平台。渠道方(电商平台、订单管理系统、ERP 等)通过本接口把已确认的订单推送给 OrangeShip,由 OrangeShip 分配海外仓或承运商完成履约;渠道方再通过本接口取回运单号和物流状态,回写到自己的系统。
调用方向是单向的:只有渠道方调用 OrangeShip,OrangeShip 不回调渠道方。订单状态的变化通过「拉取订单变更」接口按游标增量获取。
1.1接口一览
| 接口 | 方法与路径 | 用途 | 限流分组 |
|---|---|---|---|
| 连通性检查 | GET/ping | 校验凭证与签名,返回连接信息与服务器时间。 | 查询 |
| 推送订单 | POST/orders | 批量推送订单,每次最多 200 单,按 order_id 幂等。 | 写入 |
| 取消订单 | POST/orders/{order_id}/cancel | 退款、取消或改地址时取消订单。 | 写入 |
| 拉取订单变更 | GET/orders/changes | 按游标增量取回状态、运单号的变化。取运单号的主通道。 | 查询 |
| 查询订单 | GET/orders/{order_id} | 单笔订单快照,含商品行映射状态。 | 查询 |
| 查询物流轨迹 | GET/orders/{order_id}/tracking | 单笔订单的物流事件,买家查看物流时按需调用。 | 查询 |
1.2对接流程
- 获取凭证
商家在 OrangeShip 后台「渠道接入」创建连接,把 AppKey 和 AppSecret 交给渠道方(§2.2)。
- 实现签名
按 §3.2 实现签名,先用 §3.4 的测试向量离线自测。
- 验证连通
调用
GET /ping,返回code: 0即凭证与签名正确。 - 推送与取消
订单锁定后调用
POST /orders;退款、取消或改地址时调用POST /orders/{order_id}/cancel。 - 取回运单号
定时调用
GET /orders/changes,按游标拉取状态与运单号并回写(§6)。 - 按需查询
买家查看物流时调用
GET /orders/{order_id}/tracking。
1.3渠道专用字段
部分字段只适用于特定渠道类型,在字段说明中以标签注明(如 PledgeBox)。其余字段,以及鉴权、幂等、订单变更、订单状态、错误码与限流,对所有渠道相同。
02环境与凭证
2.1环境
| 环境 | Base URL | 说明 |
|---|---|---|
| 生产 | https://orangeshipping.xyz/open/v1 | 正式订单。凭证由商家在 OrangeShip 后台生成。 |
| 联调 | 同生产 | OrangeShip 为渠道方开通独立的联调商家与连接,凭证由 OrangeShip 直接提供。联调连接的订单不会发往真实仓库。 |
本文所有路径都相对于 Base URL。例如「推送订单」的完整地址是 POST https://orangeshipping.xyz/open/v1/orders。
2.2凭证
| 凭证 | 格式 | 说明 |
|---|---|---|
AppKey | 32 位十六进制 | 连接的公开标识,放在请求头 X-App-Key。 |
AppSecret | 48 位字符 | 签名密钥,不随请求发送。只在商家创建连接时显示一次,不可找回;遗失需新建连接。 |
- 一个连接对应渠道方的一个店铺或项目。
- 订单归属哪个商家、哪个连接由
AppKey决定,请求中不需要传商家或企业标识。 order_id在连接内唯一;不同连接可以出现相同的order_id。- 轮换凭证:商家新建连接并切换到新凭证,再停用旧连接。停用后旧连接拒绝推单和取消,但仍可读:请继续用旧凭证拉取其订单变更,直到在途订单全部取回(订单不会在连接之间迁移)。
2.3通用约定
| 项目 | 约定 |
|---|---|
| 传输 | 仅 HTTPS。 |
| 编码 | JSON,UTF-8。带 body 的请求必须带 Content-Type: application/json。 |
| 时间 | ISO 8601 日期时间,必须带时区,推荐 UTC(2026-09-18T02:10:00Z),也可带偏移(2026-09-18T10:10:00+08:00)。不带时区的值会被拒绝。响应中的时间一律为 UTC 并带毫秒,如 2026-09-20T08:00:05.000Z。 |
| 金额 | 小数字符串,如 "129.00",不接受 JSON 数字。格式 ^-?\d{1,10}(\.\d{1,6})?$;按 2 位小数存储,多出的小数位四舍五入。 |
| 标识 | 一律为字符串。oms_order_id 是 OrangeShip 内部订单号(64 位整数的字符串形式),仅供参考,调用接口时始终使用 order_id。 |
| 字符串 | 校验前先去掉首尾空白;空串(含只有空白的字符串)按未传处理。 |
| 响应信封 | 所有响应为 { "code": 整数, "message": 字符串, "data": 对象或 null },另带 requestId(字符串或 null)。code = 0 表示成功,其它见 §8.2。 |
| 可空字段 | 标为可空的字段总会出现,值为 null,不会省略。 |
| 未知字段 | 请求中的未知字段会被忽略;客户端也必须忽略响应中的未知字段(§10)。 |
03鉴权
每个请求都用连接的 AppSecret 做 HMAC-SHA256 签名,并把签名放在请求头里。不需要登录,也没有 token 交换。
3.1请求头
| 请求头 | 必填 | 说明 |
|---|---|---|
X-App-Key | 是 | 连接的 AppKey。 |
X-Timestamp | 是 | Unix 时间戳,单位秒,十进制整数字符串。与服务器时间相差超过 300 秒的请求被拒绝(41102)。 |
X-Signature | 是 | 按 §3.2 计算的签名,小写十六进制。 |
Content-Type | POST 必填 | application/json。缺失时服务端读不到请求体,签名校验失败(41101)。 |
3.2签名算法
method = 大写的 HTTP 方法,如 POST、GET
path = 请求路径,含 /open/v1 前缀,不含 query string,如 /open/v1/orders/changes
body_hash = lowercase_hex( SHA-256( 请求体的原始字节 ) )
canonical = X-Timestamp + "." + method + "." + path + "." + body_hash
X-Signature = lowercase_hex( HMAC-SHA256( key = AppSecret, message = canonical ) )规则:
- 签名的是实际发送的字节。请求体只序列化一次,用这串字节计算
body_hash并原样发送;签名后不要再次序列化。 - 无请求体(所有 GET)时按空串计算,其 SHA-256 为
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855。 path为绝对路径,以/开头并包含/open/v1前缀;不含 scheme、host、端口和 query string。路径中的order_id先做百分号编码再参与签名,与发出的请求行保持一致。X-Timestamp原样参与签名,不补零、不带小数。- 方法用大写,十六进制用小写。
AppSecret按 UTF-8 原始字节使用,不做 base64 解码。- 四段之间用字符
.连接,末尾没有换行。
3.3示例代码
const { createHash, createHmac } = require('node:crypto');
// url:完整请求地址;body:将要发送的字符串(GET 传 '')
function signRequest({ appKey, appSecret, method, url, body = '' }) {
const timestamp = String(Math.floor(Date.now() / 1000));
const path = new URL(url).pathname; // 如 /open/v1/orders,不含 query
const bodyHash = createHash('sha256').update(body, 'utf8').digest('hex');
const canonical = `${timestamp}.${method.toUpperCase()}.${path}.${bodyHash}`;
const signature = createHmac('sha256', appSecret).update(canonical, 'utf8').digest('hex');
return {
'X-App-Key': appKey,
'X-Timestamp': timestamp,
'X-Signature': signature,
...(body ? { 'Content-Type': 'application/json' } : {}),
};
}
// 调用示例:推送订单
const url = 'https://orangeshipping.xyz/open/v1/orders';
const body = JSON.stringify(payload); // 只序列化一次
const res = await fetch(url, { method: 'POST', body, headers: signRequest({ appKey, appSecret, method: 'POST', url, body }) });import hashlib, hmac, json, time, urllib.parse, urllib.request
def sign_request(app_key, app_secret, method, url, body=b""):
timestamp = str(int(time.time()))
path = urllib.parse.urlsplit(url).path # 如 /open/v1/orders,不含 query
body_hash = hashlib.sha256(body).hexdigest()
canonical = f"{timestamp}.{method.upper()}.{path}.{body_hash}".encode("utf-8")
signature = hmac.new(app_secret.encode("utf-8"), canonical, hashlib.sha256).hexdigest()
headers = {"X-App-Key": app_key, "X-Timestamp": timestamp, "X-Signature": signature}
if body:
headers["Content-Type"] = "application/json"
return headers
# 调用示例:推送订单
url = "https://orangeshipping.xyz/open/v1/orders"
body = json.dumps(payload, separators=(",", ":")).encode("utf-8") # 只序列化一次
req = urllib.request.Request(url, data=body, method="POST",
headers=sign_request(app_key, app_secret, "POST", url, body))
with urllib.request.urlopen(req, timeout=60) as resp:
result = json.loads(resp.read())3.4测试向量
用以下固定输入验证签名实现,结果必须逐字一致。向量已分别用 Node.js 与 Python 独立计算。
- AppSecret
demo-secret-do-not-use-in-production- X-Timestamp
1789000000
向量 A:POST /orders(带请求体)
| Method | POST |
|---|---|
| Path | /open/v1/orders |
| 原始请求体(精确字节,无换行) | {"batch_id":"wave-2-2026-09-18-001","orders":[]} |
| body_hash | 3884346b6e08c3ad2dfcec6473332f733a7e646cb6c5641b670419e2b993878d |
| canonical | 1789000000.POST./open/v1/orders.3884346b6e08c3ad2dfcec6473332f733a7e646cb6c5641b670419e2b993878d |
| X-Signature | dba4cb3f99da1ca98e32848cfa7a6b918f2f230fa432aebfc1fc1e00017f8fd5 |
向量 B:GET /orders/changes(空请求体;query string 不影响签名)
| Method | GET |
|---|---|
| Path | /open/v1/orders/changes |
| 原始请求体 | (空) |
| body_hash | e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 |
| canonical | 1789000000.GET./open/v1/orders/changes.e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 |
| X-Signature | 86025f3bbf1b59a5c73dbb4742f2dd33e7e350fd2d474bc02f4ef30a30f94c7c |
向量 C:POST /orders/{order_id}/cancel(请求体 {})
| Method | POST |
|---|---|
| Path | /open/v1/orders/ord_8f3a1c/cancel |
| 原始请求体 | {} |
| body_hash | 44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a |
| canonical | 1789000000.POST./open/v1/orders/ord_8f3a1c/cancel.44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a |
| X-Signature | d2ef46faae2cf8f2ff84dd3a23385ab92b590d96653fd0836d8b048a60ca72ae |
命令行复现向量 A(两条均输出 dba4cb3f99da1ca98e32848cfa7a6b918f2f230fa432aebfc1fc1e00017f8fd5):
node -e "const c=require('crypto');const b='{\"batch_id\":\"wave-2-2026-09-18-001\",\"orders\":[]}';console.log(c.createHmac('sha256','demo-secret-do-not-use-in-production').update('1789000000.POST./open/v1/orders.'+c.createHash('sha256').update(b).digest('hex')).digest('hex'))"python3 -c "import hashlib,hmac;b=b'{\"batch_id\":\"wave-2-2026-09-18-001\",\"orders\":[]}';print(hmac.new(b'demo-secret-do-not-use-in-production',('1789000000.POST./open/v1/orders.'+hashlib.sha256(b).hexdigest()).encode(),hashlib.sha256).hexdigest())"3.5时间窗与重放
X-Timestamp与服务器时间相差超过 300 秒(任一方向)返回41102。请开启 NTP 时间同步;收到41102时,用GET /ping返回的server_time核对时钟后,换新时间戳重试。- 不使用 nonce。方法、路径、时间戳与请求体都参与签名,截获的签名只对同一个请求在 300 秒内有效;用于其它路径或方法会被拒绝(
41101)。在时间窗内重放同一个请求是无害的:写接口幂等(§5),查询接口只读。 - 三个鉴权请求头应视为机密:只走 HTTPS,代理与日志不要记录请求头。
04接口参考
所有接口都需要 §3.1 的鉴权请求头。鉴权失败、连接停用、限流等通用错误统一见 §8.2,以下各接口只列出特有的错误。
4.1连通性检查
/ping校验凭证与签名,返回连接信息和服务器时间。适合做「测试连接」按钮。
请求参数
无。
请求示例
GET /open/v1/ping HTTP/1.1
Host: orangeshipping.xyz
X-App-Key: 3f9c1d2e4b5a69788796a5b4c3d2e1f0
X-Timestamp: 1789000000
X-Signature: 0ffa6fd797ff65dc66f440be6a1911f99ac4d1bb357292621bd10bdf13a18cef示例签名由 §3.4 的演示密钥对 1789000000.GET./open/v1/ping.e3b0c442…b855 计算得出。
响应
| HTTP | code | 说明 |
|---|---|---|
| 200 | 0 | 凭证与签名有效。连接已停用时同样返回 200,status 为 disabled(本接口不返回 41103)。 |
响应字段(data)
| 字段 | 类型 | 说明 |
|---|---|---|
tenant_name | string | null | 连接所属商家的显示名;读取失败时为 null。 |
connection_name | string | 连接名称(商家配置)。 |
connection_code | string | 连接短码,与 OrangeShip 后台显示一致。建议与 connection_name 一起展示,供商家确认凭证填对了连接。 |
status | string | active 或 disabled。disabled 表示连接已停用:可以查询,不能推单和取消;「测试连接」应将其视为失败并提示商家启用。 |
server_time | string | 服务器当前时间(UTC,带毫秒),用于核对时钟偏差。 |
响应示例
{
"code": 0,
"message": "ok",
"data": {
"tenant_name": "Shenzhen Damai Technology",
"connection_name": "Foldable Drone",
"connection_code": "PB-7K3M9Q",
"status": "active",
"server_time": "2026-09-18T02:10:00.000Z"
}
}4.2推送订单
/orders批量推送已锁定的订单。每次最多 200 单、请求体不超过 2 MB;每单独立处理,单个订单不合法不影响同批其它订单。按 order_id 幂等(§5)。
请求参数(body)
「必填」列:必填 缺失或不合规时该单进入 rejected;应填 缺失时仍接收,但可能在分发时转异常(如直发承运商要求电话);可选 可省略。长度与格式限制对所有出现的字段生效,超限时整单进入 rejected,原因形如 shipping.wave must be at most 64 characters。
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
batch_id | string | 应填 | 本批推送的标识,仅用于日志与排查,不参与幂等。≤ 64 字符。 |
orders | array | 必填 | 订单对象,1–200 个。 |
orders[].order_id | string | 必填 | 订单级、不可变、连接内唯一。买家升级或追加商品产生的新订单必须使用新的 order_id。≤ 64 字符。 |
orders[].pbidPledgeBox | string | 必填 | PledgeBox 展示 ID,用于对账并显示给仓库人员;多个订单可共用。≤ 64 字符。 |
orders[].platformPledgeBox | string | 应填 | 众筹平台,如 kickstarter、indiegogo。≤ 32 字符。 |
orders[].platform_backer_idPledgeBox | string | 可选 | 支持者在众筹平台上的标识。≤ 64 字符。 |
orders[].projectPledgeBox | object | 应填 | { "id", "name" }。连接配置了项目 ID 时,id 必须一致,否则整单拒收,原因 project.id does not match the project of this connection;任一方为空时不校验。id ≤ 64,name ≤ 256。 |
orders[].status | string | 必填 | 固定为 locked,表示订单已锁定、不会再修改;其它值拒收。 |
orders[].locked_atPledgeBox | string | 应填 | 锁定时间(§2.3 时间格式),作为订单在 OrangeShip 的创建时间。 |
orders[].updated_at | string | 必填 | 订单最后修改时间(§2.3 时间格式)。重推时与已存值比较,更早的推送返回 stale。 |
orders[].currency | string | 应填 | ISO 4217 币种代码,按 ISO 列表校验,不区分大小写。 |
orders[].amount | object | 应填 | { "total", "shipping", "tax" },金额字符串(§2.3)。 |
orders[].backer.emailPledgeBox | string | 应填 | 买家邮箱,加密存储,仅用于售后排查。≤ 256 字符。 |
orders[].shipping_address | object | 必填 | 收货地址,字段见下。 |
…shipping_address.name | string | 应填 | 收件人。≤ 128 字符。 |
…shipping_address.phone | string | 应填 | 收件人电话。直发承运商必须有电话,缺失的订单会在分发时转异常。≤ 64 字符。 |
…shipping_address.address1 | string | 应填 | ≤ 256 字符。 |
…shipping_address.address2 | string | null | 可选 | ≤ 256 字符。 |
…shipping_address.city | string | 应填 | ≤ 128 字符。 |
…shipping_address.province | string | null | 可选 | 州 / 省代码或名称。≤ 128 字符。 |
…shipping_address.zip | string | null | 可选 | 邮编。多数目的地的承运商要求邮编。≤ 32 字符。 |
…shipping_address.country_code | string | 必填 | ISO 3166-1 二位国家代码,按 ISO 列表校验,不区分大小写。 |
orders[].shipping | object | 可选 | { "method", "wave", "region" },自由文本,用于展示与路由(wave / region 为发货波次与地区)。method ≤ 128,wave ≤ 64,region ≤ 64。 |
orders[].items | array | 必填 | 商品行,至少 1 行。 |
…items[].line_id | string | 必填 | 订单内唯一,重推时保持不变。≤ 64 字符。 |
…items[].typePledgeBox | string | 应填 | reward / addon / bundle / gift(不校验取值)。≤ 16 字符。 |
…items[].sku | string | 必填 | 可发货的 SKU。任一行缺失时整单拒收。≤ 128 字符。 |
…items[].name | string | 应填 | 商品名称。≤ 512 字符。 |
…items[].quantity | integer | 必填 | 1–9999。 |
…items[].unit_price | string | 应填 | 单价,金额字符串(§2.3)。 |
orders[].note | string | null | 可选 | 给仓库的备注。≤ 1000 字符。 |
请求示例
POST /open/v1/orders HTTP/1.1
Host: orangeshipping.xyz
Content-Type: application/json
X-App-Key: 3f9c1d2e4b5a69788796a5b4c3d2e1f0
X-Timestamp: 1789000000
X-Signature: <按 §3.2 计算>
{
"batch_id": "wave-2-2026-09-18-001",
"orders": [
{
"order_id": "ord_8f3a1c",
"pbid": "PB123456",
"platform": "kickstarter",
"platform_backer_id": "KS-98765",
"project": { "id": "prj_001", "name": "Foldable Drone" },
"status": "locked",
"locked_at": "2026-09-18T02:10:00Z",
"updated_at": "2026-09-18T02:10:00Z",
"currency": "USD",
"amount": { "total": "129.00", "shipping": "19.00", "tax": "0.00" },
"backer": { "email": "backer@example.com" },
"shipping_address": {
"name": "Jane Doe", "phone": "+1 415 555 0100",
"address1": "1 Market St", "address2": "Suite 300",
"city": "San Francisco", "province": "CA", "zip": "94105", "country_code": "US"
},
"shipping": { "method": "Standard", "wave": "Wave 2", "region": "North America" },
"items": [
{ "line_id": "L1", "type": "reward", "sku": "DRONE-BLK", "name": "Drone (Black)", "quantity": 1, "unit_price": "99.00" },
{ "line_id": "L2", "type": "addon", "sku": "BATT-01", "name": "Extra Battery", "quantity": 2, "unit_price": "15.00" }
],
"note": "Leave at door"
}
]
}处理规则(按订单)
| 订单在 OrangeShip 的现状 | 处理 | 结果 |
|---|---|---|
| 不存在 | 创建订单。 | created |
已存在,尚未推送到仓库(pending,或未进仓的 exception) | 用新报文整单替换。 | updated |
已存在,已推送到仓库(processing,或进仓后的 exception) | 只更新备注等不影响履约的字段;金额、地址、商品行保持推仓时的内容。地址或商品行与推仓时不同时,订单转为 exception(exception_code: order_changed,已是 exception 的保持不变)由运营核对。不会再次推送到仓库。 | touched |
已存在,报文 updated_at 早于已存值 | 忽略。 | stale |
| 与此前某次推送的订单对象完全相同且已处理完 | 返回上次的结果,不再处理(§5)。 | duplicate |
| 本次请求内未处理完(临时错误,或大批次用完了 25 秒的同步处理时间) | 订单对象已保存,转为异步处理,通常一分钟内完成,结果在订单变更中可见。oms_order_id 为 null。不需要重试。 | queued |
已存在,状态为 shipped / in_transit / delivered | 不更新。 | 进入 rejected,原因 already_shipped |
已存在,状态为 cancelled | 不更新、不恢复。替换订单须使用新的 order_id(§9)。 | 进入 rejected,原因 already_cancelled |
只要请求本身合法,HTTP 状态就是 200,即使所有订单都被拒收。请逐条检查 data.rejected。
响应
| HTTP | code | 说明 |
|---|---|---|
| 200 | 0 | 请求已处理,逐单结果见 accepted / rejected。 |
| 400 | 40001 | 请求体不是合法 JSON;orders 缺失、为空、不是数组或超过 200 个;batch_id 超过 64 字符。整批不做处理。 |
| 413 | 41300 | 请求体超过 2 MB,请拆批。 |
响应字段(data)
| 字段 | 类型 | 说明 |
|---|---|---|
accepted[] | array | 被保存或匹配到的订单,每单一条。 |
accepted[].order_id | string | 回显 order_id。 |
accepted[].pbidPledgeBox | string | 回显 pbid。 |
accepted[].oms_order_id | string | null | OrangeShip 内部订单号;结果为 queued 时为 null。 |
accepted[].result | string | 处理结果,取值见 §7.3。 |
rejected[] | array | 未被接收的订单,每单一条。拒收记录(订单号、原因,不含收件人信息)在商家后台保留 90 天,供双方对账。 |
rejected[].order_id | string | null | 回显;报文没有 order_id 时为 null。 |
rejected[].pbidPledgeBox | string | null | 回显;缺失时为 null。 |
rejected[].reason | string | 拒收原因。already_shipped、already_cancelled 为固定值,可用于程序判断;其余为校验信息(如 items[0].sku is required),措辞可能调整。 |
响应示例
{
"code": 0,
"message": "ok",
"data": {
"accepted": [
{ "order_id": "ord_8f3a1c", "pbid": "PB123456", "oms_order_id": "1024", "result": "created" },
{ "order_id": "ord_8f3a1f", "pbid": "PB123459", "oms_order_id": null, "result": "queued" }
],
"rejected": [
{ "order_id": "ord_8f3a1d", "pbid": "PB123457", "reason": "items[0].sku is required" },
{ "order_id": "ord_8f3a1e", "pbid": "PB123458", "reason": "already_shipped" }
]
}
}4.3取消订单
/orders/{order_id}/cancel因退款、取消或买家修改地址而取消订单。修改地址的正确做法是取消旧订单后用新的 order_id 重新推送(§9)。
请求参数
| 参数 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
order_id | path | string | 必填 | 推单时使用的 order_id,百分号编码。 |
reason | body | string | 可选 | 取消原因,≤ 256 字符,展示给运营。建议取值:refunded、cancelled_by_customer、address_changed、fraud。无原因时请求体发 {}。 |
请求示例
POST /open/v1/orders/ord_8f3a1c/cancel HTTP/1.1
Host: orangeshipping.xyz
Content-Type: application/json
X-App-Key: 3f9c1d2e4b5a69788796a5b4c3d2e1f0
X-Timestamp: 1789000000
X-Signature: <按 §3.2 计算>
{ "reason": "refunded" }处理规则
| 订单现状 | 处理 | 结果 |
|---|---|---|
pending,或未进仓的 exception | 取消订单,订单变更中出现 cancelled。例外:正在推送到仓库途中的订单无法安全撤回,按下一行处理。 | cancelled |
processing,或进仓后的 exception | 订单转为 exception(exception_code: cancel_intercept),由运营到仓库拦截。拦截成功后变为 cancelled;包裹已发出则继续变为 shipped。 | exception |
| 已在拦截中(此前已取消过,原因不同) | 不做改变,保留第一次的原因。 | noop |
已 cancelled(原因与此前不同) | 不做改变。 | noop |
| 与此前对该单的取消原因相同 | 不做改变,也不重新判断(永久去重,§5)。 | duplicate |
shipped / in_transit / delivered | 不取消。 | already_shipped |
| 订单推送仍在排队(尚未建单) | 取消请求已保存,建单后立即执行,该订单不会发往仓库。 | queued |
| 其它状态但本次请求内未能执行 | 取消请求已保存,转为异步执行(通常一分钟内),结果在订单变更中可见。 | queued |
响应
| HTTP | code | 说明 |
|---|---|---|
| 200 | 0 | 已处理,结果见 result(包括 already_shipped,不作为错误返回)。 |
| 400 | 40001 | 请求体不是 JSON 对象,或 reason 不是字符串、超长。 |
| 404 | 40401 | 本连接内没有该订单,也没有排队中的推送(属于其它连接的订单同样返回 404)。 |
响应字段(data)
| 字段 | 类型 | 说明 |
|---|---|---|
order_id | string | 回显路径参数。 |
oms_order_id | string | null | OrangeShip 内部订单号;订单尚未建出时为 null。 |
result | string | 处理结果,取值见 §7.4。 |
响应示例
{ "code": 0, "message": "ok", "data": { "order_id": "ord_8f3a1c", "oms_order_id": "1024", "result": "cancelled" } }4.4拉取订单变更
/orders/changes按游标增量返回对外可见状态发生变化的订单,是取回运单号的主通道。每个订单只返回最新状态。拉取协议与客户端处理要求见 §6。
请求参数(query)
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
cursor | string | 可选 | 上一次响应的 next_cursor。仅首次调用可省略。 |
since | string | 可选 | 仅在不带 cursor 时生效:从该时间及之后的变更开始。格式为日期(2026-09-18,按 UTC 零点)或带时区的日期时间。省略时从该连接的第一笔订单开始。 |
limit | integer | 可选 | 每页行数,1–200,默认 100。 |
请求示例
GET /open/v1/orders/changes?cursor=eyJ0IjoiMjAyNi0wOS0yMFQwODowMDowNS4wMDBaIiwiaWQiOiIxMDI0In0&limit=100 HTTP/1.1
Host: orangeshipping.xyz
X-App-Key: 3f9c1d2e4b5a69788796a5b4c3d2e1f0
X-Timestamp: 1789000000
X-Signature: <按 §3.2 计算>响应
| HTTP | code | 说明 |
|---|---|---|
| 200 | 0 | 成功(orders 可能为空)。 |
| 400 | 40001 | cursor 格式错误或越界、since 格式错误、limit 不在 1–200。错误的游标不会被自动重置。 |
响应字段(data)
| 字段 | 类型 | 说明 |
|---|---|---|
orders[] | array | 变更行,按 changed_at 升序;同一页中每个订单最多一行。 |
orders[].order_id | string | 订单号。 |
orders[].pbidPledgeBox | string | PledgeBox 展示 ID。 |
orders[].status | string | 订单状态,取值见 §7.1。 |
orders[].tracking_code | string | null | 运单号。状态为 shipped、in_transit 时一定有值;已出库但运单号未到的订单报 processing。 |
orders[].carrier | string | null | 承运商名称,如 UPS。 |
orders[].carrier_code | string | null | 履约系统的服务代码,仅供参考。 |
orders[].courier_code | string | null | 在渠道系统中标记发货时使用的承运商代码,见 §7.5。 |
orders[].shipped_at | string | null | 包裹交给承运商的时间。 |
orders[].delivered_at | string | null | 签收时间,与 delivered 状态一起出现。 |
orders[].exception_code | string | null | 异常代码,状态为 exception 时有值,取值见 §7.2。 |
orders[].exception_reason | string | null | 异常说明,供展示给商家,措辞与语言可能变化,不要用于程序判断。 |
orders[].changed_at | string | 最近一次对外可见变化的时间。 |
next_cursor | string | 下次调用使用的游标,总会返回(包括空页)。 |
has_more | boolean | true 时立即用 next_cursor 继续拉取。 |
server_time | string | 服务器时间。晚于 server_time − 60 s 的变更暂不返回(§6.1)。 |
响应示例
{
"code": 0,
"message": "ok",
"data": {
"orders": [
{
"order_id": "ord_8f3a1e", "pbid": "PB123458", "status": "exception",
"tracking_code": null, "carrier": null, "carrier_code": null, "courier_code": null,
"shipped_at": null, "delivered_at": null,
"exception_code": "sku_unmapped", "exception_reason": "SKU mapping missing: DRONE-RED",
"changed_at": "2026-09-19T10:00:00.000Z"
},
{
"order_id": "ord_8f3a1c", "pbid": "PB123456", "status": "shipped",
"tracking_code": "1Z999AA10123456784", "carrier": "UPS", "carrier_code": "UPS-EXP", "courier_code": "UPS",
"shipped_at": "2026-09-20T08:00:00.000Z", "delivered_at": null,
"exception_code": null, "exception_reason": null,
"changed_at": "2026-09-20T08:00:05.000Z"
}
],
"next_cursor": "eyJ0IjoiMjAyNi0wOS0yMFQwODowMDowNS4wMDBaIiwiaWQiOiIxMDI0In0",
"has_more": false,
"server_time": "2026-09-21T03:10:00.000Z"
}
}4.5查询订单
/orders/{order_id}返回单笔订单快照:包含订单变更行的全部字段,另加仓库参考号与商品行。不返回收件人个人信息(姓名、地址、电话、邮箱)。
请求参数
| 参数 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
order_id | path | string | 必填 | 推单时使用的 order_id,百分号编码。 |
响应
| HTTP | code | 说明 |
|---|---|---|
| 200 | 0 | 成功。 |
| 404 | 40401 | 本连接内没有该订单。 |
响应字段(data)
包含 §4.4 orders[] 的全部字段,另加:
| 字段 | 类型 | 说明 |
|---|---|---|
reference_number | string | null | OrangeShip 参考号,印在仓库单据上。一般与 pbid 相同;pbid 不符合仓库或物流系统的单号规则时,为 OrangeShip 生成的编号。 |
external_number | string | null | 推仓后,仓库或承运商系统中的单号。 |
items[].line_id | string | 商品行 ID。 |
items[].sku | string | SKU。 |
items[].quantity | integer | 数量。 |
items[].mapping_ok | boolean | SKU 是否已在 OrangeShip 映射到仓库货品。任一行为 false 的订单停在 exception(sku_unmapped),由商家补映射后自动继续,渠道方无需操作。 |
locked_at | string | null | 推单时的 locked_at;未传为 null。 |
updated_at | string | 最近一次对外可见变化的时间,与 changed_at 相同;不是推单时的 updated_at。 |
响应示例
{
"code": 0,
"message": "ok",
"data": {
"order_id": "ord_8f3a1c", "pbid": "PB123456", "status": "shipped",
"tracking_code": "1Z999AA10123456784", "carrier": "UPS", "carrier_code": "UPS-EXP", "courier_code": "UPS",
"shipped_at": "2026-09-20T08:00:00.000Z", "delivered_at": null,
"exception_code": null, "exception_reason": null,
"changed_at": "2026-09-20T08:00:05.000Z",
"reference_number": "PB123456",
"external_number": "WH2609200000456",
"items": [
{ "line_id": "L1", "sku": "DRONE-BLK", "quantity": 1, "mapping_ok": true },
{ "line_id": "L2", "sku": "BATT-01", "quantity": 2, "mapping_ok": true }
],
"locked_at": "2026-09-18T02:10:00.000Z",
"updated_at": "2026-09-20T08:00:05.000Z"
}
}4.6查询物流轨迹
/orders/{order_id}/tracking返回单笔订单的物流事件。请在买家查看物流时按需调用,不要定时全量轮询;签收状态会通过订单变更返回。
海外仓发货的订单只有里程碑事件(shipped、delivered)。直发订单另有承运商的转运事件:对已发货、未签收的直发订单,调用时若距上次成功刷新超过 30 分钟,OrangeShip 会先向承运商刷新再返回(刷新最多等待数秒,失败时返回已有事件,5 分钟后才会再次尝试);刷新到签收会同时将订单置为 delivered。订单还没有任何事件时,按订单的发货、签收时间生成里程碑,source 为 oms。
请求参数
| 参数 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
order_id | path | string | 必填 | 推单时使用的 order_id,百分号编码。 |
响应
| HTTP | code | 说明 |
|---|---|---|
| 200 | 0 | 成功。未发货的订单同样返回 200,tracking_code 为 null,events 为空数组。 |
| 404 | 40401 | 本连接内没有该订单。 |
响应字段(data)
| 字段 | 类型 | 说明 |
|---|---|---|
order_id 等 | — | order_id、pbid、tracking_code、carrier、carrier_code、courier_code、shipped_at、delivered_at 同 §4.4。 |
status | string | 订单状态,推导方式同订单变更(§7.1)。 |
events[] | array | 物流事件,按 occurred_at 从新到旧;重复通知已去重。 |
events[].status | string | 归一化状态:shipped、in_transit、delivered、cancelled 或 exception。 |
events[].raw_status | string | null | 履约系统的原始状态码,不要解释其含义。 |
events[].description | string | null | 事件描述(承运商语言,通常为英文)。 |
events[].location | string | null | 事件地点。 |
events[].occurred_at | string | 事件时间;来源未提供时为 OrangeShip 收到的时间。 |
events[].source | string | 事件来源,取值见 §7.6。 |
refreshed_at | string | null | 最近一次向承运商刷新的时间;从未刷新过(包括海外仓订单)为 null。 |
响应示例
{
"code": 0,
"message": "ok",
"data": {
"order_id": "ord_8f3a1c", "pbid": "PB123456",
"status": "in_transit",
"tracking_code": "1Z999AA10123456784", "carrier": "UPS", "carrier_code": "UPS-EXP", "courier_code": "UPS",
"shipped_at": "2026-09-20T08:00:00.000Z", "delivered_at": null,
"events": [
{ "status": "in_transit", "raw_status": "NT", "description": "Departed facility", "location": "Shenzhen", "occurred_at": "2026-09-21T02:00:00.000Z", "source": "carrier" },
{ "status": "shipped", "raw_status": "PU", "description": null, "location": null, "occurred_at": "2026-09-20T08:00:00.000Z", "source": "carrier" }
],
"refreshed_at": "2026-09-21T03:10:00.000Z"
}
}05幂等与重试
| 操作 | 幂等键 | 重复请求的处理 |
|---|---|---|
POST /orders(订单) | 连接 + order_id | 同一 order_id 再次推送按 §4.2 的规则更新,不会产生重复订单。 |
POST /orders(报文) | 连接 + order_id + 订单对象的规范化 JSON(键递归排序,键顺序与空白不影响) | 完全相同的订单对象不重复处理:已处理完的返回上次结果(已接收的为 duplicate,拒收的仍在 rejected);仍在异步处理的返回 queued。校验不通过的订单不会被保存,重推会再次校验。 |
POST /orders/{order_id}/cancel | 连接 + order_id + 去除首尾空白后的 reason(缺失、null、空白视为同一值) | 相同原因的取消不重复处理,永久返回 duplicate。 |
GET 接口 | — | 只读,可任意重试。 |
5.1重试策略
- 超时、HTTP 5xx:用同一串请求体字节重试(重新生成时间戳与签名),指数退避,从 1 秒开始,最多 5 次。
- HTTP 429:至少等待 1 秒后指数退避重试。
41102:核对时钟后换新时间戳重试。40001、41101、41103、41104、41300:修正请求后再发,原样重试没有意义。- 客户端超时请设为 60 秒以上。推送订单通常在 30 秒内返回:请求内同步处理最多 25 秒,未处理到的订单返回
queued后异步完成。超时后重试时,已保存的订单返回duplicate或queued,其余正常处理。
5.2顺序保证
对已存在的订单,推送的 updated_at 与已存值比较:严格更早的推送返回 stale 并被忽略,乱序到达的旧报文不会覆盖新报文。updated_at 相等时正常处理。
06订单变更拉取
6.1规则
- 触发条件。订单的
changed_at只在对外可见的内容变化时更新:状态、运单号、承运商、发货时间、签收时间、异常信息,或新增物流事件。渠道方自己的重推若没有改变这些内容,不会出现在变更中;新建的订单会出现一次,状态为拉取时已到达的状态。 - 排序与游标。结果按
(changed_at, 内部 id)升序。游标记录最后一行的位置,下次返回其后的行。游标不透明,不要解析或自行构造;游标不会过期。 - 安全滞后。
changed_at晚于server_time − 60 秒的变更暂不返回,以免并发提交的变更落在已推进的游标之前。因此变化最多约一分钟后可见。 - 只返回最新状态。两次拉取之间发生多次变化的订单只返回一行,可能跳过中间状态(例如
processing之后直接是delivered)。同一订单可能再次出现(例如商家修正承运商映射后重新下发),请按order_id更新而不是新增。 - 已取走标记。返回过的、带运单号的已发货订单,会在响应发出时被记为「已取走」,除非订单再次变化,不会重复返回。请持久化并处理完整页后再拉取下一页。
6.2拉取频率
每个连接每 15–60 分钟拉取一次;has_more 为 true 时立即继续,直到为 false。
6.3客户端处理
function pull(conn):
loop:
resp = GET /orders/changes?limit=200 + (conn.cursor ? &cursor=conn.cursor : "")
if resp 不是 HTTP 200 / code 0:
按 §8.2 处理;return # 保留原游标,不推进
for row in resp.data.orders:
apply(row) # 必须幂等
conn.cursor = resp.data.next_cursor # 整页处理完后再持久化(存数据库)
if not resp.data.has_more: break
function apply(row):
order = 按 row.order_id 查本地订单;不存在则记录并跳过
if row.changed_at <= order.last_changed_at: return # 已处理过
switch row.status:
shipped / in_transit / delivered:
if row.tracking_code 与本地不同:
标记已发货(tracking_code、courier_code);通知买家 # 仅首次拿到运单号时通知
if row.status == delivered: 标记已签收
exception: 提示商家(按 exception_code 分类,exception_reason 用于展示)
cancelled: 标记已取消
pending / processing: 标记履约中(可选)
order.last_changed_at = row.changed_at- 收到错误响应时不推进游标;处理完整页后再保存
next_cursor,空页也要保存。 - 一个连接一个游标。
- 订单首次出现时可能是任意状态,不要假设从
pending开始。 - 游标丢失时,不带
cursor和since重新从头拉取;数据量大时可用since指定一个明显早于丢失时刻的时间(留出几分钟时钟偏差余量)。重新收到的行按幂等规则处理。
07数据字典
7.1订单状态 status
| 取值 | 含义 | 可能的下一状态 |
|---|---|---|
pending | 已接收,等待推送到仓库(SKU 映射与分仓处理中)。 | processing、exception、cancelled |
processing | 已推送到仓库或承运商,等待拣货、打包、出面单;或已出库但运单号尚未返回。 | shipped、exception |
shipped | 已交付承运商,运单号已分配。 | in_transit、delivered |
in_transit | 最近一条物流事件是承运商的转运扫描,尚未签收。海外仓订单没有此状态。 | delivered |
delivered | 已签收。终态。 | — |
cancelled | 发货前已取消(取消接口或运营操作)。终态。 | — |
exception | 需要处理,原因见 exception_code。 | pending、processing、shipped、cancelled |
同一状态可能多次出现(例如商家修正映射期间 exception → pending → exception),每次有新的 changed_at。
7.2异常代码 exception_code
| 取值 | 含义 | 渠道方处理 |
|---|---|---|
sku_unmapped | 部分 SKU 尚未映射到仓库货品。 | 无需操作,商家补映射后自动继续。 |
unfulfillable_line | 有商品行仓库无法发货。 | 联系商家。 |
filing_required | 商品需先在仓库建档。 | 无需操作,由商家处理。 |
cancel_intercept | 已进仓的订单被取消,运营拦截中。 | 无需操作,之后变为 cancelled,或已发出时变为 shipped。 |
order_changed | 已进仓的订单被重推修改了地址或商品,运营核对中。 | 无需操作;改地址请按 §9 取消后用新 order_id 推送。 |
route_conflict | 订单内商品的发货地不一致,需人工决定。 | 无需操作,由商家处理。 |
address_incomplete | 地址信息不满足仓库或承运商要求。 | 修正地址:取消后用新 order_id 重新推送。 |
push_failed | 仓库或承运商拒绝了订单。 | 无需操作,商家要求时配合。 |
other | 其它原因。 | 把 exception_reason 展示给商家。 |
v1 内可能新增异常代码,客户端遇到未知值时按 other 处理。
7.3推单结果
| 位置 | 取值 | 含义 |
|---|---|---|
accepted[].result | created | 新建订单。 |
updated | 未进仓的订单已整单替换。 | |
touched | 已进仓的订单,仅更新备注等字段。 | |
stale | 报文比已存的旧,已忽略。 | |
duplicate | 与已处理的报文相同,返回上次结果。 | |
queued | 已保存,异步处理中。 | |
rejected[].reason | already_shipped | 订单已发货,不再更新。 |
already_cancelled | 订单已取消,不再更新。 | |
| 其它文本 | 校验失败原因,如 items[0].sku is required、status must be locked。以 order could not be stored 开头表示报文无法保存,需修正后重推。 |
7.4取消结果 result
| 取值 | 含义 |
|---|---|
cancelled | 已取消。 |
exception | 订单已进仓,转人工拦截。 |
already_shipped | 已发货,无法取消。 |
noop | 无需变更(已取消或已在拦截中)。 |
duplicate | 与此前的取消请求相同。 |
queued | 已保存,异步执行。 |
status、推单结果与取消结果在 v1 内为封闭集合,新增取值会提前通知(§10)。
7.5承运商字段
| 字段 | 来源 | 用途 |
|---|---|---|
carrier | OrangeShip 中的承运商名称,如 UPS、YunExpress。 | 展示。 |
carrier_code | 履约系统的服务或渠道代码。 | 参考,提交工单时有用。 |
courier_code | 按商家维护的连接级映射表转换:先按 carrier 查,再按 carrier_code 查;都没有映射时等于 carrier(承运商名称未知时为 null)。 | 在渠道系统中标记发货时填写的承运商代码。 |
如果渠道系统要求承运商代码来自固定列表,请提供列表,商家会在 OrangeShip 后台维护映射。
7.6物流事件来源 events[].source
| 取值 | 含义 |
|---|---|
warehouse | 海外仓系统上报。 |
carrier | 承运商系统上报。 |
oms | OrangeShip 根据订单的发货、签收时间生成的里程碑。 |
遇到未知值时按 carrier 处理。
08错误处理与限流
8.1错误响应
{ "code": 41101, "message": "signature invalid", "data": null, "requestId": "c1f4…" }请按 code 判断,不要依赖 message 文本。联系技术支持时请提供 requestId。
8.2错误码
| code | HTTP | 名称 | 说明 | 处理 |
|---|---|---|---|---|
0 | 200 | OK | 成功。推单中单个订单的问题在 data.rejected 中返回。 | — |
40001 | 400 | InvalidParam | 请求格式或参数不合法。 | 修正请求,不要原样重试。 |
40401 | 404 | NotFound | 本连接内没有该订单。 | 核对 order_id。 |
41101 | 401 | SignatureInvalid | 签名不匹配(POST 缺少 Content-Type: application/json 也会导致)。 | 按 §8.4 排查。 |
41102 | 401 | TimestampExpired | X-Timestamp 缺失、不是整数或偏差超过 300 秒。 | 同步时钟后换新时间戳重试。 |
41103 | 403 | ConnectionDisabled | 连接已被商家停用。只对推单、取消返回,查询接口仍可用。 | 停止推单与取消,继续拉取变更直到在途订单取回;联系商家启用或更换凭证。 |
41104 | 401 | AppKeyUnknown | X-App-Key 缺失或不存在。 | 检查凭证。 |
41300 | 413 | PayloadTooLarge | 请求体超过 2 MB。 | 拆批后重发。 |
42901 | 429 | RateLimited | 超过连接限流,或来源 IP 鉴权失败过多(§8.3)。 | 退避后重试;如同时鉴权失败,先修正签名。 |
50000 | 500 | Internal | 服务端错误。 | 退避重试(写接口幂等);持续出现请联系技术支持。 |
超过 5 MB 的请求会被网关直接拒绝(HTTP 413,无 JSON 响应体);HTTP 502 / 503 / 504 也可能没有 JSON 响应体,可退避重试。
8.3限流
| 分组 | 接口 | 限额 |
|---|---|---|
| 写入 | POST /orders、POST /orders/{order_id}/cancel | 每个连接每分钟 600 次 |
| 查询 | GET /ping、GET /orders/changes、GET /orders/{order_id}、GET /orders/{order_id}/tracking | 每个连接每分钟 120 次 |
| 鉴权失败 | 全部 | 同一来源 IP 每分钟鉴权失败超过 60 次后,该分钟内再失败的请求返回 429;签名正确的请求不受影响 |
限流在签名校验通过后按连接计数,不按来源 IP,多个连接共用出口 IP 时互不影响。其它限制:请求体 ≤ 2 MB,每次推送 ≤ 200 单,每页变更 ≤ 200 行。
8.4签名失败排查
- 用 §3.4 的测试向量离线验证签名函数。
- 确认
path以/open/v1开头且不含 query string。 - 确认签名的是实际发送的字节,HTTP 库没有重新序列化请求体。
- 确认方法大写、十六进制小写、
X-Timestamp为秒。 - 路径中的
order_id已百分号编码后再签名。 - POST 请求带有
Content-Type: application/json。
09对接要求
渠道方的实现须满足以下要求。
- 只推送已锁定、不会再修改的订单。推送即进入履约。
- 修改地址 = 取消旧订单 + 新
order_id推送。不要用同一个order_id推送改过地址的订单:已进仓的订单地址不会更新,已取消的order_id会被拒收。 order_id订单级且不可变。升级、追加商品、拆单产生的新发货都是新订单。- 每个商品行都是可发货的 SKU。组合商品推送前拆成最小可发货 SKU;尺码、颜色等选项编入 SKU 本身(如
TEE-BLK-L)。同一个 SKU 在商家的所有店铺 / 项目中必须代表同一件实物:OrangeShip 按商家维度把 SKU 映射到仓库货品。 - 退款和取消都调用取消接口,且越早越好。订单进入
processing后需人工拦截,shipped后无法拦截。 - 按 §6 拉取订单变更,每 15–60 分钟一次;拿到运单号后在渠道系统标记发货并通知买家。
- 物流轨迹按需查询,不要定时全量轮询。
- 一单一包裹。v1 不支持多包裹,仓库拆包时只返回第一个运单号。
10版本管理
- 版本号在路径中(
/open/v1)。不兼容的变更会使用新的路径(/open/v2),v2 发布后 v1 至少继续运行 6 个月。 - v1 内只做兼容的新增:新增可选请求字段、新增响应字段、新增接口、新增枚举取值(至少提前两周通知)。客户端必须忽略未知字段。
- v1 内不会重命名或删除字段、修改字段类型、修改签名算法、破坏游标兼容,也不会收紧已接受输入的校验。
- 变更会在上线前通知各渠道方的对接人。
10.1变更记录
| 日期 | 变更 |
|---|---|
| 2026-09-29 | Base URL 改为 https://orangeshipping.xyz/open/v1(原地址仍可访问,建议迁移);测试向量随路径与演示密钥更新。文档按接口参考格式重排,接口行为不变。 |
| 2026-09-28 | 冻结前收紧输入校验:时间必须带时区、国家与币种按 ISO 列表校验、数量 1–9999、空串按未传处理;新增 exception_code;物流事件 source 改为系统类别;连接停用后仍可查询;限流改为签名通过后按连接计数。 |
| 2026-09-19 | v1 草案首次发布。 |