Skip to content

包裹状态回调(Webhook)

接口概述

每当包裹状态发生变化,WMG 会把新状态推送到你登记的 URL。这与本文档中其他所有接口方向相反:WMG 是客户端,你的端点是服务端

推送使用 HMAC-SHA256 签名,便于你验证请求确实来自 WMG。你的端点必须以 {"success": true} 确认每次推送;返回其他内容都会被判定为投递失败,该事件之后会被重新推送。

NOTE

回调 URL 和 Webhook 密钥在后台配置,位置见下图。

回调 URL 与 Webhook 密钥的配置位置

请求信息

  • Method: POST
  • Path: 你登记的回调 URL
  • Authentication: HMAC-SHA256 签名(x-wmg-hmac-sha256 头)
  • 方向: WMG → 你的端点

请求头

字段说明
Content-Typeapplication/json
x-wmg-timestamp生成本次推送时的 UNIX 时间戳(10 位,秒)。以字符串形式发送,并以字符串参与签名。
x-wmg-hmac-sha256请求体的 HMAC-SHA256 签名,Base64 编码,见 认证方式

请求参数

请求体为 JSON 格式,包含以下字段:

参数类型必填说明
tracking_numstring包裹的 WMG 运单号。
custom_tracking_numstring你自己的包裹跟踪号。包裹没有时为空字符串。
statusstring状态码,如 DR_DROK_OK——见 物流状态码
remarkstring该状态码的标准描述文本。该状态码没有文本时为空字符串。
status_timeinteger状态事件本身发生的 UNIX 时间戳(秒)。以数字发送,不是字符串。
filesarray<string>该事件附带文件的完整 http/https URL,例如 ePOD 照片。没有时为空数组。

请求体示例

json
{
    "tracking_num": "CY180000662SG",
    "custom_tracking_num": "CUST0001234567",
    "status": "OK_OK",
    "remark": "Item has been delivered",
    "status_time": 1784128800,
    "files": [
        "https://files.wmgdelivery.com/epod/CY180000662SG-1.jpg"
    ]
}

签名串示例

参与签名的字符串是「请求体字段 + 以 x-wmg 开头的请求头(排除 x-wmg-hmac-sha256)」合并后按键名升序排序,再按 PHP 默认转义规则序列化为紧凑 JSON——正斜杠转义为 \/,非 ASCII 字符转义为 \uXXXX

{"custom_tracking_num":"CUST0001234567","files":["https:\/\/files.wmgdelivery.com\/epod\/CY180000662SG-1.jpg"],"remark":"Item has been delivered","status":"OK_OK","status_time":1784128800,"tracking_num":"CY180000662SG","x-wmg-timestamp":"1784128805"}

注意两种不同的值类型:status_time 是不带引号的整数,而 x-wmg-timestamp 是带引号的字符串。

认证方式

每次推送都带有签名,用你的 Webhook 密钥验证:

Signature = Base64( HMAC-SHA256( JSON String, Secret Key ) )

消息在前、密钥在后,与 PHP hash_hmac($algo, $data, $key) 的参数顺序一致。

重建该 JSON 字符串的步骤:

  1. 取请求体的全部字段。
  2. 加入所有以 x-wmg 开头的请求头,排除 x-wmg-hmac-sha256。实际上就是 x-wmg-timestamp,其值以字符串参与签名。
  3. 对合并后的映射按键名升序排序(ASCII 顺序)。只排顶层。
  4. 序列化为紧凑 JSON,正斜杠转义为 \/非 ASCII 转义为 \uXXXX
  5. 用你的密钥对该字符串计算 HMAC-SHA256,取原始二进制摘要,再做 Base64 编码。
  6. 用恒定时间比较把结果与 x-wmg-hmac-sha256 对比。

转义规则不是各语言的默认行为

第 4 步对应的是 PHP json_encode() 不带任何 JSON_UNESCAPED_* 标志的行为。多数其他语言默认恰好相反:Python 和 JavaScript 都不转义正斜杠,JavaScript 连非 ASCII 也不转义。由于 files 里始终是 URL,只要漏了 / 的转义,签名必然对不上。下面的示例都显式做了这两种转义。

响应信息

你的端点返回的响应为 JSON 格式。

响应格式

字段类型说明
successbooleantrue 表示已接收并处理成功。其他值或缺失该字段都算失败。
msgstring可选。失败时该文本会被 WMG 记录为失败原因。也接受 message 作为别名。

成功响应

  • Status Code: 200
json
{
    "success": true
}

错误响应

返回不含 success: true 的响应体。请附上原因,便于在 WMG 的投递日志中排查:

json
{
    "success": false,
    "msg": "signature mismatch"
}

结果码

本页描述的是推送给你的请求,因此标准的 {Code, Message, Data} 结构不适用。WMG 根据你响应体中的 success 字段判定结果:

success说明
true投递成功,该事件不会再次推送。
其他值或缺失投递失败,该事件会进入重推队列。

示例

以下示例用于验证收到的推送。body 是你收到的原始请求体timestampx-wmg-timestamp 头的值。

Bash

bash
SECRET="your_webhook_secret"
TS="1784128805"                       # x-wmg-timestamp 头
RECEIVED="signature_from_x_wmg_hmac_sha256_header"

# jq -S 按键排序,-c 紧凑输出,-a 把非 ASCII 转义为 \uXXXX;
# 随后用 sed 转义正斜杠,PHP 的 json_encode() 同样会转义它。
SIGNING_JSON=$(jq -acS --arg ts "$TS" '. + {"x-wmg-timestamp":$ts}' body.json | sed 's|/|\\/|g')

EXPECTED=$(printf '%s' "$SIGNING_JSON" \
  | openssl dgst -sha256 -hmac "$SECRET" -binary | base64)

[ "$EXPECTED" = "$RECEIVED" ] && echo "signature ok" || echo "signature mismatch"

Windows PowerShell

powershell
$SECRET   = "your_webhook_secret"
$TS       = "1784128805"              # x-wmg-timestamp 头
$RECEIVED = "signature_from_x_wmg_hmac_sha256_header"

$payload = Get-Content body.json -Raw | ConvertFrom-Json -AsHashtable
$payload["x-wmg-timestamp"] = $TS

# 手工拼接 JSON:整个映射过 ConvertTo-Json 会打乱键顺序并加空格,
# 但对单个值调用它能得到正确的转义结果。
$parts = foreach ($key in ($payload.Keys | Sort-Object)) {
    "`"$key`":$($payload[$key] | ConvertTo-Json -Compress)"
}
$signingJson = "{$($parts -join ',')}"

# PHP 的 json_encode() 会转义正斜杠和非 ASCII,PowerShell 两者都不转义。
$signingJson = $signingJson -replace '/', '\/'
$signingJson = [regex]::Replace($signingJson, '[^\x00-\x7F]', {
    param($m) '\u{0:x4}' -f [int][char]$m.Value
})

$hmac = New-Object System.Security.Cryptography.HMACSHA256
$hmac.Key = [System.Text.Encoding]::UTF8.GetBytes($SECRET)
$expected = [Convert]::ToBase64String($hmac.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($signingJson)))

if ($expected -ceq $RECEIVED) { "signature ok" } else { "signature mismatch" }

Python

python
import base64
import hashlib
import hmac
import json


def signing_string(payload: dict, timestamp: str) -> str:
    combined = {**payload, "x-wmg-timestamp": str(timestamp)}
    # ensure_ascii=True(默认值)与 PHP 的 \uXXXX 转义一致;
    # replace() 补上 PHP 会做、而 Python 不做的正斜杠转义。
    encoded = json.dumps(dict(sorted(combined.items())), separators=(",", ":"))
    return encoded.replace("/", "\\/")


def verify(raw_body: bytes, timestamp: str, received_signature: str, secret: str) -> bool:
    message = signing_string(json.loads(raw_body), timestamp).encode()
    expected = base64.b64encode(
        hmac.new(secret.encode(), message, hashlib.sha256).digest()
    ).decode()
    return hmac.compare_digest(expected, received_signature)

Node.js / TypeScript

typescript
import {createHmac, timingSafeEqual} from "node:crypto";

function signingString(payload: Record<string, unknown>, timestamp: string): string {
  const combined: Record<string, unknown> = {...payload, "x-wmg-timestamp": String(timestamp)};
  const sorted = Object.keys(combined).sort()
    .reduce<Record<string, unknown>>((acc, k) => (acc[k] = combined[k], acc), {});

  // PHP 的 json_encode() 会转义正斜杠和非 ASCII,JSON.stringify 两者都不做,
  // 因此这里显式补上。先转义斜杠——\uXXXX 那一步不会引入新的斜杠。
  return JSON.stringify(sorted)
    .replace(/\//g, "\\/")
    .replace(/[^\x00-\x7F]/g, (c) =>
      "\\u" + c.charCodeAt(0).toString(16).padStart(4, "0"));
}

export function verify(rawBody: string, timestamp: string,
                       receivedSignature: string, secret: string): boolean {
  const message = signingString(JSON.parse(rawBody), timestamp);
  const expected = createHmac("sha256", secret).update(message).digest("base64");
  const a = Buffer.from(expected);
  const b = Buffer.from(receivedSignature);
  return a.length === b.length && timingSafeEqual(a, b);
}

PHP

php
<?php
function build_signature(string $secret, array $data, array $headers = [], string $algo = 'sha256'): string
{
    $header_data = [];
    foreach ($headers as $k => $v) {
        $k = strtolower($k);
        // 只取以 'x-wmg' 开头的头,并排除签名头本身
        if (str_starts_with($k, 'x-wmg') && 'x-wmg-hmac-sha256' !== $k) {
            $header_data[$k] = (string) $v;
        }
    }

    $d = [...$data, ...$header_data];
    ksort($d);

    // 这里不要加 JSON_UNESCAPED_* 标志:发送方用的是 PHP 默认转义,
    // 因此斜杠保持 \/,非 ASCII 保持 \uXXXX。
    $str = json_encode($d);

    return base64_encode(hash_hmac($algo, $str, $secret, true));
}

$raw_body = file_get_contents('php://input');
$payload  = json_decode($raw_body, true);
$received = $_SERVER['HTTP_X_WMG_HMAC_SHA256'] ?? '';

$expected = build_signature('your_webhook_secret', $payload, [
    'x-wmg-timestamp' => $_SERVER['HTTP_X_WMG_TIMESTAMP'] ?? '',
]);

if (!hash_equals($expected, $received)) {
    http_response_code(200);
    echo json_encode(['success' => false, 'msg' => 'signature mismatch']);
    exit;
}

// ... 以 tracking_num + status + status_time 为键做幂等处理 ...

echo json_encode(['success' => true]);
?>

注意事项

  • 你的端点必须是幂等的。 投递失败的事件会被定时任务重新入队并重试,重试携带的是同一个事件。请按 tracking_num + status + status_time 去重,不要假设每次推送都是新事件。
  • 判定结果只看响应体,HTTP 状态码会被忽略。 返回 200 但没有 success: true 会被记为失败并重试;返回 500 但响应体含 success: true 会被记为投递成功。请始终显式返回 JSON 确认。
  • 投递失败会一直重试到成功为止。契约中没有固定的重试间隔,也没有明确的重试次数上限,因此一个永不确认的端点会持续收到同样的事件。
  • 并非所有内部状态都会推送。只有出现在 物流状态码 中的状态码会被推送;仅内部使用的状态变更会被静默跳过,既不推送也不重试。
  • 只有在你账号的回调方式设为 webhook 且已配置回调 URL 时才会推送。任一项被清空,推送即停止。
  • status_time 是状态事件发生的时间;x-wmg-timestamp 是生成本次推送的时间。两者不同,重试时时间戳头会更新,而 status_time 保持不变。
  • 如需防重放,请自行校验时效:WMG 不会在你这一侧强制时间窗口。请把 x-wmg-timestamp 与你自己的时钟比较,并为旧事件的重试留出足够宽的容差。
  • 只对映射的顶层排序。files 保持 WMG 发送时的顺序——重排其元素会改变签名。
  • 请在框架重新序列化之前读取原始请求体,并按示例用解析后的值重建签名串。不要直接对原始请求体签名:签名覆盖的是「请求体 + 时间戳头」的合并结果。
  • remark 是状态码的标准描述,不是逐条事件录入的自由文本备注。
  • 所有字段名和取值都区分大小写。

错误码

以下是 WMG 在投递日志中为一次推送记录的原因。它们来自你的响应,因此请用 msg 给出有意义的说明:

successmsg说明
false你的端点拒绝了该事件。你放在 msg(或 message)里的内容会被存为失败原因。
缺失fail你的响应没有 success 字段,或不是合法 JSON。记录为 fail
请求未能完成——DNS 失败、TLS 错误、连接被拒、超时等。传输错误文本会被记录,该事件会重试。