接入总览
商户通过 OpenAPI 创建入金、出金订单,并查询汇率、余额与订单状态。生产请求必须同时通过商户状态、OpenAPI 权限、IP 白名单、RSA 请求加密、HMAC-SHA256 签名、5 分钟时间窗和一次性 nonce 校验。
上线前检查
- 确认商户状态正常且 OpenAPI 权限已由管理员启用。
- 在商户后台复制 UID;业务 JSON 和外层 envelope 的 merchantNo 必须都使用该 UID。
- 配置所有调用服务器的固定出口 IPv4 或 IPv4 CIDR,禁止使用全网通配地址。
- 配置可公网解析的回调 URL,并确保接收端不会重定向请求。
- 安全保存请求加密公钥和仅显示一次的 HMAC signing secret;同步系统时间。
- 回调接收端准备 eventId 唯一约束、验签逻辑,以及可选的回调解密私钥。
请求加密与签名
先把包含 merchantNo 的业务 JSON 按 UTF-8 编码,并使用平台请求加密公钥执行 RSA PKCS#1 v1.5 分段加密,Base64 后得到 encryptedData;再用商户 HMAC signing secret 对六行 canonical string 签名,输出无填充 Base64URL。
内外层 merchantNo 必须一致。
按 modulusBytes - 11 分段,拼接密文后使用标准 Base64。
对精确六行字符串签名,结果使用无填充 Base64URL。
请求 canonical string 按以下顺序组成,使用 LF 换行且末尾没有换行。method 必须是大写 POST,path 只包含 URL pathname,不包含域名、query 或 fragment。
merchantNo=MCH-DEMO-1001 method=POST path=/api/open/order/createBuyOrder timestamp=1783737600000 nonce=dGVzdC1ub25jZS0wMDAx encryptedData=BASE64_RSA_CIPHERTEXT
timestamp 必须是 13 位 Unix 毫秒;nonce 必须是 16-128 位 Base64URL/安全字符且每次请求唯一。签名计算必须覆盖最终发送的 encryptedData,任何空格、路径或密文变化都会使签名失效。
密钥管理
由平台生成,可复制到调用服务。平台私钥加密存储且不对商户展示。轮换后旧公钥立即不能用于新请求。
由平台生成,只在注册成功或轮换时显示一次,同时用于请求签名和回调验签。只能保存在服务端密钥管理系统。
由商户浏览器本地生成且永不上传;平台只保存公钥。轮换时保留旧私钥,直到旧回调任务全部结束。
回调验签与解密
每个回调 Body 都是带 signature 的外层 envelope。必须先用同一 HMAC signing secret 验签并原子占用 eventId,再判断 payload 是否为 RSA-OAEP-SHA256 + AES-256-GCM 加密对象并解密,最后校验内外层商户号和订单号一致。
签名覆盖收到的 payload 原始对象语义:对象 key 递归按字典序排序、忽略值为 undefined 的对象字段、数组保持原顺序,并输出紧凑 JSON。必须先验签,再解密 payload。
{
"eventId": "cb_1783737600000_abcd1234",
"merchantNo": "MCH-DEMO-1001",
"orderNo": "PI1783737600000ABC123",
"timestamp": "1783737600000",
"payload": {
"encryption": "RSA_OAEP_SHA256_AES_256_GCM",
"merchantNo": "MCH-DEMO-1001",
"orderNo": "PI1783737600000ABC123",
"encryptedKey": "BASE64_RSA_OAEP_SHA256_KEY",
"iv": "BASE64_12_BYTE_IV",
"tag": "BASE64_GCM_TAG",
"encryptedData": "BASE64_AES_GCM_CIPHERTEXT",
"encryptedAt": "2026-07-12T02:00:00.000Z"
},
"signature": "UNPADDED_BASE64URL_HMAC_SHA256"
}回调 canonical string 同样使用 LF 且无末尾换行:
eventId=cb_1783737600000_abcd1234 merchantNo=MCH-DEMO-1001 orderNo=PI1783737600000ABC123 timestamp=1783737600000 payloadSha256=BASE64URL_SHA256_OF_STABLE_PAYLOAD_JSON
- 1
校验 eventId、merchantNo、orderNo、timestamp、payload、signature,并拒绝超过 5 分钟的回调。
- 2
计算 stable JSON payload SHA-256 和五行 canonical string,常量时间校验 HMAC signature。
- 3
在数据库事务中用唯一约束原子占用 eventId;重复 eventId 不得再次更新订单或资金。
- 4
payload.encryption 为 RSA_OAEP_SHA256_AES_256_GCM 时,用本地私钥解开 AES key,再用 AES-256-GCM 解密。
- 5
核对解密后 MerchantNo/OrderNo 与已认证外层字段一致,幂等推进本地业务状态。
- 6
成功后返回 HTTP 2xx,且 Body 为空、{"code":200}、{"success":true} 或纯文本 success;其他响应会进入重试。
幂等、重试与限流
创建入金和出金时必填,并在同一商户内唯一。超时重试保持原值;收到 40007 后调用 queryOrder 回查。
每一次发送和重试都生成新的 nonce、timestamp 和 signature。可复用同一业务 payload/encryptedData。
仅重试网络错误、408、429 和 5xx,并使用退避。普通 4xx 是确定性请求错误,应修正后再发。
每商户与来源 IP 默认每分钟 120 次;HTTP JSON 请求体上限 96 KB,encryptedData 解码后上限 8 KB。
全局错误码
| 错误码 | 说明与处理建议 |
|---|---|
| 40000 | 通用业务参数错误,也用于解密失败或订单不存在;请结合 message 检查具体原因。 |
| 40001 | 商户号无效,merchantNo 不存在或填写错误。 |
| 40002 | 商户账户已被暂停,当前不能继续创建订单或查询资金。 |
| 40003 | 商户未开启 OpenAPI 权限,请在商户 API 安全配置中启用接口访问。 |
| 40004 | 未配置回调地址,请在商户后台设置默认 callbackUrl,或在创建订单时传入合法回调地址。 |
| 40005 | 请求来源 IP 不在商户白名单内,请将服务器出口 IP 加入白名单后重试。 |
| 40007 | 商户订单号重复,同一商户下 merchantOrderNo 必须唯一;重复请求请先查询原订单状态。 |
| 40010 | 暂不支持该法币币种,请使用平台已开启的 CNY、USD、HKD、THB、VND 等币种。 |
| 40011 | 当前没有匹配的流动性池,平台暂时无法为该币种或金额创建订单。 |
| 40012 | 没有可用广告,或订单金额不在广告支持的最小/最大额度范围内。 |
| 40013 | 入金可能表示广告额度不足;出金表示商户可用 USDT 余额不足。 |
| 40014 | 订单金额不是有效正数、精度或金额拆分无效,或超过业务限额。 |
| 40015 | 该客户姓名已被风控名单拦截,请联系平台客服或管理员处理。 |
| 40016 | 生产模式已禁用明文 JSON 请求,请使用加密且签名的 OpenAPI envelope。 |
| 40017 | 回调地址不允许访问 |
| 40018 | IP 白名单包含不安全地址 |
| 40019 | HMAC-SHA256 请求签名缺失或无效,请检查签名密钥和 canonical string。 |
| 40020 | timestamp 无效或与平台时间相差超过 5 分钟。 |
| 40021 | nonce 已被使用;每次请求和每次重试都必须生成新 nonce。 |
| 40022 | 创建入金或出金订单时 merchantOrderNo 不能为空。 |
| 40023 | HTTP 请求体超过接口允许大小。 |
| 40024 | HTTP 请求体不是有效 UTF-8 JSON。 |
SDK 与完整示例
docs/openapi-sdk/node/client.js仅使用内置 crypto 和 fetch。
docs/openapi-sdk/python/client.pypip install cryptography
docs/openapi-sdk/php/client.phpcomposer require phpseclib/phpseclib:^3.0
docs/openapi-sdk/java/OpenApiExample.java使用 JDK HttpClient 与 JCA。
const crypto = require("crypto");
const baseUrl = "https://pay.example.com";
const merchantNo = "MCH-DEMO-1001";
const signingSecret = process.env.CORRIDORPAY_SIGNING_SECRET;
const platformPublicKey = `-----BEGIN PUBLIC KEY-----
PASTE_PLATFORM_PUBLIC_KEY_HERE
-----END PUBLIC KEY-----`;
const payload = {
merchantNo,
amount: 1000,
currencyType: "CNY",
merchantOrderNo: "demo-001",
customerName: "Alice",
redirectUrl: "https://merchant.example.com/return",
hideUsdt: false,
paymentType: 1,
callbackUrl: "https://merchant.example.com/callback",
customerCountry: "CN",
customerPhoneNumber: "13800000000",
customerIdCardNumber: "110101199001010000"
};
function encryptPayload(data) {
const key = crypto.createPublicKey(platformPublicKey);
const blockSize = Math.floor((key.asymmetricKeyDetails?.modulusLength || 2048) / 8);
const maxPayloadBlock = blockSize - 11;
const input = Buffer.from(JSON.stringify(data), "utf8");
const encryptedBlocks = [];
for (let offset = 0; offset < input.length; offset += maxPayloadBlock) {
encryptedBlocks.push(
crypto.publicEncrypt(
{ key, padding: crypto.constants.RSA_PKCS1_PADDING },
input.subarray(offset, offset + maxPayloadBlock)
)
);
}
return Buffer.concat(encryptedBlocks).toString("base64");
}
async function main() {
const encryptedData = encryptPayload(payload);
const timestamp = String(Date.now());
const nonce = crypto.randomBytes(24).toString("base64url");
const canonical = [
`merchantNo=${merchantNo}`,
"method=POST",
"path=/api/open/order/createBuyOrder",
`timestamp=${timestamp}`,
`nonce=${nonce}`,
`encryptedData=${encryptedData}`
].join("\n");
const body = {
merchantNo,
timestamp,
nonce,
encryptedData,
signature: crypto.createHmac("sha256", signingSecret).update(canonical, "utf8").digest("base64url")
};
const response = await fetch(baseUrl + "/api/open/order/createBuyOrder", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body)
});
console.log(response.status, await response.text());
}
main().catch((error) => {
console.error(error);
process.exit(1);
});全部接口
| 方法 | 接口路径 | 说明 |
|---|---|---|
| POST | /api/open/order/createBuyOrder | 创建商户入金订单,平台分配交易员收款方式并返回平台订单号和收银台地址。 |
| POST | /api/open/order/getBuyRate | 按商户号和法币币种查询当前入金汇率。 |
| POST | /api/open/order/getBalance | 查询商户 USDT 账面总余额和冻结余额。 |
| POST | /api/open/order/createSellOrder | 创建法币出金订单,平台冻结商户对应 USDT,等待后台分配和处理。 |
| POST | /api/open/order/getSellRate | 查询出金汇率,支持普通和加急策略。 |
| POST | /api/open/order/queryOrder | 按平台订单号或商户订单号查询入金/出金订单状态。 |