开发者/快速开始

认证与签名

每个 API 请求都要带上商户号和 API Key。下单请求(POST/orders)还需要附带签名,确保请求内容不能被篡改,也不能被重复利用。

相关文档:商品与下单 · 安全 · 概览

#凭证

凭证示例是否保密
商户号(Merchant ID)M00000001否,它只表明你是谁。
API Keycvb2b_...是,它既是你的密码,也是签名用的密钥。
  • 账户审核通过后,CardV 会分配商户号。
  • API Key 由 Owner 在商户后台创建(见 API Key 管理)。
  • 正式环境和沙盒的 Key 各自独立,不能混用。
  • 只有在 CardV 审核通过你的企业资料后,API 才能使用。此时 GET/account 会返回 "api_access_enabled": true。

#请求头

每个请求都要带这两个请求头:

HTTP
X-Merchant-Id: M00000001
X-Api-Key: cvb2b_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

POST/orders 还要再带这三个:

HTTP
X-Timestamp: 1790000000
X-Nonce: 0123456789abcdef0123456789abcdef
X-Signature: fd55fa31a3b6817d5368903dbb4bad4458381e024c17e688580bac96577acfed
  • X-Timestamp:当前 Unix 时间(秒),和 CardV 服务器时间相差不能超过 5 分钟(300 秒)。
  • X-Nonce:随机串,每次都不能重复。建议使用 32 位随机十六进制字符。
  • X-Signature:签名,计算方法见下单签名,必须是小写十六进制。

GET 请求不需要签名。

#下单签名

  1. 把订单内容序列化成 JSON 文本,只序列化一次。签名和发送都用这同一份字节。

  2. 计算请求体哈希:BODY_HASH = 请求体的 SHA-256,小写十六进制。

  3. 把下面五行用换行符(\n)连接起来,末尾不要加换行:

    Text
    POST
    /api/v1/orders
    <X-Timestamp>
    <X-Nonce>
    <BODY_HASH>
  4. 计算签名:X-Signature = 用完整的 API Key 作为密钥,对上面这段文本做 HMAC-SHA256,结果写成小写十六进制。

最常见的错误是:签名用的 JSON 和实际发送的 JSON 不是同一份。比如 {"a":1} 和 {"a": 1} 的字节就不一样。请确认你的 HTTP 库在签名之后没有改动请求体。

测试数据:用下面的数据验证你的签名代码(Key 是假的)。

Text
API key      cvb2b_TEST_ONLY_not_a_real_key_0123456789abcdef
Method       POST
Path         /api/v1/orders
X-Timestamp  1790000000
X-Nonce      0123456789abcdef0123456789abcdef

请求体(108 字节,单行,末尾无换行):

JSON
{"external_order_id":"TEST-0001","items":[{"sku_id":"S000001","quantity":1,"expected_unit_price":"9.2500"}]}

应得到的结果:

Text
BODY_HASH    b0545ae25d54b219f27d8bd90e4dcbf26cf0d491f53982da67b8eef0a1a59960
X-Signature  fd55fa31a3b6817d5368903dbb4bad4458381e024c17e688580bac96577acfed

#示例代码

以下示例都能算出上面测试数据的结果。Key 请从密钥管理系统读取,不要写在代码里。

cURL(bash + OpenSSL)

Shell
BASE_URL="https://sandbox.cardv.net"
REQ_PATH="/api/v1/orders"      # do not call this variable PATH
BODY='{"external_order_id":"SHOP-10001","items":'
BODY+='[{"sku_id":"S000001","quantity":1,"expected_unit_price":"9.2500"}]}'
TS=$(date +%s)
NONCE=$(openssl rand -hex 16)
BODY_HASH=$(printf '%s' "$BODY" | openssl dgst -sha256 -hex | sed 's/^.*= //')
SIG=$(printf 'POST\n%s\n%s\n%s\n%s' "$REQ_PATH" "$TS" "$NONCE" "$BODY_HASH" \
  | openssl dgst -sha256 -hmac "$CARDV_API_KEY" -hex | sed 's/^.*= //')

curl -sS -X POST "$BASE_URL$REQ_PATH" \
  -H "Content-Type: application/json" \
  -H "X-Merchant-Id: $CARDV_MERCHANT_ID" \
  -H "X-Api-Key: $CARDV_API_KEY" \
  -H "X-Timestamp: $TS" \
  -H "X-Nonce: $NONCE" \
  -H "X-Signature: $SIG" \
  --data-raw "$BODY"

请用 --data-raw,不要用 -d @file。-d 会去掉换行,导致发送的字节和签名时不一致。

Python(requests)

Python
import hashlib, hmac, json, os, secrets, time
import requests

BASE_URL = "https://sandbox.cardv.net"
MERCHANT_ID = os.environ["CARDV_MERCHANT_ID"]
API_KEY = os.environ["CARDV_API_KEY"]
AUTH = {"X-Merchant-Id": MERCHANT_ID, "X-Api-Key": API_KEY}


def sign(path: str, body: bytes, ts: str, nonce: str) -> str:
    body_hash = hashlib.sha256(body).hexdigest()
    text = "\n".join(["POST", path, ts, nonce, body_hash])
    return hmac.new(API_KEY.encode(), text.encode(), hashlib.sha256).hexdigest()


def cardv_get(path: str, params=None) -> requests.Response:
    return requests.get(BASE_URL + path, params=params, headers=AUTH, timeout=30)


def cardv_post(path: str, payload: dict) -> requests.Response:
    body = json.dumps(payload, separators=(",", ":"), ensure_ascii=False).encode()
    ts, nonce = str(int(time.time())), secrets.token_hex(16)
    headers = {
        **AUTH,
        "Content-Type": "application/json",
        "X-Timestamp": ts,
        "X-Nonce": nonce,
        "X-Signature": sign(path, body, ts, nonce),
    }
    return requests.post(BASE_URL + path, data=body, headers=headers, timeout=30)


resp = cardv_post("/api/v1/orders", {
    "external_order_id": "SHOP-10001",
    "items": [{"sku_id": "S000001", "quantity": 1, "expected_unit_price": "9.2500"}],
})
print(resp.status_code, resp.json())

order_id = resp.json()["order"]["order_id"]
print(cardv_get(f"/api/v1/orders/{order_id}").json()["status"])

Node.js 18+(内置 fetch)

Node.js
import crypto from "node:crypto";

const BASE_URL = "https://sandbox.cardv.net";
const { CARDV_MERCHANT_ID, CARDV_API_KEY } = process.env;
const AUTH = { "X-Merchant-Id": CARDV_MERCHANT_ID, "X-Api-Key": CARDV_API_KEY };

function sign(path, body, timestamp, nonce) {
  const bodyHash = crypto.createHash("sha256").update(body).digest("hex");
  const text = ["POST", path, timestamp, nonce, bodyHash].join("\n");
  return crypto.createHmac("sha256", CARDV_API_KEY).update(text, "utf8").digest("hex");
}

async function cardvPost(path, payload) {
  const body = Buffer.from(JSON.stringify(payload), "utf8"); // serialize once
  const timestamp = String(Math.floor(Date.now() / 1000));
  const nonce = crypto.randomBytes(16).toString("hex");
  const headers = {
    ...AUTH,
    "Content-Type": "application/json",
    "X-Timestamp": timestamp,
    "X-Nonce": nonce,
    "X-Signature": sign(path, body, timestamp, nonce),
  };
  const res = await fetch(BASE_URL + path, { method: "POST", headers, body });
  return { status: res.status, body: await res.json() };
}

console.log(await cardvPost("/api/v1/orders", {
  external_order_id: "SHOP-10001",
  items: [{ sku_id: "S000001", quantity: 1, expected_unit_price: "9.2500" }],
}));

PHP(仅签名部分)

PHP
<?php
function cardv_sign(
    string $apiKey, string $path, string $body, string $ts, string $nonce
): string {
    $text = implode("\n", ['POST', $path, $ts, $nonce, hash('sha256', $body)]);
    return hash_hmac('sha256', $text, $apiKey); // lowercase hex
}

// Send exactly $body.
$body = json_encode($payload, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
$ts = (string) time();
$nonce = bin2hex(random_bytes(16));
$signature = cardv_sign($apiKey, '/api/v1/orders', $body, $ts, $nonce);

#错误处理

下面这些错误都返回 HTTP 403(不是 401),只有频率超限返回 429。响应格式如下:

JSON
{"detail": "Invalid HMAC signature."}
问题怎么处理
商户号或 API Key 缺失、错误检查两个请求头,以及是否用错了环境。
API 尚未开通等待 CardV 审核通过你的企业资料。
你的服务器 IP 不在白名单内在商户后台把它加入 IP 白名单。
该接口只能在商户后台使用请到商户后台操作。API Key 只能调用 7 个接口。
签名缺失或错误修正签名代码,用测试数据核对。
时间戳过早或过晚校准服务器时间(NTP)。
随机串已被使用每次下单请求都生成新的随机串。
请求过于频繁(429)按 Retry-After 给出的秒数等待后再试。

注意:

  • 重新提交订单时,要生成新的时间戳、随机串和签名,订单内容保持不变。见安全重试。
  • 即使签名错误,这个随机串也会被记为已使用。
  • CardV 会记录失败的请求,失败次数异常时会通知我们的团队。

#IP 白名单

IP 白名单可以限定只有你自己的服务器才能使用这个 Key。由 Owner 在商户后台管理。

  • 白名单是可选的。没有任何规则时,任何 IP 都可以调用。
  • 只要添加了一条规则,其他 IP 的请求都会返回 HTTP 403。
  • 规则格式如 203.0.113.10/32(IPv4)或 2001:db8::/48(IPv6)。
  • 白名单只限制 API Key 调用,不影响商户后台登录。
  • 启用前,请把所有服务器的出口 IP 都加进去,包括 NAT 网关和备用机房。

#API Key 管理

API Key 只能调用接口一览里的 7 个接口。其中包括下单和读取卡密,所以请像保护 Owner 密码一样保护它。

只有 Owner 可以管理 Key,位置在商户后台的 Integrations → API keys。正式环境和沙盒需要分别操作。

  1. 创建:新建一个 Key 并命名。此时后台还不会显示完整的 Key。
  2. 查看(Reveal):CardV 会给你发一封含 6 位验证码的邮件,10 分钟内有效(最多尝试 5 次)。输入验证码后可以看到完整的 Key,请立即保存。每次查看都会记录日志。
  3. 停用:不再需要的 Key 请及时停用,立即生效且不可恢复。

在不停机的情况下更换 Key:

  1. 创建并查看一个新 Key。
  2. 把新 Key 部署到你所有的服务器。
  3. 在商户后台确认旧 Key 已经没有调用。
  4. 停用旧 Key。

建议至少每年更换一次 Key;接触过 Key 的人员离职时也要更换。如果怀疑 Key 已经泄露,请先停用,再排查原因。

接入遇到问题?请发送邮件至 [email protected],并附上 Merchant ID 与订单号或请求 ID。