购物车一键导入 · 通用对接规范
版本 v1 · 2026-09-24 · 发起方:QCradar(qcradar.com)
一句话:用户在 QCradar(或任何实现了本规范的工具)把多件商品加入购物车,点一下「导入到 {你的平台}」,浏览器落到你们的购物车时商品已全部加好、规格已预选;用户在你们站内自己登录、自己结算。
你们只需要实现一个带签名的接口:建立导入会话,返回一条跳转链接。本规范已在生产环境与多家代购平台跑通,QCradar 侧客户端代码现成,你们按本文实现完成后,1~2 个工作日即可联调上线。
已有自己的购物车导入 / 开放接口?直接把文档发给我们(点击联系),我们按你们的契约适配。本文是我们建议的最省事方案。
为什么要做这件事、QCradar 怎样推荐代购平台,见致各代购平台的公开信。
0. 闭环模型(先看这个)
用户在 QCradar 购物车勾选商品,点「导入 {平台}」
│
▼
QCradar 服务端
│ 1) 组装 items(货源平台 + 商品 ID + 规格 + 数量)
│ 2) 用 secretKey 做 HMAC-SHA256 签名
│ 3) POST {你们的网关}/open/cart/import-session/create
▼
你们的网关
│ 校验签名 → 把这批商品存成一个「导入会话」→ 生成不可猜的 cartToken
│ 返回 { code:200, data:{ sessionId, redirectUrl, expiresAt, itemCount } }
▼
QCradar 前端 window.open(redirectUrl)
▼
用户浏览器落在 https://{你们的域名}/cart?cartToken=xxxx
│ 你们用 cartToken 反查这批商品 → 合并进【当前登录用户】的购物车
│ (未登录:先进游客购物车并提示登录,登录后并入账号)
▼
用户在你们站内自己结算支付 → 订单按 promotionCode 归属
三个要点:
| 要点 | 说明 |
|---|---|
| 谁的车 | 由你们自己的登录态决定。调用方传的 externalUserId 只作归属/分析,不做认证。 |
| 哪些货 | 由 URL 里的 cartToken 决定。token 必须不可猜(≥128 bit 随机),绝不能是明文 ?userId=。 |
| 归属 | 请求体带 promotionCode(你们分配给调用方的推广码/邀请码),写到这批购物车记录上,后续下单按它计佣。 |
1. 全局约定
| 项 | 值 |
|---|---|
| 测试 / 生产 Base URL | 由你们分配 |
| 编码 / 请求体 | 全程 UTF-8,application/json |
| 响应 | 必须是 JSON,Content-Type: application/json(调用方收到非 JSON 会判定为被拦截) |
| 鉴权 | HMAC-SHA256 签名,4 个 X-* 请求头(第 3 节) |
| 凭据交付 | 线下交付即可:每个环境一套 clientId + secretKey(不需要做自助注册接口) |
| 时间戳窗口 | 与服务器时间偏差 ≤ 300 秒 |
| Nonce 防重放 | 同一 clientId 下 nonce 10 分钟内不可复用 |
测试与生产各一套独立凭据,调用方切换环境只换配置,不改代码。
2. 接口总览
只有一个必需接口:
| 接口 | 方法 | 签名 | 说明 |
|---|---|---|---|
/open/cart/import-session/create | POST | 是 | 建立导入会话,返回 redirectUrl |
路径可以按你们的规范改,但请固定一个、大小写敏感、与签名行 2 完全一致,不要在网关层做大小写归一或重定向(签名里包含路径)。
3. 签名算法(核心,请逐字实现)
签名通过 4 个请求头传递:
| 请求头 | 内容 |
|---|---|
X-API-Key | clientId |
X-Timestamp | Unix 秒级时间戳(字符串) |
X-Nonce | 每次请求唯一的随机串(UUID 去连字符,32 位 hex) |
X-Signature | HMAC-SHA256 结果,小写 hex |
3.1 拼待签名串(Canonical String)
6 个部分用 \n 连接,顺序固定:
行1: HTTP 方法 大写,固定 POST
行2: 请求路径 不含域名、不含 query,大小写敏感,与真实 URL 完全一致
行3: 排序后的 query 串 本接口无 query → 空字符串(但换行符仍在)
行4: 请求体 SHA256 对收到的【原始请求体字节】做 SHA256 → 小写 hex
行5: X-Timestamp 与请求头同值
行6: X-Nonce 与请求头同值
示例(注意第 3 行是空行):
POST
/open/cart/import-session/create
b43bd828c850c096d1872ed79b8e0d2ac087a27d767b2ba7b6bca2de2d2fb303
1760000000
0123456789abcdef0123456789abcdef
3.2 计算签名
X-Signature = HMAC_SHA256( key = secretKey, message = CanonicalString ) → 小写 hex
3.3 服务端校验流程
- 取
X-API-Key查出secretKey;查不到 → 401Invalid app credentials。 |now − X-Timestamp| > 300s→ 403Timestamp expired。X-Nonce在 10 分钟内出现过 → 403Duplicate request;否则记下。- 对原始请求体字节(不要先 JSON 反序列化再重新序列化)做 SHA256,按 3.1 拼串,按 3.2 算签名,与
X-Signature做常量时间比较;不一致 → 401Invalid signature。
⚠️ 最常见的失败原因就是第 4 步:框架先把 body 解析成对象、再序列化一遍去算哈希,字段顺序/空格一变,哈希就不一样。请在过滤器/中间件层拿到原始字节再校验。
3.4 测试向量(可离线核对你们的实现)
用下面这组固定输入,你们的校验代码应当算出同样的签名:
| 项 | 值 |
|---|---|
secretKey | demo_secret_0123456789abcdef0123 |
clientId(X-API-Key) | ak_demo0123456789 |
X-Timestamp | 1760000000 |
X-Nonce | 0123456789abcdef0123456789abcdef |
| 路径 | /open/cart/import-session/create |
请求体(一行、无空格、无换行,共 600 字节):
{"idempotencyKey":"qcr_cart_user_12345_9f86d081884c_1760000000","externalUserId":"user_12345","returnUrl":"https://qcradar.com/cart","promotionCode":"DEMO_PROMO_CODE","items":[{"platform":"weidian","itemId":"7412345678","proUrl":"https://weidian.com/item.html?itemID=7412345678","proName":"Demo Hoodie Black","proPrice":128,"quantity":2,"selectedSku":{"skuId":"4501234567","skuText":"Black / L","imgUrl":"https://cdn.example.com/sku.jpg"}},{"platform":"taobao","itemId":"650123456789","proUrl":"https://item.taobao.com/item.htm?id=650123456789","proName":"Demo Sneaker","proPrice":299,"quantity":1}]}
期望结果:
| 项 | 值 |
|---|---|
| 请求体 SHA256 | b43bd828c850c096d1872ed79b8e0d2ac087a27d767b2ba7b6bca2de2d2fb303 |
| 空请求体 SHA256(备查) | e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 |
| X-Signature | 61fc4fc001802b5257a7f80e7f4ef5fb28f9a77a6c6053e36e0aab0fa55d13b8 |
3.5 参考实现 · 服务端校验(Node.js)
const crypto = require('node:crypto');
const sha256Hex = (buf) => crypto.createHash('sha256').update(buf).digest('hex');
// rawBody: Buffer,必须是收到的原始字节
function verify({ method, path, rawBody, headers, secretKey }) {
const canonical = [
method.toUpperCase(),
path,
'', // 无 query
sha256Hex(rawBody),
headers['x-timestamp'],
headers['x-nonce'],
].join('\n');
const expected = crypto.createHmac('sha256', secretKey).update(canonical, 'utf8').digest('hex');
const given = String(headers['x-signature'] ?? '').toLowerCase();
return expected.length === given.length &&
crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(given));
}
3.6 参考实现 · 服务端校验(Java)
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.security.MessageDigest;
import java.nio.charset.StandardCharsets;
static String hex(byte[] b) { StringBuilder s = new StringBuilder(); for (byte x : b) s.append(String.format("%02x", x)); return s.toString(); }
static String sign(String secretKey, String path, byte[] rawBody, String ts, String nonce) throws Exception {
String bodyHash = hex(MessageDigest.getInstance("SHA-256").digest(rawBody));
String canonical = String.join("\n", "POST", path, "", bodyHash, ts, nonce);
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(secretKey.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
return hex(mac.doFinal(canonical.getBytes(StandardCharsets.UTF_8))); // 与 X-Signature 比较(小写)
}
3.7 调用方(QCradar)的行为(便于你们对照)
- 请求头:
Content-Type: application/json、Accept: application/json、Origin: https://qcradar.com、浏览器样式的User-Agent,加上 4 个签名头。 - 请求体只序列化一次,签名和发送用同一个字符串。
- 单次超时 12 秒,最多重试 4 次,总预算 20 秒;重试时
idempotencyKey和请求体不变,X-Timestamp/X-Nonce换新并重新签名。 - 同时在途请求 ≤ 8 个;超过则调用方自行快速失败,不会压到你们。
- 出口 IP 固定,需要的话可以提供给你们加白名单(见 §8)。
4. 接口:建立导入会话
POST {BASE}/open/cart/import-session/create
Content-Type: application/json
Origin: https://qcradar.com
X-API-Key / X-Timestamp / X-Nonce / X-Signature
4.1 请求体
{
"idempotencyKey": "qcr_cart_user_12345_9f86d081884c_1760000000",
"externalUserId": "user_12345",
"returnUrl": "https://qcradar.com/cart",
"promotionCode": "你们分配给调用方的推广码",
"clientType": "H5",
"items": [
{
"platform": "weidian",
"itemId": "7412345678",
"proUrl": "https://weidian.com/item.html?itemID=7412345678",
"proName": "示例卫衣 黑色",
"proPrice": 128.00,
"quantity": 2,
"selectedSku": { "skuId": "4501234567", "skuText": "黑色 / L", "imgUrl": "https://.../sku.jpg" }
},
{
"platform": "taobao",
"itemId": "650123456789",
"proUrl": "https://item.taobao.com/item.htm?id=650123456789",
"proName": "示例运动鞋",
"proPrice": 299.00,
"quantity": 1
}
]
}
顶层字段:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
idempotencyKey | string | 是 | 幂等键,≤128 字符。同 key + 同内容在有效期内重复请求 → 返回同一个会话(同一 redirectUrl);同 key 不同内容 → 409。用于调用方重试时不重复建会话。 |
externalUserId | string | 否 | 调用方站内的用户标识(登录用户 user_<id>;游客不传)。只作映射/分析,不做认证。 |
returnUrl | string | 否 | 用户结算后可返回的调用方地址。可在购物车/订单完成页放一个「返回」入口;不做也不影响闭环。 |
promotionCode | string | 否(要计佣必填) | 你们分配给调用方的推广码/邀请码。整批商品生效,写入每条购物车记录。 |
clientType | string | 否 | PC / H5。QCradar 目前恒传 H5,可忽略。 |
items | array | 是 | 商品明细,1~50 项。 |
items[] 字段:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
platform | string | 是 | 货源平台:weidian / taobao / 1688(小写字符串)。微店占 QCradar 货源的大头,请务必支持。 |
itemId | string | 是 | 该平台商品 ID(纯数字字符串)。 |
proUrl | string | 是 | 源站商品链接,按平台固定模板拼(见 4.2)。 |
proName | string | 是 | 商品名(中文优先),仅作展示。 |
proPrice | number | 是 | 人民币元,参考价。请以你们抓取到的实时价格为准,不要把它当成结算价。 |
quantity | int | 是 | 数量,1~99。 |
selectedSku | object | 否 | 预选规格 { skuId, skuText, imgUrl }。skuId 是该平台的真实 SKU ID(微店 skuId / 淘宝、1688 的 sku_id),skuText 为展示文本,imgUrl 可能为空串。不传或空对象 → 商品整款加入,用户落地后自己选规格。 |
✅ 最小可跑通字段集:
platform / itemId / proUrl / proName / proPrice / quantity。 其它顶层字段和selectedSku都可以先不处理。请忽略未知字段,不要因为多了字段而报错,方便双方后续各自演进。
4.2 proUrl 模板
| platform | proUrl |
|---|---|
weidian | https://weidian.com/item.html?itemID={itemId} |
taobao | https://item.taobao.com/item.htm?id={itemId} |
1688 | https://detail.1688.com/offer/{itemId}.html |
4.3 关于规格(SKU):请按这个语义实现
- 调用方能给出真实
skuId的比例:微店款几乎全部;淘宝/1688 款约 90%。剩下的一定会有不带selectedSku的多规格商品。 - 因此请采用「没传规格就整款加入、用户落地后再选」的语义,不要做成「多规格商品未传 skuId 就整批失败」。那会让一部分购物车永远导不进去,而且失败发生在用户跳转之后,调用方收不到任何信号。
- 1688 的规格有两套标识(数字
sku_id与 32 位spec_id),调用方传的是数字sku_id,请在你们侧兼容。 - 传了
skuId但在你们侧解析不到(例如上游已改规格):按整款加入并提示用户选规格,同样不要整批失败。
4.4 成功响应
{
"code": 200,
"msg": "success",
"data": {
"sessionId": "CIS_20260924_xxxxxxxx",
"redirectUrl": "https://{你们的域名}/cart?cartToken=8f3a2b1c9d0e4f5a6b7c8d9e0f1a2b3c",
"expiresAt": "2026-09-24T12:30:00Z",
"itemCount": 2
}
}
| 字段 | 说明 |
|---|---|
code | 业务码,成功固定 200 |
data.sessionId | 会话 ID,调用方留存做对账/排错 |
data.redirectUrl | 核心。调用方前端直接跳转;调用方会校验它必须是 https 且域名属于你们(防开放重定向),所以请把测试与生产会用到的购物车域名都告诉我们 |
data.expiresAt | 会话过期时间,ISO 8601(带时区) |
data.itemCount | 实际纳入会话的商品数 |
4.5 失败响应
顶层 code != 200,HTTP 状态码可与业务码一致(推荐),也可保持 200;但响应体必须是 JSON:
{ "code": 401, "msg": "Invalid signature", "data": null }
| code | msg(示例) | 含义 | 调用方处理 |
|---|---|---|---|
| 400 | Invalid request / Too many items / Invalid quantity | 参数错误 | 报错给用户,不重试 |
| 401 | Invalid app credentials / Invalid signature | 凭据或签名错 | 报错,不重试 |
| 403 | Timestamp expired / Duplicate request | 时间戳超窗 / nonce 重放 | 换新 nonce 重试 |
| 409 | Idempotency key conflict | 同幂等键不同内容 | 报错,不重试 |
| 429 | Too many requests | 限流 | 退避后重试 |
| 110 | Item fetch failed, please retry | 商品抓取瞬时失败(可重试) | 换新 nonce 立即重试 |
| 500 | Internal server error | 内部错误 | 保持幂等键重试 |
code 110请专门留给「重试大概率就能成功」的瞬时失败;其它业务失败别用它,否则调用方会白重试。
5. 用户落地行为(你们侧,建议)
| 场景 | 建议行为 |
|---|---|
| 用户已登录 | 把会话里的商品合并进该用户购物车(同商品同规格叠加数量),然后展示购物车。 |
| 用户未登录 | 先进游客购物车并提示登录/注册,登录后游客车并入账号;或者先登录、登录后自动认领。二选一即可,但不要要求用户在调用方站点做任何授权:游客也能用,转化最好。 |
同一 cartToken 被打开多次(刷新、回退) | 按 sessionId 去重,不重复加购。 |
| 会话过期后打开 | 提示「链接已过期,请返回重新导入」。建议有效期 30~60 分钟。 |
| 某一项商品解析失败 | 跳过该项、导入其余商品,并在购物车页提示哪几项没成功。不要整批失败。 |
| 购物车容量不足 | 能加多少加多少并提示,或整批拒绝并明确提示;二者任选,但请提示用户。 |
| 佣金归属 | promotionCode 写入每条购物车记录;用户后续从这些记录下单,订单归属调用方。用户自己删掉再手动加的商品不归属,这是正常的。 |
6. 幂等、限流、容量
- 幂等:
clientId + idempotencyKey为键。同键同内容 → 返回原会话(不延长过期时间);同键不同内容 → 409。已过期会话按你们方便:返回原会话或新建都可以,调用方每次用户点击都会生成新键。 - 限流(建议值):同一
clientId60 次/分钟;同一 IP 30 次/分钟。超限返回 429 JSON 即可。 - 容量:单次
items≤ 50 项。
7. 安全要求
secretKey只在双方服务端保存,不进代码仓库、不进日志、不下发浏览器。- 签名一律服务端校验;
cartToken用密码学随机数生成(≥128 bit),不可枚举、不可从用户 ID 推导。 redirectUrl是临时敏感凭证:不要写进可公开的日志,不要长期有效。- 服务器校时(NTP),否则时间戳窗口会误拒。
- 不要用
externalUserId做任何鉴权或账号绑定,它是调用方站内的标识,可被伪造。
8. 网关 / 反爬层请注意
这几条是在真实对接中踩过的坑,提前说:
- 开放接口路径请不要挂 Cloudflare 托管挑战 / JS 挑战。服务端到服务端的调用过不了浏览器挑战,表现为 403 + 一段 HTML,而不是 JSON。如果网关前必须有 WAF,请按
X-API-Key请求头或调用方出口 IP 放行这条路径。 - WAF 规则如果是按路径放行,请确认放行的路径与真实路径大小写完全一致。
- 调用方请求会带浏览器样式
User-Agent和Origin,便于你们做白名单。 - 返回任何情况都请给 JSON(含 4xx/5xx/429),不要返回 HTML 错误页。
9. 对接时需要你们提供的信息
| # | 项 | 说明 |
|---|---|---|
| 1 | 测试环境 Base URL + clientId / secretKey | 线下安全渠道交付 |
| 2 | 生产环境 Base URL + clientId / secretKey | 联调通过后 |
| 3 | 推广码 promotionCode | 每个环境一个;确认测试环境是否认生产码 |
| 4 | redirectUrl 会落到的域名 | 测试与生产各自的购物车域名(调用方要加进跳转白名单) |
| 5 | 支持的货源平台 | 确认 weidian / taobao / 1688 三个都支持 |
| 6 | 会话有效期 | 分钟数 |
| 7 | 是否需要出口 IP 白名单 | 需要则调用方提供 IP |
| 8 | 对账方式 | 后台能按推广码查到订单/佣金即可;有查询接口更好,没有也不阻塞上线 |
| 9 | 联调对接人 | 技术 + 商务各一位 |
10. 联调步骤(建议顺序)
- 按第 3 节实现签名校验,先用 3.4 的测试向量离线核对,通过后交付测试凭据。
- 调用方指向测试环境,发一个单商品、只含必填字段的请求 → 期望
code 200+redirectUrl。- 签名错 → 双方对照 canonical 逐行比;最常见是第 4 行(原始字节 vs 重新序列化)。
- 浏览器打开
redirectUrl:未登录 / 已登录两种状态各验一次,确认商品与数量落进购物车。 - 扩到多商品 +
selectedSku预选规格 +promotionCode;验证同一链接刷新不重复加购;验证一项解析失败时其余照常导入。 - 下一笔测试订单,确认后台归属到调用方的推广码。
- 切生产凭据,把第 2~5 步各复测一遍。
- 调用方上线导入入口。
11. 联系方式
技术与商务对接统一走站内反馈系统:点击联系我们,打开后已预选「联系 → 合作」,写明平台名称和对接人即可。提交需要先登录 QCradar 账号,之后的往来都在同一个会话线程里。
本文所有域名、凭据、推广码均为示例。希望改路径、字段名或签名头名称的,提前告知即可,调用方适配成本很低。
One-Click Cart Import · General Integration Spec
Version v1 · 2026-09-24 · Initiated by: QCradar (qcradar.com)
In one sentence: the user adds multiple items to their cart on QCradar (or any tool implementing this spec) and clicks "Import to {your platform}"; when the browser lands on your cart, the items are already added and SKUs pre-selected, and the user logs in and checks out on your own site.
You only need to implement one signed endpoint: create an import session and return a redirect link. This spec has already been validated in production with multiple shopping-agent platforms, and the QCradar-side client code is ready to go — once you implement this spec, integration testing and launch typically take 1-2 business days.
Already have your own cart-import / open API? Just send us the documentation (contact us) and we'll adapt to your contract. This document describes the least-effort option we recommend.
For why we're doing this and how QCradar recommends shopping-agent platforms, see the open letter to shopping-agent platforms.
0. Closed-Loop Model (Read This First)
User selects items in the QCradar cart, clicks "Import to {platform}"
│
▼
QCradar backend
│ 1) Assembles items (source platform + item ID + SKU + quantity)
│ 2) Signs with secretKey using HMAC-SHA256
│ 3) POST {your gateway}/open/cart/import-session/create
▼
Your gateway
│ Verifies signature → stores this batch as an "import session" → generates an unguessable cartToken
│ Returns { code:200, data:{ sessionId, redirectUrl, expiresAt, itemCount } }
▼
QCradar frontend window.open(redirectUrl)
▼
User's browser lands on https://{your domain}/cart?cartToken=xxxx
│ You look up this batch of items by cartToken → merge into the 【currently logged-in user's】 cart
│ (Not logged in: goes into a guest cart first and prompts login; merged into the account after login)
▼
User checks out and pays on your own site → order attributed via promotionCode
Three key points:
| Point | Description |
|---|---|
| Whose cart | Determined by your own login state. The externalUserId sent by the caller is for attribution/analytics only — not authentication. |
| Which items | Determined by the cartToken in the URL. The token must be unguessable (≥128-bit random), never a plain ?userId=. |
| Attribution | The request body carries promotionCode (the promo/referral code you assign to the caller), written onto this batch's cart records; subsequent orders are commissioned based on it. |
1. Global Conventions
| Item | Value |
|---|---|
| Test / production Base URL | Assigned by you |
| Encoding / request body | UTF-8 throughout, application/json |
| Response | Must be JSON, Content-Type: application/json (the caller treats a non-JSON response as being blocked) |
| Auth | HMAC-SHA256 signature, 4 X-* request headers (Section 3) |
| Credential delivery | Offline delivery is fine: one clientId + secretKey pair per environment (no self-service registration endpoint needed) |
| Timestamp window | ≤ 300 seconds skew from server time |
| Nonce replay protection | A nonce cannot be reused within 10 minutes under the same clientId |
Test and production each have an independent credential set; the caller switches environments by changing config only, no code changes.
2. API Overview
There is only one required endpoint:
| Endpoint | Method | Signed | Description |
|---|---|---|---|
/open/cart/import-session/create | POST | Yes | Creates an import session, returns redirectUrl |
The path may be adjusted to fit your conventions, but please fix on one, case-sensitive, exactly matching signature line 2 — don't normalize case or redirect at the gateway layer (the path is part of the signature).
3. Signature Algorithm (Core — Implement Exactly as Specified)
The signature is carried in 4 request headers:
| Header | Content |
|---|---|
X-API-Key | clientId |
X-Timestamp | Unix timestamp in seconds (string) |
X-Nonce | A random string unique per request (UUID with hyphens removed, 32-char hex) |
X-Signature | HMAC-SHA256 result, lowercase hex |
3.1 Building the Canonical String
6 parts joined with \n, in fixed order:
Line 1: HTTP method Uppercase, fixed as POST
Line 2: Request path No domain, no query, case-sensitive, exactly matches the real URL
Line 3: Sorted query string This endpoint has no query → empty string (but the newline is still there)
Line 4: Request body SHA256 SHA256 of the 【raw request body bytes】 received → lowercase hex
Line 5: X-Timestamp Same value as the header
Line 6: X-Nonce Same value as the header
Example (note line 3 is blank):
POST
/open/cart/import-session/create
b43bd828c850c096d1872ed79b8e0d2ac087a27d767b2ba7b6bca2de2d2fb303
1760000000
0123456789abcdef0123456789abcdef
3.2 Computing the Signature
X-Signature = HMAC_SHA256( key = secretKey, message = CanonicalString ) → lowercase hex
3.3 Server-Side Verification Flow
- Look up
secretKeyusingX-API-Key; not found → 401Invalid app credentials. |now − X-Timestamp| > 300s→ 403Timestamp expired.X-Nonceseen within the last 10 minutes → 403Duplicate request; otherwise record it.- Compute SHA256 on the raw request body bytes (do not deserialize to JSON and re-serialize first), build the string per 3.1, compute the signature per 3.2, and compare against
X-Signaturein constant time; mismatch → 401Invalid signature.
⚠️ The most common failure is in step 4: the framework parses the body into an object first and then re-serializes it to compute the hash — any change in field order or whitespace changes the hash. Get the raw bytes at the filter/middleware layer before verifying.
3.4 Test Vector (Verify Your Implementation Offline)
Using this fixed set of inputs, your verification code should compute the same signature:
| Item | Value |
|---|---|
secretKey | demo_secret_0123456789abcdef0123 |
clientId (X-API-Key) | ak_demo0123456789 |
X-Timestamp | 1760000000 |
X-Nonce | 0123456789abcdef0123456789abcdef |
| Path | /open/cart/import-session/create |
Request body (one line, no spaces, no line breaks, 600 bytes total):
{"idempotencyKey":"qcr_cart_user_12345_9f86d081884c_1760000000","externalUserId":"user_12345","returnUrl":"https://qcradar.com/cart","promotionCode":"DEMO_PROMO_CODE","items":[{"platform":"weidian","itemId":"7412345678","proUrl":"https://weidian.com/item.html?itemID=7412345678","proName":"Demo Hoodie Black","proPrice":128,"quantity":2,"selectedSku":{"skuId":"4501234567","skuText":"Black / L","imgUrl":"https://cdn.example.com/sku.jpg"}},{"platform":"taobao","itemId":"650123456789","proUrl":"https://item.taobao.com/item.htm?id=650123456789","proName":"Demo Sneaker","proPrice":299,"quantity":1}]}
Expected result:
| Item | Value |
|---|---|
| Request body SHA256 | b43bd828c850c096d1872ed79b8e0d2ac087a27d767b2ba7b6bca2de2d2fb303 |
| Empty request body SHA256 (for reference) | e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 |
| X-Signature | 61fc4fc001802b5257a7f80e7f4ef5fb28f9a77a6c6053e36e0aab0fa55d13b8 |
3.5 Reference Implementation · Server-Side Verification (Node.js)
const crypto = require('node:crypto');
const sha256Hex = (buf) => crypto.createHash('sha256').update(buf).digest('hex');
// rawBody: Buffer, must be the raw bytes received
function verify({ method, path, rawBody, headers, secretKey }) {
const canonical = [
method.toUpperCase(),
path,
'', // no query
sha256Hex(rawBody),
headers['x-timestamp'],
headers['x-nonce'],
].join('\n');
const expected = crypto.createHmac('sha256', secretKey).update(canonical, 'utf8').digest('hex');
const given = String(headers['x-signature'] ?? '').toLowerCase();
return expected.length === given.length &&
crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(given));
}
3.6 Reference Implementation · Server-Side Verification (Java)
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.security.MessageDigest;
import java.nio.charset.StandardCharsets;
static String hex(byte[] b) { StringBuilder s = new StringBuilder(); for (byte x : b) s.append(String.format("%02x", x)); return s.toString(); }
static String sign(String secretKey, String path, byte[] rawBody, String ts, String nonce) throws Exception {
String bodyHash = hex(MessageDigest.getInstance("SHA-256").digest(rawBody));
String canonical = String.join("\n", "POST", path, "", bodyHash, ts, nonce);
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(secretKey.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
return hex(mac.doFinal(canonical.getBytes(StandardCharsets.UTF_8))); // compare against X-Signature (lowercase)
}
3.7 Caller (QCradar) Behavior (For Reference)
- Headers:
Content-Type: application/json,Accept: application/json,Origin: https://qcradar.com, a browser-styleUser-Agent, plus the 4 signature headers. - The request body is serialized only once; the same string is used for both signing and sending.
- 12-second timeout per attempt, up to 4 retries, 20-second total budget; on retry,
idempotencyKeyand the request body stay the same, whileX-Timestamp/X-Nonceare refreshed and re-signed. - ≤ 8 concurrent in-flight requests; beyond that, the caller fails fast on its own side and won't overload you.
- Egress IPs are fixed; we can provide them for allowlisting if needed (see §8).
4. Endpoint: Create Import Session
POST {BASE}/open/cart/import-session/create
Content-Type: application/json
Origin: https://qcradar.com
X-API-Key / X-Timestamp / X-Nonce / X-Signature
4.1 Request Body
{
"idempotencyKey": "qcr_cart_user_12345_9f86d081884c_1760000000",
"externalUserId": "user_12345",
"returnUrl": "https://qcradar.com/cart",
"promotionCode": "the promo code you assign to the caller",
"clientType": "H5",
"items": [
{
"platform": "weidian",
"itemId": "7412345678",
"proUrl": "https://weidian.com/item.html?itemID=7412345678",
"proName": "Demo hoodie, black",
"proPrice": 128.00,
"quantity": 2,
"selectedSku": { "skuId": "4501234567", "skuText": "Black / L", "imgUrl": "https://.../sku.jpg" }
},
{
"platform": "taobao",
"itemId": "650123456789",
"proUrl": "https://item.taobao.com/item.htm?id=650123456789",
"proName": "Demo sneakers",
"proPrice": 299.00,
"quantity": 1
}
]
}
Top-level fields:
| Field | Type | Required | Description |
|---|---|---|---|
idempotencyKey | string | Yes | Idempotency key, ≤128 characters. Same key + same content, repeated within the validity window → returns the same session (same redirectUrl); same key, different content → 409. Used so the caller's retries don't create duplicate sessions. |
externalUserId | string | No | The caller's own site user identifier (logged-in user: user_<id>; omit for guests). For mapping/analytics only — not authentication. |
returnUrl | string | No | The caller's URL the user can return to after checkout. You can place a "Back" entry point on the cart/order-completion page; omitting it doesn't affect the closed loop. |
promotionCode | string | No (required for commission) | The promo/referral code you assign to the caller. Applies to the whole batch, written onto every cart record. |
clientType | string | No | PC / H5. QCradar currently always sends H5; can be ignored. |
items | array | Yes | Item details, 1-50 items. |
items[] fields:
| Field | Type | Required | Description |
|---|---|---|---|
platform | string | Yes | Source platform: weidian / taobao / 1688 (lowercase string). Weidian accounts for the bulk of QCradar's sourcing — please be sure to support it. |
itemId | string | Yes | The item ID on that platform (numeric string). |
proUrl | string | Yes | Source-site item URL, built from a fixed per-platform template (see 4.2). |
proName | string | Yes | Item name (Chinese preferred), display only. |
proPrice | number | Yes | CNY, reference price. Use your own scraped real-time price as authoritative; don't treat this as the settlement price. |
quantity | int | Yes | Quantity, 1-99. |
selectedSku | object | No | Pre-selected SKU { skuId, skuText, imgUrl }. skuId is the real SKU ID on that platform (Weidian's skuId / Taobao's and 1688's sku_id), skuText is display text, imgUrl may be an empty string. Omitted or an empty object → the item is added at the parent-SKU level, and the user picks a variant after landing. |
✅ Minimum viable field set:
platform / itemId / proUrl / proName / proPrice / quantity. All other top-level fields andselectedSkucan be left unhandled for now. Please ignore unknown fields rather than erroring on extra fields, so both sides can evolve independently going forward.
4.2 proUrl Templates
| platform | proUrl |
|---|---|
weidian | https://weidian.com/item.html?itemID={itemId} |
taobao | https://item.taobao.com/item.htm?id={itemId} |
1688 | https://detail.1688.com/offer/{itemId}.html |
4.3 On SKUs: Implement This Semantics
- Share of items where the caller can provide a real
skuId: nearly all for Weidian; about 90% for Taobao/1688. There will always be some multi-variant items without aselectedSku. - So please implement the semantics of "no SKU provided → add at the parent-item level, user picks it after landing"; do not implement "multi-variant item without a skuId → fail the whole batch." That would make some carts permanently un-importable, and the failure happens after the user has already been redirected, so the caller gets no signal at all.
- 1688 SKUs have two kinds of identifiers (numeric
sku_idand a 32-characterspec_id); the caller sends the numericsku_id— please support that on your side. - If
skuIdis provided but can't be resolved on your side (e.g., the upstream SKU has since changed): add at the parent-item level and prompt the user to pick a variant — again, don't fail the whole batch.
4.4 Success Response
{
"code": 200,
"msg": "success",
"data": {
"sessionId": "CIS_20260924_xxxxxxxx",
"redirectUrl": "https://{your domain}/cart?cartToken=8f3a2b1c9d0e4f5a6b7c8d9e0f1a2b3c",
"expiresAt": "2026-09-24T12:30:00Z",
"itemCount": 2
}
}
| Field | Description |
|---|---|
code | Business status code; fixed 200 on success |
data.sessionId | Session ID; the caller retains it for reconciliation/debugging |
data.redirectUrl | The key field. The caller's frontend redirects to it directly; the caller validates that it must be https and that the domain belongs to you (to prevent open redirects), so please tell us all cart domains used in both test and production |
data.expiresAt | Session expiration time, ISO 8601 (with timezone) |
data.itemCount | Number of items actually included in the session |
4.5 Failure Response
Top-level code != 200; the HTTP status code can match the business code (recommended), or stay 200; but the response body must be JSON:
{ "code": 401, "msg": "Invalid signature", "data": null }
| code | msg (example) | Meaning | Caller action |
|---|---|---|---|
| 400 | Invalid request / Too many items / Invalid quantity | Parameter error | Show an error to the user, do not retry |
| 401 | Invalid app credentials / Invalid signature | Bad credentials or signature | Show an error, do not retry |
| 403 | Timestamp expired / Duplicate request | Timestamp out of window / nonce replay | Retry with a fresh nonce |
| 409 | Idempotency key conflict | Same idempotency key, different content | Show an error, do not retry |
| 429 | Too many requests | Rate limited | Back off and retry |
| 110 | Item fetch failed, please retry | Transient item-fetch failure (retryable) | Retry immediately with a fresh nonce |
| 500 | Internal server error | Internal error | Retry keeping the same idempotency key |
Please reserve
code 110specifically for transient failures where "retrying will likely succeed"; don't use it for other business failures, or the caller will retry for nothing.
5. User Landing Behavior (Your Side, Recommended)
| Scenario | Recommended behavior |
|---|---|
| User already logged in | Merge the items from the session into that user's cart (same item + same SKU → add quantities), then show the cart. |
| User not logged in | Either put items into a guest cart first and prompt login/signup, merging into the account after login; or require login first and auto-claim the cart afterward. Either approach is fine, but don't require the user to perform any authorization on the caller's site — letting guests use it too gives the best conversion. |
Same cartToken opened multiple times (refresh, back navigation) | Deduplicate by sessionId; don't add items twice. |
| Opened after session expiry | Show "Link expired, please go back and import again." Recommended validity: 30-60 minutes. |
| An individual item fails to resolve | Skip that item, import the rest, and indicate on the cart page which items didn't succeed. Don't fail the whole batch. |
| Cart capacity insufficient | Either add as many as fit and notify the user, or reject the whole batch with a clear message — either is fine, just notify the user. |
| Commission attribution | promotionCode is written onto every cart record; orders the user later places from these records are attributed to the caller. Items the user deletes and re-adds manually are not attributed — that's expected. |
6. Idempotency, Rate Limiting, Capacity
- Idempotency: keyed by
clientId + idempotencyKey. Same key, same content → return the original session (without extending its expiry); same key, different content → 409. For an already-expired session, do whatever's convenient for you: return the original session or create a new one — the caller generates a new key on every user click anyway. - Rate limiting (suggested values): 60 requests/minute per
clientId; 30 requests/minute per IP. Just return a 429 JSON response when exceeded. - Capacity:
items≤ 50 per request.
7. Security Requirements
secretKeyis kept only on both sides' servers — never in a code repo, never in logs, never sent to the browser.- Signatures are always verified server-side;
cartTokenmust be generated with a cryptographically random source (≥128 bit), non-enumerable, and not derivable from a user ID. redirectUrlis a temporary sensitive credential: don't write it to publicly readable logs, and don't make it long-lived.- Keep server clocks synced (NTP), or the timestamp window will cause false rejections.
- Don't use
externalUserIdfor any authentication or account binding — it's the caller's own site identifier and can be spoofed.
8. Gateway / Anti-Bot Layer Notes
These are pitfalls we've hit in real integrations — flagging them in advance:
- Please don't put Cloudflare Managed Challenge / JS Challenge on the open API path. Server-to-server calls can't pass a browser challenge — you'll see a 403 with an HTML body instead of JSON. If a WAF must sit in front of the gateway, allowlist this path by the
X-API-Keyheader or the caller's egress IP. - If the WAF rule allowlists by path, make sure the allowlisted path matches the real path's case exactly.
- The caller's requests carry a browser-style
User-AgentandOrigin, to make allowlisting easier on your side. - Please return JSON for every case (including 4xx/5xx/429) — never an HTML error page.
9. Information We Need From You for Integration
| # | Item | Description |
|---|---|---|
| 1 | Test environment Base URL + clientId / secretKey | Delivered via an offline secure channel |
| 2 | Production environment Base URL + clientId / secretKey | After integration testing passes |
| 3 | Promo code promotionCode | One per environment; confirm whether the test environment recognizes the production code |
| 4 | Domain(s) redirectUrl will point to | Cart domains for test and production respectively (the caller needs to add them to the redirect allowlist) |
| 5 | Supported source platforms | Confirm that weidian / taobao / 1688 are all supported |
| 6 | Session validity period | In minutes |
| 7 | Whether an egress IP allowlist is needed | If so, the caller will provide the IPs |
| 8 | Reconciliation method | As long as the backend can look up orders/commissions by promo code; a query API is a nice-to-have but not a launch blocker |
| 9 | Integration contacts | One technical and one business contact |
10. Integration Steps (Suggested Order)
- Implement signature verification per Section 3, first verifying offline against the test vector in 3.4; once it passes, we'll deliver test credentials.
- The caller points at the test environment and sends a request with a single item, only required fields → expect
code 200+redirectUrl.- Signature mismatch → compare the canonical string line by line on both sides; the most common culprit is line 4 (raw bytes vs. re-serialized).
- Open
redirectUrlin a browser: verify once each for logged-out / logged-in states, confirming items and quantities land in the cart. - Expand to multiple items +
selectedSkupre-selected variants +promotionCode; verify that refreshing the same link doesn't duplicate items; verify that when one item fails to resolve, the rest still import normally. - Place a test order, confirming the backend attributes it to the caller's promo code.
- Switch to production credentials and re-run steps 2-5.
- The caller launches the import entry point.
11. Contact
Both technical and business contact go through our on-site feedback system: Contact us. It opens with "Contact → Partnership" pre-selected — just fill in your platform name and contact person. Submitting requires logging into a QCradar account first; all subsequent exchanges stay in the same thread.
All domains, credentials, and promo codes in this document are examples. If you'd like to change paths, field names, or signature header names, just let us know in advance — the adaptation cost on the caller's side is low.