← 商户后台

中金支付 · 开发接口

V1.8 · 2026-11-02 · 先跑通只读请求,再接入真实交易

快速开始:按这个顺序对接

商户只对接千词支付的 JSON 接口,不直接调用中金 XML 接口,也不需要在商户系统保存中金证书。本文示例中 YOUR_*、merchant.example.com 等都是占位内容,不是实际账号、单号或可用地址。

① 准备凭证与能力

取得平台 AppId / App Secret;确认中金通道、API、所需交易能力已开启,以及允许的扣款金额。

② 先读银行,再绑卡

先确定借记卡还是贷记卡;按同一 card_type 查询银行清单,绑卡成功后保存平台 bind_id。

③ 处理状态与通知

扣款/退款保留原商户单号;同步不明确就查询原单。接收重复通知时幂等处理,不重复发货或记账。

  1. 复制 Node.js 18 示例,只调用一次 /banks,先验证公网连接、AppId 和签名。
  2. 选借记卡传 DEBIT_CARD(默认);选贷记卡传 CREDIT_CARD。贷记卡不是全部不支持,但必须单独开通且只能使用银行一键方式。 两类银行名单可能不同,以本商户本次查询结果为准。
  3. 发起 /bindcard,引导持卡人打开 redirect_url;只有查询/通知确认 bound 后才能扣款。不要用浏览器回跳判断成功。
  4. 用该中金 bind_id 发起 /pay。保存 out_trade_no、trade_no;已明确成功/失败可同步处理,paying 或超时则查询原单。
  5. 需要退本金时发起一次全额 /refund;需要撤销银行签约时调用 /unbind。退款不等于解绑,解绑也不会退款。
  6. 联调验收回调签名、HTTP200 + SUCCESS 应答及主动查询兜底,再逐步开放实际业务。
三个判断不要混淆: HTTP200 ≠ 交易成功;code=0 ≠ 交易成功;回到 return_url ≠ 签约成功。最终按各业务的字符串状态判断,不能把不同业务的中金数字状态码混用。

1. 接入约定

专用网关:https://qiancipay.com/api/openapi/cpcn

全部接口使用 POST application/json; charset=utf-8。沿用千词支付已有 app_id 和平台签名密钥,不向商户下发中金私钥。此接口需由平台为各商户单独开放、配置独立机构与证书、明确固定技术费及通道成本并验证;完成后商户后台显示本页入口。文档可见不等于交易已开启,真实新交易还须全局及该商户对应能力开关均开启。

生产证书、机构号、收款配置和权限确认齐备且平台启用后,才能发起真实交易。本地证书验证不代替本商户实际银行、卡型和通知地址的联调验收。支持实际获批银行的借记卡,以及单独开通的贷记卡银行一键绑卡;贷记卡启用不等于全局交易及商户能力已开启。原首信易、快钱商户无需修改代码,其计费与退款规则不因本次中金调整而改变。
  • 所有金额为整数“分”:59元传5900,59.9元传5990。不要传浮点元。
  • 响应格式 {"code":0,"msg":"ok","data":{...}}。HTTP200只说明HTTP请求完成;先判断 code,再检查 data.status。中金已建单后的明确交易失败也可能返回 code=0、data.status=failed,不能把 code=0 或 msg=ok 当作扣款成功;非0业务码按错误含义处理。
  • 中金与首信易、快钱的绑卡记录不可混用。中金业务统一使用本页专用接口,不能仅向旧接口增加 channel=cpcn。常规手工扣款/退款表单不受理中金交易;商户后台提供独立中金扣款、退款和解绑面板。
  • 平台技术服务费、扣费模板、绑卡成功周期限制、月度限制、商户停用及扣款风控继续有效。失败不占成功额度,待确认订单保留额度,避免并发重复成功。
  • 时间采用北京时间。结算安排以本商户与中金的约定为准;受理成功不等同于退款已到账。对接时使用平台字符串业务状态,不直接把中金底层数字状态码套用到各业务。
  • 本页金额、单号均为报文示例,不是各商户默认价格;银行能力和费用以该商户当前配置及原单快照为准。

接口速查

以下全部为 POST,完整地址为“专用网关 + 路径”;每次调用均携带第2节公共签名参数。示例单号及金额仅用于说明,不是生产请求。

流程 / 路径业务参数判断依据
银行发现 /bankscard_type(可选)取得本商户当前卡型银行清单及 binding_way。
签约绑卡 /bindcardout_bind_no、out_user_id、身份及银行要素;可选通知/回跳地址一键方式打开 redirect_url;短信方式按 need_sms 确认。最终看 bound,不看浏览器跳回。
短信确认 /bindcard/confirmbind_id、sms_code仅短信绑卡使用,不发起新签约。
绑卡查询 /bindcard/querybind_id 或 out_bind_nosubmitting / bound / failed / unbound。
扣款 /payout_trade_no、bind_id、amount;建议 notify_urlsuccess / failed / paying;paying 保留原商户单号。
扣款查询 /pay/queryout_trade_no查询原支付,不能通过换号查单或重新扣款。
全额退款 /refundout_trade_no、out_refund_no、amount;可选 reason、notify_url仅原支付全额,processing 不等于已成功到账。
退款查询 /refund/queryout_refund_noprocessing / success / failed;费用状态不替代本金 status。
主动解绑 /unbindbind_id只有 unbind_status=success 且 status=unbound 才确认解绑。
解绑查询 /unbind/querybind_id读取原解绑结果,不再次发起解绑。

中金账户余额、提现、清算文件及电子回单不在本页开放范围。后台“中金对接文档”与本页使用同一份接口说明。

2. 签名与鉴权

公共字段规则
app_id现有平台 AppId
timestampUnix秒时间戳,与平台偏差不超过300秒
nonce每次 HTTP 请求使用新的随机字符串,包括查询和幂等重试
signHMAC-SHA256,64位小写十六进制

除 sign 外的所有非空字段,按键名字典序排序,按 k=v&k=v 拼接,使用平台 App Secret 计算 HMAC。值不进行 URL 编码;不要额外发送嵌套对象或未定义字段。HTTP JSON编码的转义不属于签名原文。

可复制的 Node.js 18 客户端(仅自动执行只读银行查询)

保存为 cpcn-client.cjs,在服务器安全设置环境变量 QC_APP_ID、QC_APP_SECRET 后运行 node cpcn-client.cjs。不要把真实密钥写入代码、网页、URL或日志。例子默认读取贷记卡银行;尚未开通贷记卡时,把最后的 CREDIT_CARD 改为 DEBIT_CARD 验证借记卡能力。

'use strict';
const crypto = require('node:crypto');
const BASE = 'https://qiancipay.com/api/openapi/cpcn';
const APP_ID = process.env.QC_APP_ID;
const APP_SECRET = process.env.QC_APP_SECRET;

function sign(params) {
  const text = Object.keys(params)
    .filter(k => k !== 'sign' && params[k] != null && params[k] !== '')
    .sort().map(k => `${k}=${params[k]}`).join('&');
  return crypto.createHmac('sha256', APP_SECRET).update(text, 'utf8').digest('hex');
}

async function cpcnRequest(apiPath, fields = {}) {
  if (!APP_ID || !APP_SECRET) throw new Error('请先设置 QC_APP_ID / QC_APP_SECRET');
  const allowed = new Set(['/banks', '/bindcard', '/bindcard/confirm', '/bindcard/query',
    '/pay', '/pay/query', '/refund', '/refund/query', '/unbind', '/unbind/query']);
  if (!allowed.has(apiPath)) throw new Error('请使用本文列出的接口路径');
  const body = { ...fields, app_id: APP_ID,
    timestamp: String(Math.floor(Date.now() / 1000)),
    nonce: crypto.randomBytes(16).toString('hex') };
  body.sign = sign(body);
  let response, result;
  try {
    response = await fetch(BASE + apiPath, {
      method: 'POST', headers: { 'Content-Type': 'application/json; charset=utf-8' },
      body: JSON.stringify(body), signal: AbortSignal.timeout(15000)
    });
    result = await response.json();
  } catch (cause) {
    const error = new Error('请求或响应未确认:保留原业务单号,先查原单,不要自动换号重发', { cause });
    error.code = 'RESULT_UNKNOWN'; throw error;
  }
  if (!response.ok) {
    const error = new Error(`HTTP ${response.status}:响应未确认,请查询原业务单号`);
    error.code = 'RESULT_UNKNOWN'; error.http_status = response.status; throw error;
  }
  if (!result || result.code !== 0) {
    const error = new Error(result?.msg || result?.message || '响应缺少有效平台业务码');
    error.code = result?.code ?? 'RESULT_UNKNOWN'; error.http_status = response.status;
    error.result = result; throw error; // 5030同样先查原单;这里绝不自动重试。
  }
  return result.data; // 仍需判断 data.status;code=0也可能是明确失败。
}

// 以下自动运行的唯一请求是银行列表,不绑卡、不扣款、不退款、不解绑。
if (require.main === module) {
  cpcnRequest('/banks', { card_type: 'CREDIT_CARD' })
    .then(data => console.log(JSON.stringify(data, null, 2)))
    .catch(error => { console.error(error.code || 'CLIENT_ERROR', error.message); process.exitCode = 1; });
}
module.exports = { cpcnRequest, sign };

签名使用最终上送的字段和值,不要先 URL 编码、不要签名后再改 amount/notify_url。中文按 UTF-8,金额单位为整数分;可选参数不传就不要自行加默认空对象。每次请求生成新的 timestamp/nonce,但原业务单号必须保持。

业务代码如何使用(以下仅展示,不自动执行)

// 在你的业务服务中按实际时机调用,先完成参数、持卡人授权和金额校验。
// const { cpcnRequest } = require('./cpcn-client.cjs');
// const card = await cpcnRequest('/bindcard', bindFields); // 参数见下一节
// const pay = await cpcnRequest('/pay', payFields);        // 会真实扣款
// if (pay.status === 'success') { /* 幂等确认成功 */ }
// else if (pay.status === 'failed') { /* 记录 pay.fail_reason,不重复发货 */ }
// else { /* 保存原 out_trade_no,用 /pay/query 查询,不生成新号 */ }
// const refund = await cpcnRequest('/refund', refundFields); // 会申请真实退款
// const unbind = await cpcnRequest('/unbind', { bind_id: existingBindId }); // 会撤销签约

后续各接口给出的“业务参数示例”不含公共字段,也不是完整可直接发送的签名请求;请交给上面的 cpcnRequest 合并公共参数并签名。代码中没有资金请求自动重试循环,请勿在外层套用通用 HTTP 自动重试器。

3. 绑卡与解绑

POST /banks

公共字段之外可选传入 card_type:DEBIT_CARD 为借记卡(默认),CREDIT_CARD 为贷记卡(信用卡)。返回所选 card_type、该卡型对应的 banks:[{code,name}] 和 binding_way(40一键/10短信)。银行编码必须来自所选卡型的清单,借贷记名单不能合并,也不能直接沿用首信易/快钱编码;不传 card_type 的旧商户仍使用借记卡。

贷记卡需由平台单独启用,仅支持银行一键绑卡(binding_way=40),当前不支持短信绑卡(10)或CVN方案。商户仅传上述 card_type 字符串,不要传中金内部 BankCardType 数字10/20,也不要自行传 binding_way 改变通道配置。

各商户独立银行能力

实际请求前须按 card_type 调用 /banks 获取本商户最新银行编码和名称,不应硬编码其他商户的银行矩阵。借记卡和贷记卡分别配置、分别判断停用状态;银行支持某项业务不代表本机构已获相应权限。未开通的卡型不能通过更换 bank_code 绕过。

POST /bindcard

业务字段必填说明
out_bind_no是商户绑卡请求号,≤96字符;同一请求重试保持不变
out_user_id是商户用户号,≤64字符;不同签约记录独立计周期
name / id_card_num / phone是真实姓名、18位身份证、银行预留手机号;不得使用测试身份发起生产请求
bank_code是/banks 按同一 card_type 返回的中金银行数字编码
card_type否DEBIT_CARD(借记卡,默认)或 CREDIT_CARD(贷记卡,仅开通后的一键绑卡);不得传数字10/20
bank_card_no条件借记卡短信绑卡必填;银行一键绑卡由银行页面选卡,不必传
notify_url建议签约结果通知地址,HTTP/HTTPS,≤500字符
return_url建议银行一键签约后同步返回的商户地址,≤1000字符;原查询参数保留,不额外附加成功标志

以下为一键绑卡业务参数示例;尖括号内容须替换为经持卡人授权的真实资料,并合并公共参数后签名。不要把示例占位内容提交到生产环境。

{
  "out_bind_no":"YOUR_BIND_001","out_user_id":"YOUR_USER_001",
  "name":"<真实姓名>","id_card_num":"<18位身份证>",
  "phone":"<银行预留手机号>","bank_code":"<从banks取得的编码>",
  "card_type":"DEBIT_CARD",
  "notify_url":"https://merchant.example.com/notify/bind",
  "return_url":"https://merchant.example.com/bind/result"
}

返回 bind_id(平台绑卡编号)、status、channel:cpcn、card_type(DEBIT_CARD/CREDIT_CARD)、redirect_url、need_sms、card_mask。一键绑卡有 redirect_url 时引导用户打开;平台签约跳转会话有效期30分钟,银行页面有效期以银行实际提示为准。同步返回页不表示成功,商户应以通知/查询为准。

{"code":0,"msg":"ok","data":{
  "bind_id":12345,"out_user_id":"YOUR_USER_001","channel":"cpcn",
  "status":"submitting","bank_code":"104","card_type":"DEBIT_CARD",
  "card_mask":"","redirect_url":"https://qiancipay.com/bind/cpcn/redirect/EXAMPLE_TOKEN",
  "need_sms":false
}}

POST /bindcard/confirm

字段:bind_id、sms_code(6位)。仅借记卡短信快捷绑卡使用;借记卡或贷记卡一键绑卡均在银行页面完成,无需调用。验证码失败不直接等同于原签约最终失败,应查询确认;本版不提供短信重发接口。

POST /bindcard/query

字段:bind_id 或 out_bind_no(请求超时未取得bind_id时使用,二选一即可)。返回绑定记录的 card_type(DEBIT_CARD/CREDIT_CARD)及本地已确认状态:submitting待确认、bound成功、failed失败、unbound已解绑;失败时有 fail_reason。后台通过上游通知和定时查询收敛状态,查询不会重复绑卡。暂时没有 redirect_url 不代表签约成功或失败,仍应查询原 out_bind_no/bind_id,不要自动生成新的签约号。

POST /unbind

完整地址:https://qiancipay.com/api/openapi/cpcn/unbind。业务必填字段为本商户的中金 bind_id,同时携带原有公共字段 app_id、timestamp、nonce、sign;签名方式不变。无需上送姓名、身份证、卡号或中金证书,也不要自行生成新的解绑流水。平台使用该绑定的原中金流水和原证书版本发起解绑。

只接受已成功绑定的中金记录。code=0不等于已经解绑,必须同时检查 data.unbind_status 与原卡状态 data.status:只有确认成功后,卡状态才变为 unbound。处理中卡仍可能显示 bound,不得据此认定解绑成功;超时或结果未知时只查询原 bind_id,不要换号或重新绑定来重试。

{"code":0,"msg":"ok","data":{
  "bind_id":12345,"channel":"cpcn","status":"bound",
  "unbind_status":"processing","unbind_fail_reason":null,
  "last_checked_at":"2026-10-31 12:00:00"
}}

POST /unbind/query

完整地址:https://qiancipay.com/api/openapi/cpcn/unbind/query。公共签名参数与 /unbind 相同,业务字段仍为 bind_id。该接口只读取平台本地的已确认解绑状态,不重新发起解绑;平台通过中金通知和后台查询核验原解绑结果。

字段/取值含义与处理
status原绑卡状态;解绑确认成功时为 unbound。原有绑卡查询接口及字段保持兼容。
unbind_status = not_started该绑定尚未发起解绑,不代表当前一定具备解绑资格。
unbind_status = processing解绑已提交或结果待确认,继续查询原 bind_id,不重复提交。
unbind_status = success解绑已确认成功,卡状态为 unbound;重复请求返回原结果,不重新创建解绑交易。
unbind_status = failed本次解绑已明确失败;查看 unbind_fail_reason 并联系平台核实,不自动清除失败记录或发起新尝试。
unbind_fail_reason解绑失败原因;没有明确失败时为空。原卡尚未解绑,不应当作解绑成功。
last_checked_at最近处理或查询时间,北京时间;未记录为 null,时间本身不是成功凭证。

明确失败的提交可能返回业务码 4121,并携带可查询的解绑状态;查询失败或 5030 不代表原解绑失败。已经存在的处理中或失败记录不会因为再次调用而自动重发上游指令。关闭新绑卡、扣款或全局新交易能力,不妨碍已有有效授权的主动解绑;账户、合作签约及API权限仍按原有鉴权规则检查。

解绑成功后,这条绑卡记录不能继续用于新扣款;需要再次使用时,应由持卡人重新签约。解绑不是扣款撤销或退款接口:已发往上游的扣款按其实际结果处理,尚未执行的请求仍受绑卡状态检查;不会自动退还历史交易,也不改变已有退款处理。成功时沿用该绑卡原来配置的 notify_url,发送原 event:bindcard、status:unbound 通知,不新增事件类型;原绑卡未设置通知地址则不推送,请查询确认。后台按钮需明确确认后才提交,不会因为打开页面或查询状态而执行解绑。

4. 扣款与查询

POST /pay

业务字段必填说明
out_trade_no是商户扣款单号,≤64字符;在同商户所有支付通道之间也不能重复使用
bind_id是本商户已成功的中金绑卡编号
amount是正整数分,须符合商户扣费模板
notify_url建议最终扣款结果通知地址
goods_name否商品/服务说明,≤100字符

业务参数示例(需合并公共签名参数):

{"out_trade_no":"YOUR_ORDER_001","bind_id":12345,"amount":5990,
 "goods_name":"商品服务","notify_url":"https://merchant.example.com/notify/pay"}
{"code":0,"msg":"ok","data":{
  "out_trade_no":"YOUR_ORDER_001","trade_no":"Q...",
  "pay_order_no":"20261027...","amount":5990,"fee":100,
  "status":"paying","channel":"cpcn"
}}

平台持久化订单后进入独立队列,在约5秒同步等待窗口内已确认的结果返回 success 或 failed;排队中、上游超时或结果未确认返回 paying。失败时附 fail_reason。fee 仅表示平台技术服务费,不包含中金通道费;以原支付的费用快照为准,不由当前配置追溯重算。

中金独立计费

每个商户分别约定中金固定技术服务费(元/笔)和通道成本;平台技术费从商户预付费余额扣收,与中金通道费分开。固定技术费必须明确设置,0表示明确不收取;未配置时不能开启新交易,不会默默沿用首信易、快钱的技术费率。通道成本按本商户费率及单笔最低费配置计算。订单创建时保留原配置及费用快照,后续换证或调价不改变原单计费;首信易、快钱既有计费不受影响。

POST /pay/query

字段:out_trade_no。返回同样的订单字段;全额退款后的支付单为 refunded,表示原支付成功且已退款。兼容历史数据时仍可能读到 part_refunded,但本版不接受新的部分退款。查询不重新扣款。失败订单如果确需重新支付,应核实失败后使用新的商户单号。

5. 全额退款与查询

POST /refund

必填 out_trade_no(原支付单)、out_refund_no(商户退款单号≤64字符)、amount(正整数分);选填 reason(≤200字符)、notify_url。中金仅支持原支付全额退款,amount 必须等于原支付本金;不支持部分退款或把一笔原支付拆成多笔退款。部分退款返回 4306,平台在本地拒绝,不向中金发送退款请求。未完成退款会预占可退本金,防止并发重复退款。

业务参数示例(需合并公共签名参数,金额须等于该原支付本金):

{"out_trade_no":"YOUR_ORDER_001","out_refund_no":"YOUR_REFUND_001",
 "amount":5990,"reason":"客户申请全额退款",
 "notify_url":"https://merchant.example.com/notify/refund"}
{"code":0,"msg":"ok","data":{
  "out_refund_no":"YOUR_REFUND_001","refund_no":"RF...",
  "pay_refund_no":"20261027...","amount":5990,
  "status":"processing","channel":"cpcn",
  "refund_fee_amount":0,"refund_fee_charged_amount":0,
  "refund_fee_status":"not_required","returned_channel_fee":null
}}

POST /refund/query

字段:out_refund_no。本金退款状态 status:processing待确认,success中金确认退款成功,failed明确失败(含fail_reason)。中金受理成功不等于退款成功;实际到账时间以银行为准。

查询读取平台本地已确认的订单状态,由中金异步通知及平台后台主动对账更新,并非每次商户查询都直连中金。查询不会重新发起退款;处理中请保留原退款单号,不要换号重复提交。

退款待确认、退票等进入平台核实流程,对外暂为 processing,保留额度,不自动再退。平台核实处理后,以最新查询结果及已配置通知地址收到的终态通知为准。即使曾收到 success,后续银行退票也可能触发人工核实,商户仍应保留主动查询兜底,不能只凭首次受理报文认定到账。

退款核验信息(兼容新增,可选)

退款响应、退款查询可能增加 refund_tracking 对象,不改变原有 status、单号或金额字段。历史记录或暂未取得追踪信息时字段可为 null/unknown;不要推定未记录就代表未收到通知或未进行查询。

refund_tracking 字段说明
last_verified_at最近核验时间,北京时间 YYYY-MM-DD HH:mm:ss;未记录为 null。
result_sourcesync_response 同步响应、notification 异步通知、query 主动查询、unknown 未记录。
upstream_status最近核验的中金原始状态;未记录为 null。业务仍按原 status 判断,不凭此字段重新退款。
notification_modeconfigured 已配置该笔商户通知地址,not_configured 未配置。已配置不等于商户已收到;实际投递情况以通知记录为准。

退款手续费规则:原费用不退,不再补收

中金退款不退原通道手续费,平台原技术服务费也不退。平台只退原支付本金,不因退款从商户预付费余额额外扣费,也不从持卡人退款本金扣除费用。例如原通道手续费200分、技术服务费100分,则退款后仍保留原300分费用,不再追加200分。示例价格不适用于全部商户。

本规则统一用于中金退款,包括原支付费用快照曾启用补收的旧订单;旧补收开关、迟到通知、主动查询及充值均不会重新触发退款手续费补收。已实际发生的历史费用流水保留原记录,不自动冲正或再次收费。首信易、快钱计费与退款规则不变。

商户请求、签名、本金退款状态及异步通知流程保持不变。为兼容已有接入,下列可选费用字段继续保留,金额均为整数分;费用状态独立于本金 status,不能据“无需补收”认定退款已成功。费用信息变化不会另行触发本金终态通知,历史费用以查询和账单为准。

字段含义
refund_fee_amount无需补收的记录为0,不重新计算或修改原支付已收费用;历史 charged 或已有实收凭证的记录仍返回原补收目标金额,仅保留历史事实,不代表新的收款目标,也不会再次扣收。
returned_channel_fee已记录的中金退费金额;未记录时为 null 或不提供,不因新规则伪造为0。该字段不会触发补收。
refund_fee_charged_amount历史实际补收金额;未发生补收为0,已有真实流水保留。
refund_fee_status独立手续费状态,见下表;不得用来替代本金退款 status。
refund_fee_status说明
not_required无需补收;中金原通道手续费不退,退款不额外收取费用。该状态不代表本金退款已成功。
charged历史已补收记录;保留已发生的真实流水,不自动冲正或再次收费。
review历史费用需人工核账;不自动强制扣收。
awaiting_refund / awaiting_return / balance_due旧版历史状态,分别表示曾待确认本金、待核实退费、余额不足待补收;新规则已停用补收,不会因查询、回调或充值自动扣费。

6. 异步通知

平台向该笔请求的 notify_url 发送 JSON,沿用相同 HMAC-SHA256 算法。通知包含公共签名字段及 event(bindcard/payment/refund)、channel:cpcn 和相应业务编号、金额、状态。绑卡通知(event=bindcard)还包含绑定记录的 card_type(DEBIT_CARD/CREDIT_CARD),该字段参与验签。不要把中金底层 XML 通知地址配置成商户地址。

notify_url 应使用商户真实可公网访问的 HTTPS 接收地址,不要填写 localhost、127.0.0.1、内网地址或后台页面地址。localhost 对平台表示平台服务器自身,不是商户服务器。请确认平台可访问该路径、证书有效且服务支持 POST JSON;平台当前的格式校验不等于已替商户验证公网可达性。return_url 仅用于持卡人浏览器回跳,不能替代 notify_url。
{
  "app_id":"YOUR_APP_ID","timestamp":"...","nonce":"...","sign":"...",
  "event":"payment","channel":"cpcn","out_trade_no":"YOUR_ORDER_001",
  "trade_no":"Q...","pay_order_no":"20261026...",
  "amount":5990,"fee":100,"status":"success"
}
  • 先验证签名、商户归属、业务单号及金额,再持久化处理。请兼容未知新增字段参与验签。
  • 处理成功返回 HTTP200,正文纯文本 SUCCESS(大小写均兼容),不要返回JSON或HTML页面。
  • 通知可能重复或早于接口响应;以业务单号+事件幂等处理,避免重复发货。同步已确认结果仍可能收到通知。
  • 通知签名的 timestamp/nonce 可能在重试时保持原值,不能直接套用请求接口300秒去重规则;应按业务状态做幂等验签。
  • 未确认通知按原平台规则退避重推,最多8次,失败可由管理端查看投递记录/重推;商户也应主动查询兜底。
  • 未传 notify_url 不推送,必须通过查询取得结果。商户后台手动退款不填写商户通知地址,结果请在后台查询,不会自动继承原支付的通知地址。浏览器 return_url 不可作为付款/签约成功凭证。
  • 本金退款与手续费处理独立,手续费后续状态变化以查询及账单为准,不新增本金终态回调事件;原有商户通知流程保持不变。

7. 错误与重试规则

业务码含义
4001–4007平台公共鉴权、API开通、商户/合作签约检查未通过
4100–4121绑卡参数、记录、银行能力或解绑失败
4201/4203/4204/4206/4208/4209金额/签约/预付费余额/单笔上限/日失败上限/商户风控拦截
4212/4213月度额度/签约周期成功次数/扣费模板金额拦截
4300–4310退款参数、原交易、可退金额或记录不合法
4306中金不支持部分退款;金额必须等于原支付本金,本地拒绝且不发送上游请求
4401/4403/4404中金配置未齐或未启用 / 未授权或跨通道单号 / 订单映射不存在
5030、HTTP异常或超时结果不确定;保持原业务单号查询,不得换单重发

业务状态速判:先区分是什么业务

业务已确认结果未确认时怎么做
绑卡bound 成功;failed 明确失败;unbound 已解绑submitting:保存原 out_bind_no/bind_id,等待持卡人完成授权并查原记录。
扣款success 成功;failed 明确失败;refunded 原支付成功后已退款paying:不重复发货、不换号重扣,按原 out_trade_no 查询。
退款success 中金确认退款成功;failed 明确失败processing:保留原 out_refund_no,等待通知/查原单,不再申请一笔。
解绑unbind_status=success 且 status=unbound 才成功;unbind_status=failed 为明确失败processing:只查询原 bind_id;not_started 只表示本地没有解绑记录,不是成功。

幂等维度:绑卡 商户+out_bind_no,扣款 商户+out_trade_no,退款 商户+out_refund_no,解绑使用原 商户+bind_id。重试必须保持原业务参数,换新的 timestamp/nonce 并重新签名。同号变更金额或绑卡不会修改已受理订单,也不会再次发起资金交易;不要依赖响应存在 idempotent 标志判断幂等,始终核对返回的原单号、金额和业务状态。

超时、5030或查询暂未找到(如4210/4310)均不能单独证明原扣款/退款失败;保留原号,先查询和联系平台核实,不要自动换号重发资金请求。明确 failed 后确需再次扣款或申请退款,应先处理原失败原因,再按新业务申请发起;原成功退款不得再退,原明确失败解绑记录须联系平台核实而非自动清除重试。

本版提供绑卡、短信确认、解绑、扣款、退款、本地状态查询和上游异步/主动对账。中金账户余额、提现、清算对账文件及电子回单未在此文档开放,不能直接调用SDK中的其它交易码。

8. 常见问题:现象 → 排查 → 处理

先看返回的是接口业务码,还是 data.status=failed 内的上游失败原因。下面的“处理”不代表可以自动再扣一次;不确定原交易结果时,始终查原单。

看得到中金文档,却提示“中金扣款/绑卡/退款尚未开启”(4401 / 4403 / 4005)

排查:文档入口、商户API权限、通道授权、证书配置和交易开关是不同层级;文档可见不等于交易已开。

处理:联系平台核对当前商户的生产/测试配置、API、全局及所需能力开关。不要换成其他商户AppId,也不要把中金请求发到旧网关。

能否重试:权限修正后按原业务核查再操作;开关关闭不会把已有在途订单自动判失败。

贷记卡怎么绑?为什么某银行借记卡能绑,贷记卡却报4104?

排查:不传 card_type 默认借记卡;贷记卡须显式传 CREDIT_CARD,且本商户需启用贷记卡、采用一键方式40。借贷记银行名单和暂停状态可能不同。

处理:先用 /banks + card_type=CREDIT_CARD 获取名单,绑卡仍传同一卡型及该名单里的 bank_code。若返回未开放或无目标银行,请平台核对实际权限,不要硬编码别家商户名单。

能否重试:卡型/银行权限明确后才按实际持卡人意愿重新申请;不能把信用卡按借记卡提交来绕过限制,也不能据“支持贷记卡”推定全部银行已获批。

签名失败4004、请求过期或重复4002,如何定位?

排查:确认 AppId 与平台 App Secret 配套,不是中金证书密码;核对最终JSON实际字段,sign自身不参与,null/空串不参与,其他字段排序后以 k=v&k=v 拼接,不URL编码。中文使用UTF-8,金额仍是整数分。

处理:不要签名后再改 amount、notify_url 或补字段;校准服务器时间,timestamp用秒不是毫秒,每个HTTP请求都用新nonce。排查日志只打印脱敏业务字段,不能打印密钥。

能否重试:先修正公共参数;已发送过的资金请求仍先查原业务单号,不能因本地报错就认定从未受理。

没有拿到签约链接、银行页面打不开或提示过期

排查:查看 status、need_sms、redirect_url。短信方式没有银行跳转链接;一键方式缺链接时,原记录也可能处于待确认,不能直接当失败。

处理:按原 out_bind_no/bind_id 查签约。平台跳转会话最长30分钟且只在 submitting 时提供链接;银行页面有效期以银行提示为准。若已bound就无需重新打开;若失败查看fail_reason;长期待确认或链接过期请联系平台核实原签约。

能否重试:不能自动换新绑卡号反复生成签约;核清原结果后再由持卡人进行新的授权。

用户已跳回return_url,为什么还不能扣款?

排查:浏览器回跳只是页面导航,不是银行签约成功凭据;原 return_url 查询参数保留,但平台不会据此额外附加成功标志。

处理:在商户自己的业务会话保存 out_bind_no/bind_id,回跳后查询 /bindcard/query,或以已验签通知确认bound。不要让“支付成功页”仅凭回跳就发货。

能否重试:submitting继续查原记录;失败先展示原因;unbound需持卡人重新签约。4203表示该中金卡未成功或已解绑,不能继续对它扣款。

HTTP200、code=0、msg=ok,但data.status却是failed

排查:这表示接口成功取得了业务结果,不表示业务成功。中金明确失败仍可能code=0,fail_reason才是本笔失败原因。

处理:分别编写success、failed、paying/processing分支;只有明确成功才做幂等记账/发货,失败要保存原单号和原因供客服查询。不要只判断code,也不要把旧首信易4207规则直接套到中金。

能否重试:处理明确失败原因、确认是新的业务申请后才可再次扣款或退款,不要收到failed就无限自动重试。

扣款一直paying、接口超时、5030,或查询提示4210 / 4310查无记录

排查:排队或约5秒等待窗口内未确认会返回paying;网络超时可能只是商户没有收到响应。查询暂未找到不等于银行一定没受理。先核对AppId、专用网关、原商户单号和业务类型。

处理:保留原 out_trade_no/out_refund_no,以数秒间隔并逐步退避查询,同时接收通知;商户查询读平台本地已确认状态,上游通知和后台对账负责更新,不是每次查单都立即请求银行。长时间不收敛请提交下方排查信息。

能否重试:不允许自动换单号重扣/重退。 先查原单;如需按原号重新请求,仍必须保持原参数、更新公共签名参数,并先核实是否已有受理记录。

4204预付费不足、4212周期名额占满、4213金额不匹配

排查:4204是本平台技术服务费预付余额不足;4212是该绑卡周期/月度成功及待确认订单占用名额;4213是当前扣费模板或通道允许金额不匹配/配置不可用。

处理:4204按平台账户要求充值后再核实原单;4212先查在途结果和下一可扣时间;4213按返回的 data.allowed_amounts(分)及当前模板核对,必要时请运营确认配置。不同金额仍共用原周期额度。

能否重试:不能通过改金额、并发换号或频繁重绑绕过次数限制。只在限制原因解除、原订单结果明确后按正常业务重新申请。

退款返回4306 / 4305,或想只退一部分本金

排查:4306表示不是原支付全额;4305表示已有退款预占、已退或原支付状态不允许再全额退。

处理:amount必须等于原支付本金,不能拆成多笔。先查原支付及原退款,处理中额度会保留,不要再申请一笔。退款只退本金,原通道费和技术服务费不退,也不会从预付费额外补收。

能否重试:已有成功/处理中退款不能再退;明确失败后也要先核实原因及原单,按新的退款业务申请,不自动补发。

退款长时间processing,或失败原因为“结算轧差金额不足”

排查:processing只表示结果未确认,不是已到账;“结算轧差金额不足”属于中金返回的退款业务失败原因,不能直接解释为平台技术服务费预付余额不足。

处理:查询原 out_refund_no,必要时查看refund_tracking最近核验时间/来源。长期待确认请联系平台和中金核实;明确failed且出现该原因,请客户经理/中金核查相关结算、可退款资金及业务处理条件。

能否重试:processing不得再退;明确失败并解决资金条件、核清原单后,才考虑新的退款申请。不要自动重退、强改成功或直接给持卡人再次打款。

平台有订单结果,商户却一直收不到回调;投递显示404

排查:核对原请求notify_url是否填写、是否可公网访问、路径是否准确。localhost / 127.0.0.1 对平台是平台服务器自身,不会指向商户;网页GET能打开,也不代表同路径支持POST JSON。

处理:使用真实公网HTTPS接收接口,检查DNS、TLS证书、反向代理路径、POST路由及访问日志。把实际URL和对应业务单号提交平台核对投递明细。修正商户后续请求地址;历史通知需由平台核对后定向修正/重推。

能否重试:先修接收地址和服务,再重推原通知;不要重新发起扣款来“触发一次新回调”。

商户接口返回HTTP200,为什么平台仍重试?重复通知怎么应答?

排查:确认返回的是纯文本 SUCCESS(大小写兼容),不是登录HTML、重定向页或只有200状态。通知可能重复,也可能先于同步响应到达。

处理:先验签、核对商户/单号/金额,并以业务事件、原单号和状态幂等持久化;数据库提交后再应答SUCCESS。已经处理过的合法重复通知,也应返回SUCCESS,不重复记账。

能否重试:持久化失败时不要谎报成功;平台按退避规则重试。通知重试可能沿用原timestamp/nonce/sign,不能直接套用请求接口的300秒时效或nonce一次性规则拒绝合法旧通知;验签仍须验证完整字段。

回调验签失败,或签约成功通知把已解绑卡“恢复”了怎么办?

排查:通知新增字段也参与签名,不能只挑旧字段;不要把数字转浮点元、修改中文/空值或做URL编码后再验签。绑卡通知包括card_type,解绑成功仍使用 event=bindcard,status=unbound,不是新的事件类型。

处理:使用所属商户平台App Secret验原JSON业务值,按当前业务状态幂等处理;已明确unbound不能被迟到bound通知重新激活。解绑终态和签约终态应分别记录。

能否重试:先定位签名差异;不要把失败通知直接当成功处理,更不能关闭验签来恢复业务。无法判断时查原bind_id并联系平台。

解绑code=0,但卡还显示bound;已经解绑后还能扣吗?

排查:只有 unbind_status=success 且 status=unbound 才确认成功;processing时原卡仍可能显示bound。

处理:按原bind_id调用 /unbind/query,不要反复发起新解绑。解绑成功后停用该记录的新扣款;后续使用需持卡人重新签约。历史已受理扣款/退款不因解绑被撤销。

能否重试:处理中只查原记录;明确failed请平台核实,不自动清除失败状态;成功记录不必再次解绑,也不能继续扣款。

9. 上线前自查与问题反馈

  • 确认是本商户获批的生产配置、API及对应交易能力,不把文档可见/本地证书验证当作银行权限已验收。
  • 借记与贷记分别用/banks取名单,按真实卡型绑卡;不硬编码所有商户共用的银行列表。
  • 所有金额使用整数分;商户扣款金额满足当前模板,技术费预付余额与通道结算资金分开核对。
  • 保存 out_bind_no/bind_id、out_trade_no/trade_no、out_refund_no/refund_no,接口超时不换号自动重发。
  • 分别处理HTTP状态、平台code、各业务status及fail_reason;不能只看到code=0就认定付款成功。
  • 回跳页只展示待核实并查询;回调验签、幂等持久化、提交后SUCCESS应答和主动查单兜底均经过验收。
  • 退款测试只全额退本金,原通道费/技术费保留、不补收;解绑测试要经持卡人/商户确认,会真实撤销签约。
  • 先少量真实授权业务验证本商户银行、卡型及通知接收链路,再逐步开放,不以其他商户的成功记录替代自身验收。

联系平台时一次提供这些信息

环境:生产 / 测试(由平台确认,不自行换证书或网关)
通道:中金;接口:例如 /api/openapi/cpcn/pay/query
发生时间:YYYY-MM-DD HH:mm:ss(北京时间)
业务单号:out_bind_no / bind_id,或 out_trade_no / trade_no,或 out_refund_no / refund_no
现象:预期结果、实际HTTP状态、平台code、data.status及fail_reason
请求/响应:已脱敏的业务字段;不要提供平台App Secret、证书密码、私钥或完整卡证资料
回调问题:实际notify_url、平台投递HTTP状态、商户响应正文及接收时间
商户侧处理:是否已查原单、是否已入账/发货,是否出现重复通知

不通过群聊发送完整姓名、身份证、手机号、银行卡号或签名密钥。需要原始敏感报文时,由平台约定安全方式单独核查。不要为定位问题自行重复扣款、退款或解绑。