开发者/快速开始
认证与签名
每个 API 请求都要带上商户号和 API Key。下单请求(POST/orders)还需要附带签名,确保请求内容不能被篡改,也不能被重复利用。
#凭证
| 凭证 | 示例 | 是否保密 |
|---|---|---|
| 商户号(Merchant ID) | M00000001 | 否,它只表明你是谁。 |
| API Key | cvb2b_... | 是,它既是你的密码,也是签名用的密钥。 |
- 账户审核通过后,CardV 会分配商户号。
- API Key 由 Owner 在商户后台创建(见 API Key 管理)。
- 正式环境和沙盒的 Key 各自独立,不能混用。
- 只有在 CardV 审核通过你的企业资料后,API 才能使用。此时
GET/account会返回"api_access_enabled": true。
#请求头
每个请求都要带这两个请求头:
X-Merchant-Id: M00000001
X-Api-Key: cvb2b_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPOST/orders 还要再带这三个:
X-Timestamp: 1790000000
X-Nonce: 0123456789abcdef0123456789abcdef
X-Signature: fd55fa31a3b6817d5368903dbb4bad4458381e024c17e688580bac96577acfedX-Timestamp:当前 Unix 时间(秒),和 CardV 服务器时间相差不能超过 5 分钟(300 秒)。X-Nonce:随机串,每次都不能重复。建议使用 32 位随机十六进制字符。X-Signature:签名,计算方法见下单签名,必须是小写十六进制。
GET 请求不需要签名。
#下单签名
把订单内容序列化成 JSON 文本,只序列化一次。签名和发送都用这同一份字节。
计算请求体哈希:
BODY_HASH= 请求体的 SHA-256,小写十六进制。把下面五行用换行符(
\n)连接起来,末尾不要加换行:POST /api/v1/orders <X-Timestamp> <X-Nonce> <BODY_HASH>计算签名:
X-Signature= 用完整的 API Key 作为密钥,对上面这段文本做 HMAC-SHA256,结果写成小写十六进制。
最常见的错误是:签名用的 JSON 和实际发送的 JSON 不是同一份。比如 {"a":1} 和 {"a": 1} 的字节就不一样。请确认你的 HTTP 库在签名之后没有改动请求体。
测试数据:用下面的数据验证你的签名代码(Key 是假的)。
API key cvb2b_TEST_ONLY_not_a_real_key_0123456789abcdef
Method POST
Path /api/v1/orders
X-Timestamp 1790000000
X-Nonce 0123456789abcdef0123456789abcdef请求体(108 字节,单行,末尾无换行):
{"external_order_id":"TEST-0001","items":[{"sku_id":"S000001","quantity":1,"expected_unit_price":"9.2500"}]}应得到的结果:
BODY_HASH b0545ae25d54b219f27d8bd90e4dcbf26cf0d491f53982da67b8eef0a1a59960
X-Signature fd55fa31a3b6817d5368903dbb4bad4458381e024c17e688580bac96577acfed#示例代码
以下示例都能算出上面测试数据的结果。Key 请从密钥管理系统读取,不要写在代码里。
cURL(bash + OpenSSL)
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)
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)
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
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。响应格式如下:
{"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。正式环境和沙盒需要分别操作。
- 创建:新建一个 Key 并命名。此时后台还不会显示完整的 Key。
- 查看(Reveal):CardV 会给你发一封含 6 位验证码的邮件,10 分钟内有效(最多尝试 5 次)。输入验证码后可以看到完整的 Key,请立即保存。每次查看都会记录日志。
- 停用:不再需要的 Key 请及时停用,立即生效且不可恢复。
在不停机的情况下更换 Key:
- 创建并查看一个新 Key。
- 把新 Key 部署到你所有的服务器。
- 在商户后台确认旧 Key 已经没有调用。
- 停用旧 Key。
建议至少每年更换一次 Key;接触过 Key 的人员离职时也要更换。如果怀疑 Key 已经泄露,请先停用,再排查原因。
接入遇到问题?请发送邮件至 [email protected],并附上 Merchant ID 与订单号或请求 ID。