KeepPay 开发者文档
在用户浏览器侧令牌化,明文永不落你的服务器;用令牌向任意能收卡的通道发起扣款,一套卡库对接谁都行。
Keep your data · Keep your choices · Keep your revenue
概述
KeepPay 是基于 PCI Level 1 合规底座的支付编排平台。集成分为四步:
- 前端采集 —— 用采集组件在用户浏览器获取临时 Token Intent ID,明文不经过你的前后端。
- 首次下单 —— 商户后端将临时
token_intent_id提交到/api/payments; KeepPay 返回 PSP 下单结果,此时不创建长期 Token,响应不包含token字段。 - 数据上报 —— 商户收到 PSP 异步通知后调用
/api/order/report, 提交订单号及token_save。传true时创建长期 Token,商户自行保存上报响应的token; 传false时不创建。 - 用令牌扣款 —— 用已保存的长期 Token 提交到
/api/payments发起续费扣款,明文在合规环境内还原注入,你的系统不碰 PAN。
API 基址:
快速开始
开发前准备:创建项目与申请支付渠道
新商户开通管理后台账号后,请先完成项目申请和支付渠道申请。两项申请均审核通过后,再获取密钥并开始接入开发。
- 创建项目:登录商户管理后台,点击左上角项目选择框,在下拉菜单中选择“增加项目”; 填写项目名称、部署区域及备注,点击“保存”,提交项目申请并等待审核。项目用于区分不同业务的支付数据和接入配置。
- 申请支付渠道:在左上角选择需要接入的项目,进入“设置 → 支付通道”,点击“申请接入通道”; 按页面要求填写申请信息、选择所需 PSP,点击“提交申请”。在“申请记录”中查看审核状态和意见; 审核通过并接通后,该渠道会显示为“已开通”。需要接入多个 PSP 时,分别申请相应渠道。
- 获取密钥并开始开发:确认项目审核通过、所需支付渠道已开通后,选择对应项目和运行环境,
进入“设置 → 密钥配置”,获取项目(应用)ID(
appid)、前端密钥和接口密钥,替换到下方接入示例中。 前端密钥用于浏览器采集组件,接口密钥由商户服务端保管并通过X-Keep-Sign请求头提交;测试与正式环境的密钥请分别使用。
前端与服务端接入
从获取临时 Token Intent 到首次下单、接收 PSP 异步通知并上报,再保存长期 Token用于续费:
使用 Keeppay 托管的 Card iframe 采集卡号、有效期和 CVC。浏览器端使用 前端公钥。
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>KeepPay 临时 Token 示例</title>
<style>
body { max-width: 560px; margin: 40px auto; padding: 0 20px; font-family: sans-serif; }
#kp-card { min-height: 80px; padding: 16px; border: 1px solid #ccc; }
button { margin-top: 16px; padding: 10px 16px; cursor: pointer; }
pre { padding: 16px; background: #f5f5f5; white-space: pre-wrap; overflow-wrap: anywhere; }
</style>
</head>
<body>
<h1>获取临时 Token Intent</h1>
<p>输入测试卡信息,再点击按钮获取首次下单所需的临时凭证。</p>
<div id="kp-card"></div>
<button id="card-intent-button" type="button" disabled>获取临时 Token Intent</button>
<p id="status" role="status" aria-live="polite">正在初始化卡组件…</p>
<pre id="result" aria-label="临时凭证结果">尚未获取临时凭证</pre>
<script>
// 必须替换为你项目的测试环境前端公钥,不要填写后端私钥或 PSP 密钥。
const btConfig = {
cardPublicKey: 'REPLACE_WITH_YOUR_TEST_PUBLIC_KEY',
environment: 'test', // 正式环境使用正式前端公钥,并改为 'us'。
cardSdkUrl: 'https://js.keeppay.net/web-elements/2.12.2/index.js'
};
const button = document.getElementById('card-intent-button');
const status = document.getElementById('status');
const result = document.getElementById('result');
let btCardInstance;
let cardElement;
let tokenIntent;
let busy = false;
function loadCardSdk() {
if (typeof window.basistheory === 'function') {
return Promise.resolve(window.basistheory);
}
return new Promise((resolve, reject) => {
const script = document.createElement('script');
const timer = setTimeout(() => {
script.remove();
reject(new Error('SDK 加载超时,请检查网络后刷新页面'));
}, 20000);
script.crossOrigin = 'anonymous';
script.src = btConfig.cardSdkUrl;
script.async = true;
script.onload = () => {
clearTimeout(timer);
if (typeof window.basistheory === 'function') {
resolve(window.basistheory);
} else {
reject(new Error('SDK 未提供 basistheory 方法'));
}
};
script.onerror = () => {
clearTimeout(timer);
reject(new Error('SDK 加载失败,请检查网络和 SDK 地址'));
};
document.head.appendChild(script);
});
}
async function initCard() {
try {
if (window.location.protocol !== 'https:') {
throw new Error('示例代码必须通过 HTTPS 访问才能运行,请使用项目已授权的 HTTPS 域名打开页面');
}
if (!btConfig.cardPublicKey || btConfig.cardPublicKey === 'REPLACE_WITH_YOUR_TEST_PUBLIC_KEY') {
throw new Error('请先替换代码中的 REPLACE_WITH_YOUR_TEST_PUBLIC_KEY 为项目测试环境前端公钥');
}
const sdk = await loadCardSdk();
btCardInstance = await sdk(btConfig.cardPublicKey, {
elements: true,
environment: btConfig.environment,
useSameOriginApi: true
});
if (typeof btCardInstance.tokenIntents?.create !== 'function') {
throw new Error('当前 SDK 不支持临时 Token Intent');
}
cardElement = btCardInstance.createElement('card', {
style: { fonts: [], base: { color: '#111827', fontSize: '16px' } }
});
await cardElement.mount('#kp-card');
status.textContent = '卡组件已就绪,请输入卡信息。';
button.disabled = false;
} catch (error) {
status.textContent = '初始化失败:' + error.message;
button.disabled = true;
}
}
button.addEventListener('click', async () => {
if (busy || tokenIntent || !cardElement) return;
busy = true;
button.disabled = true;
status.textContent = '正在获取临时凭证…';
try {
const intent = await btCardInstance.tokenIntents.create({
type: 'card',
data: cardElement
});
if (!intent?.id) throw new Error('未获得临时 Token Intent ID');
tokenIntent = { id: intent.id, expires_at: intent.expires_at };
// 将下面的 token_intent_id 发送给商户后端,由后端调用 /api/payments。
// 获取临时凭证不是付款成功;此页面只演示采集,不会自动下单。
result.textContent = JSON.stringify({
token_intent_id: intent.id,
expires_at: intent.expires_at
}, null, 2);
status.textContent = '临时凭证已获取,请将 token_intent_id 提交给商户后端。';
button.textContent = '已获取临时凭证';
} catch (error) {
status.textContent = '获取失败:' + error.message;
} finally {
busy = false;
button.disabled = Boolean(tokenIntent);
}
});
window.addEventListener('beforeunload', () => cardElement?.unmount());
initCard();
</script>
</body>
</html>
intent.id 是临时凭证,首次下单时通过 token_intent_id 提交给 /api/payments,
不要将它作为长期 Token 保存或用于续费。获取临时凭证不代表下单或付款成功;
卡号、有效期和 CVC 始终保留在 Keeppay 托管 iframe 内。临时凭证过期后,应重新获取。
示例使用 Stripe 作为最终支付通道。
# 1) 首次扣款(Stripe · CIT) curl https://payment-test.keeppay.net/api/payments \ -H "X-KEEP-VERSION: 2.0.1" \ -H "X-Keep-Sign: keep_uhmwb1t87…" \ -H "Authorization: Bearer sk_test_…" \ -H "Content-Type: application/json" \ --data-raw '{ "action": "first_charge", "appid": "appid", "order_no": "ord_first_250612_5521", "token_intent_id": "665bf471-d675-4165-9336-4fc9c1a73fad", "payment": "card", "psp": "stripe", "stripe": { "amount": 1990, "currency": "usd", "confirm": true, "setup_future_usage": "off_session", "payment_method_data": { "type": "card" }, "metadata": { "trade_no": "ord_first_250612_5521" }, "expand": [ "latest_charge" ] } }'
# 2) 后续扣款(Stripe · MIT,用户无需在场) curl https://payment-test.keeppay.net/api/payments \ -H "X-KEEP-VERSION: 2.0.1" \ -H "X-Keep-Sign: keep_uhmwb1t87…" \ -H "Authorization: Bearer sk_test_…" \ -H "Content-Type: application/json" \ --data-raw '{ "action": "recurring_charge", "appid": "appid", "order_no": "ord_renew_2026_07", "token_id": "294270e5-2be1-4974-9188-96b3a1c8f25f", "payment": "card", "psp": "stripe", "stripe": { "amount": 1990, "currency": "usd", "confirm": true, "off_session": true, "payment_method_data": { "type": "card" }, "automatic_payment_methods": { "enabled": true, "allow_redirects": "never" }, "metadata": { "trade_no": "ord_renew_2026_07" } } }'
示例使用 Stripe+Antom 作为最终支付通道。
# 1.1) 首次扣款(Stripe + Antom 多通道参数;本次 psp=stripe) curl https://payment-test.keeppay.net/api/payments \ -H "X-KEEP-VERSION: 2.0.1" \ -H "X-Keep-Sign: keep_uhmwb1t87…" \ -H "Authorization: Bearer sk_test_…" \ -H "Content-Type: application/json" \ --data-raw '{ "action": "first_charge", "appid": "appid", "order_no": "ord_first_250612_5521", "token_intent_id": "665bf471-d675-4165-9336-4fc9c1a73fad", "payment": "card", "psp": "stripe", "stripe": { "amount": 1990, "currency": "usd", "confirm": true, "setup_future_usage": "off_session", "payment_method_data": { "type": "card" }, "metadata": { "trade_no": "ord_first_250612_5521" }, "expand": [ "latest_charge" ] }, "antom": { "paymentRequestId": "ord_first_250612_5521", "productCode": "CASHIER_PAYMENT", "paymentAmount": { "currency": "USD", "value": "1990" }, "settlementStrategy": { "settlementCurrency": "USD" }, "paymentMethod": { "paymentMethodType": "CARD", "paymentMethodMetaData": { "isCardOnFile": false, "is3DSAuthentication": false, "enableAuthenticationUpgrade": false } }, "paymentFactor": { "isAuthorization": true }, "order": { "referenceOrderId": "ORDER_FIRST_20260813_000001", "orderDescription": "Subscription first payment", "orderAmount": { "currency": "USD", "value": "1990" }, "buyer": { "referenceBuyerId": "USER_10001", "buyerEmail": "[email protected]" } }, "env": { "terminalType": "WEB" }, "paymentRedirectUrl": "https://merchant.example.com/payment/return", "paymentNotifyUrl": "https://merchant.example.com/api/payment/notify/antom" } }'
# 2.1) 后续扣款(Stripe + Antom 多通道参数;本次 psp=stripe) curl https://payment-test.keeppay.net/api/payments \ -H "X-KEEP-VERSION: 2.0.1" \ -H "X-Keep-Sign: keep_uhmwb1t87…" \ -H "Authorization: Bearer sk_test_…" \ -H "Content-Type: application/json" \ --data-raw '{ "action": "recurring_charge", "appid": "appid", "order_no": "ord_renew_2026_07", "token_id": "294270e5-2be1-4974-9188-96b3a1c8f25f", "payment": "card", "psp": "stripe", "stripe": { "amount": 1990, "currency": "usd", "confirm": true, "off_session": true, "payment_method_data": { "type": "card" }, "automatic_payment_methods": { "enabled": true, "allow_redirects": "never" }, "metadata": { "trade_no": "ord_renew_2026_07" } }, "antom": { "paymentRequestId": "ord_renew_2026_07", "productCode": "CASHIER_PAYMENT", "paymentAmount": { "currency": "USD", "value": "1990" }, "settlementStrategy": { "settlementCurrency": "USD" }, "paymentMethod": { "paymentMethodType": "CARD", "paymentMethodMetaData": { "isCardOnFile": true, "is3DSAuthentication": false, "enableAuthenticationUpgrade": false, "recurringType": "SCHEDULED", "networkTransactionId": "386217311034070" } }, "paymentFactor": { "isAuthorization": true }, "order": { "referenceOrderId": "ORDER_RENEWAL_20260813_000001", "orderDescription": "Monthly subscription renewal", "orderAmount": { "currency": "USD", "value": "1990" }, "buyer": { "referenceBuyerId": "USER_10001", "buyerEmail": "[email protected]" } }, "env": { "terminalType": "WEB" }, "paymentRedirectUrl": "https://merchant.example.com/payment/return", "paymentNotifyUrl": "https://merchant.example.com/api/payment/notify/antom" } }'
appid、order_no、psp、
action;首次下单传 token_intent_id,续费传已保存的长期 token_id;按 action
使用对应凭证,另一字段即使提交也会被忽略。另需
stripe 中的 amount、
currency、confirm。Stripe后续扣款传
confirm:true、off_session:true,无需 NTID,续费不传 setup_future_usage 或
CVC;Antom 续费在其官方参数中传 networkTransactionId。
psp 及后续扣款所需的
NTID(支付成功后按 PSP Webhook 字段获取)。下单响应不包含长期 token;商户收到 PSP 异步通知后,
通过 数据上报 决定是否创建长期 Token。
核心概念
| 概念 | 说明 |
|---|---|
| 项目(应用) | 你的一个 App,一一对应。每个项目 = 一套前端公钥 / 后端私钥 / 授权域名;令牌、通道、计费都按项目归集。 |
| 临时 Token Intent(token_intent_id) | 前端采集组件返回的临时凭证,用于首次下单,不用于续费。 |
| 长期 Token(上报响应 token / 续费请求 token_id) | 上报时传 token_save:true 创建并返回,或超过 1 小时未上报且成功下单时自动创建。商户保存长期 Token,续费时作为 token_id
提交。 |
| 通道 Channel | 能用 API 收卡的 PSP( Stripe / Antom…)。 |
| 环境 | 测试 / 正式,数据互相隔离。 |
鉴权与密钥
密钥按项目隔离,分三类,权限各不相同:
| 类型 | 用途 | 权限 | 放哪 |
|---|---|---|---|
| 前端公钥 | 采集组件 / 前端会话 | keeppay分配,创建临时 Token Intent | 可公开,放前端 |
| 后端私钥 | 服务器调用 API | keeppay分配,用于服务器端调用api | 商户保管,只放服务器 |
| PSP密钥 | 商户PSP端的密钥(根据各PSP要求传入) | 商户用于请求PSP鉴权 | 商户保管 |
后端请求 header头中传入 后端私钥
① 前端采集组件
Card 采集组件是由 Keeppay 托管的嵌入式 iframe。你的页面只负责加载 SDK、使用 前端公钥 初始化组件并指定挂载位置;卡号、有效期和 CVC 不会进入你的页面 DOM 或服务器。
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>KeepPay 临时 Token 示例</title>
<style>
body { max-width: 560px; margin: 40px auto; padding: 0 20px; font-family: sans-serif; }
#kp-card { min-height: 80px; padding: 16px; border: 1px solid #ccc; }
button { margin-top: 16px; padding: 10px 16px; cursor: pointer; }
pre { padding: 16px; background: #f5f5f5; white-space: pre-wrap; overflow-wrap: anywhere; }
</style>
</head>
<body>
<h1>获取临时 Token Intent</h1>
<p>输入测试卡信息,再点击按钮获取首次下单所需的临时凭证。</p>
<div id="kp-card"></div>
<button id="card-intent-button" type="button" disabled>获取临时 Token Intent</button>
<p id="status" role="status" aria-live="polite">正在初始化卡组件…</p>
<pre id="result" aria-label="临时凭证结果">尚未获取临时凭证</pre>
<script>
// 必须替换为你项目的测试环境前端公钥,不要填写后端私钥或 PSP 密钥。
const btConfig = {
cardPublicKey: 'REPLACE_WITH_YOUR_TEST_PUBLIC_KEY',
environment: 'test', // 正式环境使用正式前端公钥,并改为 'us'。
cardSdkUrl: 'https://js.keeppay.net/web-elements/2.12.2/index.js'
};
const button = document.getElementById('card-intent-button');
const status = document.getElementById('status');
const result = document.getElementById('result');
let btCardInstance;
let cardElement;
let tokenIntent;
let busy = false;
function loadCardSdk() {
if (typeof window.basistheory === 'function') {
return Promise.resolve(window.basistheory);
}
return new Promise((resolve, reject) => {
const script = document.createElement('script');
const timer = setTimeout(() => {
script.remove();
reject(new Error('SDK 加载超时,请检查网络后刷新页面'));
}, 20000);
script.crossOrigin = 'anonymous';
script.src = btConfig.cardSdkUrl;
script.async = true;
script.onload = () => {
clearTimeout(timer);
if (typeof window.basistheory === 'function') {
resolve(window.basistheory);
} else {
reject(new Error('SDK 未提供 basistheory 方法'));
}
};
script.onerror = () => {
clearTimeout(timer);
reject(new Error('SDK 加载失败,请检查网络和 SDK 地址'));
};
document.head.appendChild(script);
});
}
async function initCard() {
try {
if (window.location.protocol !== 'https:') {
throw new Error('示例代码必须通过 HTTPS 访问才能运行,请使用项目已授权的 HTTPS 域名打开页面');
}
if (!btConfig.cardPublicKey || btConfig.cardPublicKey === 'REPLACE_WITH_YOUR_TEST_PUBLIC_KEY') {
throw new Error('请先替换代码中的 REPLACE_WITH_YOUR_TEST_PUBLIC_KEY 为项目测试环境前端公钥');
}
const sdk = await loadCardSdk();
btCardInstance = await sdk(btConfig.cardPublicKey, {
elements: true,
environment: btConfig.environment,
useSameOriginApi: true
});
if (typeof btCardInstance.tokenIntents?.create !== 'function') {
throw new Error('当前 SDK 不支持临时 Token Intent');
}
cardElement = btCardInstance.createElement('card', {
style: { fonts: [], base: { color: '#111827', fontSize: '16px' } }
});
await cardElement.mount('#kp-card');
status.textContent = '卡组件已就绪,请输入卡信息。';
button.disabled = false;
} catch (error) {
status.textContent = '初始化失败:' + error.message;
button.disabled = true;
}
}
button.addEventListener('click', async () => {
if (busy || tokenIntent || !cardElement) return;
busy = true;
button.disabled = true;
status.textContent = '正在获取临时凭证…';
try {
const intent = await btCardInstance.tokenIntents.create({
type: 'card',
data: cardElement
});
if (!intent?.id) throw new Error('未获得临时 Token Intent ID');
tokenIntent = { id: intent.id, expires_at: intent.expires_at };
// 将下面的 token_intent_id 发送给商户后端,由后端调用 /api/payments。
// 获取临时凭证不是付款成功;此页面只演示采集,不会自动下单。
result.textContent = JSON.stringify({
token_intent_id: intent.id,
expires_at: intent.expires_at
}, null, 2);
status.textContent = '临时凭证已获取,请将 token_intent_id 提交给商户后端。';
button.textContent = '已获取临时凭证';
} catch (error) {
status.textContent = '获取失败:' + error.message;
} finally {
busy = false;
button.disabled = Boolean(tokenIntent);
}
});
window.addEventListener('beforeunload', () => cardElement?.unmount());
initCard();
</script>
</body>
</html>
file:// 打开;完整 HTML 页面需声明
<meta charset="UTF-8">。获取临时 token_intent_id、提交后端并发起支付的完整流程请参见
快速开始;采集组件只在已配置的授权域名加载。
② 首次扣款
前端取得临时 Token Intent 的 id 后,由你的后端以 token_intent_id 字段调用 KeepPay
下单。首次扣款属于持卡人发起交易(CIT),
示例选择 Stripe,并通过 setup_future_usage:"off_session" 为后续离线后续扣款建立使用条件。
stripe单一通道
curl https://payment-test.keeppay.net/api/payments \ -H "X-KEEP-VERSION: 2.0.1" \ -H "X-KEEP-SIGN: keep_uhmwb1t87…" \ -H "Authorization: Bearer sk_test_…" \ -H "Content-Type: application/json" \ --data-raw '{ "action": "first_charge", "appid": "appid", "order_no": "ord_first_250612_5521", "token_intent_id": "665bf471-d675-4165-9336-4fc9c1a73fad", "payment": "card", "psp": "stripe", "stripe": { "amount": 1990, "currency": "usd", "confirm": true, "setup_future_usage": "off_session", "payment_method_data": { "type": "card" }, "metadata": { "trade_no": "ord_first_250612_5521" }, "expand": [ "latest_charge" ] } }'
stripe和antom多通道
curl https://payment-test.keeppay.net/api/payments \ -H "X-KEEP-VERSION: 2.0.1" \ -H "X-KEEP-SIGN: keep_uhmwb1t87…" \ -H "Authorization: Bearer sk_test_…" \ -H "Content-Type: application/json" \ --data-raw '{ "action": "first_charge", "appid": "appid", "order_no": "ord_first_250612_5521", "token_intent_id": "665bf471-d675-4165-9336-4fc9c1a73fad", "payment": "card", "psp": "stripe", "stripe": { "amount": 1990, "currency": "usd", "confirm": true, "setup_future_usage": "off_session", "payment_method_data": { "type": "card" }, "metadata": { "trade_no": "ord_first_250612_5521" }, "expand": [ "latest_charge" ] }, "antom": { "paymentRequestId": "ord_first_250612_5521", "productCode": "CASHIER_PAYMENT", "paymentAmount": { "currency": "USD", "value": "1990" }, "settlementStrategy": { "settlementCurrency": "USD" }, "paymentMethod": { "paymentMethodType": "CARD", "paymentMethodMetaData": { "isCardOnFile": false, "is3DSAuthentication": false, "enableAuthenticationUpgrade": false } }, "paymentFactor": { "isAuthorization": true }, "order": { "referenceOrderId": "ORDER_FIRST_20260813_000001", "orderDescription": "Subscription first payment", "orderAmount": { "currency": "USD", "value": "1990" }, "buyer": { "referenceBuyerId": "USER_10001", "buyerEmail": "[email protected]" } }, "env": { "terminalType": "WEB" }, "paymentRedirectUrl": "https://merchant.example.com/payment/return", "paymentNotifyUrl": "https://merchant.example.com/api/payment/notify/antom" } }'
psp 决定本次实际执行的渠道。
当前示例设置为 stripe。支付编排关闭时只调用该已开通通道;开启后,优先尝试 Stripe,PSP
明确返回下单失败时,再按后台优先级尝试其他已开通、配置完整且本次传入了原生参数的通道。3DS、跳转、已受理、超时或结果未知均停止切换。首单和续费均支持该流程;Stripe 续费使用长期
Token,Antom、云汇续费按各自要求另传 NTID。
X-KEEP-SIGN 用于 KeepPay 鉴权;Stripe 示例还需要
Authorization: Bearer sk_test_…。token_intent_id 来自前端
tokenIntents.create 返回的 id,
action 固定为 first_charge。
/api/order/report 上报。
下单成功时不创建长期 Token,/api/payments 响应不包含 token。各 PSP 如何获取并保存 NTID
商户服务端收到 PSP 的支付成功通知后,先按该 PSP 要求验签并确认订单支付成功,再从下面的字段中取得 NTID。 将 NTID 与对应订单、卡片的长期 Token 一起保存,Antom、云汇等要求 NTID 的续费应使用同一张卡的原交易标识;Stripe 当前续费无需提交 NTID。 以下通知路径均从 Webhook JSON 根节点开始。 仅供参考,具体通知结构、字段含义及返回条件以各 PSP 官方文档为准。
| PSP | 通知字段路径 | 官方文档 |
|---|---|---|
| Stripe | charge.succeeded 通知:
data.object.payment_method_details.card.network_transaction_id。
如果收到的是 payment_intent.succeeded,先取 data.object.latest_charge,
再由商户服务端查询该 Charge,读取 payment_method_details.card.network_transaction_id;
latest_charge 已展开为对象时可直接读取其同名字段。
|
Stripe Charge |
| Antom | notifyPayment 成功通知:
paymentResultInfo.networkTransactionId。
确认通知中的 result.resultStatus 为 S 后保存;字段是否返回取决于接入产品及账户能力。
|
Antom notifyPayment、 卡支付通知字段示例 |
| PayerMax | 支付成功结果通知:
data.paymentDetails[].networkTransactionIdentifier。
确认 data.status 为 SUCCESS,从 paymentDetails 中选取对应卡支付的记录,
读取非空字段;该字段仅在支付成功且底层支付渠道提供时返回。
paymentTokenID 是 PayerMax 支付令牌,不是 NTID。
|
PayerMax 官方文档 |
| 空中云汇 Airwallex | payment_intent.succeeded 通知中的
data.object.latest_payment_attempt.payment_method_transaction_id。
若通知版本未携带该字段,由商户服务端查询对应 PaymentIntent 的
latest_payment_attempt.payment_method_transaction_id;Mastercard 如适用,还需保存生命周期标识。
|
Airwallex Webhook、 PaymentIntent |
通知字段示例
下面是用于定位 NTID 的 JSON 片段,省略了其他通知字段,不是完整的验签通知或请求参数;示例值仅为占位,请使用实际成功交易返回的值。
Stripe:charge.succeeded
{
"type": "charge.succeeded",
"data": {
"object": {
"payment_method_details": {
"card": {
"network_transaction_id": "实际NTID"
}
}
}
}
}
Antom:notifyPayment
{
"result": {
"resultStatus": "S"
},
"paymentResultInfo": {
"networkTransactionId": "实际NTID"
}
}
PayerMax:支付结果通知
{
"data": {
"status": "SUCCESS",
"paymentDetails": [
{
"paymentMethodType": "CARD",
"networkTransactionIdentifier": "实际NTID"
}
]
}
}
空中云汇:payment_intent.succeeded
{
"name": "payment_intent.succeeded",
"data": {
"object": {
"status": "SUCCEEDED",
"latest_payment_attempt": {
"payment_method_transaction_id": "实际NTID"
}
}
}
}
云汇首次下单响应已为 SUCCEEDED 且包含该字段时,也可从 KeepPay 响应的
params.latest_payment_attempt.payment_method_transaction_id 取得 NTID。
首次下单仍需完成 3DS 或尚未支付成功时,应等待成功通知或服务端查单确认后再保存。
pi_… / ch_…、云汇的 int_… / att_…、
3DS 的 dsTransactionId 或支付令牌代替 NTID。
可由商户服务端按对应 PSP 官方查询接口补查;查询后仍为空,请联系 PSP 确认当前账户、API 版本及支付渠道是否支持返回该字段。
支付成功不代表一定返回 NTID;尚未取得有效 NTID 时,不要提交要求 NTID 的续费请求。
长期 Token 仍按 /api/order/report 的 token_save 规则创建,与 NTID 是两个不同的值。空中云汇接入
使用 psp:"airwallex",将商户按云汇官方文档组装的支付参数放入 airwallex 对象。
商户服务端须使用自己的云汇 Client ID、API Key 获取 Access Token,再通过
X-Airwallex-Access-Token 请求头提交给 KeepPay,同时携带 KeepPay 签名及版本请求头。
项目对应环境须已开通云汇通道。
环境与密钥:测试接口必须使用云汇沙箱环境的 Client ID、API Key 及其 Access Token;正式环境凭证不能用于沙箱。登录返回
credentials_invalid 时,请核对环境、密钥配对及有效性;API Key 不是 Webhook Secret。环境以商户实际获取凭证的后台为准。
商户服务端获取云汇 Access Token 的测试环境示例(正式环境改为 https://api.airwallex.com):
curl 'https://api.sandbox.airwallex.com/api/v1/authentication/login' \ -X POST \ -H 'x-client-id: 替换为云汇ClientID' \ -H 'x-api-key: 替换为云汇APIKey'
使用登录响应中的 token 值,按 expires_at 在商户服务端缓存复用。
多账户密钥按云汇要求增加 x-login-as。
X-Airwallex-Access-Token 只填写令牌原值,不加 Bearer 前缀。
它是云汇接口鉴权凭证,与前端获取的 BT 临时 token_intent_id、商户保存的 BT 长期 token_id 不同。
正式集成时 Client ID、API Key、Access Token 均由商户服务端保管,不放入公开前端页面;获取方式以
云汇鉴权文档为准。
X-Airwallex-Access-Token 请求头提供有效的云汇 Access Token;
Token 失效时,请重新获取。支付请求失败或结果未知时,应先核实原订单状态,避免重复扣款。
云汇参与编排时,此请求头只供云汇使用,Stripe 仍使用自己的 Authorization。提交给 KeepPay:调用 /api/payments 时,携带
X-Keep-Sign、X-KEEP-VERSION 和 X-Airwallex-Access-Token 请求头,
提交 appid、order_no、psp:"airwallex"、payment:"card" 及商户组装的
airwallex 对象。
首次下单使用 action:"first_charge" 和前端获取的 token_intent_id;
续费使用 action:"recurring_charge" 和商户已保存的长期 token_id。每笔新订单使用新的订单号。
云汇原生参数:airwallex 对象中的支付参数、首次及续费标识、NTID、设备信息和验证交互,
均由商户按云汇官方文档准备和处理。KeepPay 文档仅说明提交给 KeepPay 的接口约定;
云汇原生字段及业务要求请以云汇支付接口官方文档为准。
NTID 获取路径可参考NTID 说明。
处理 KeepPay 响应:云汇响应放在 params 中。
下单接口不返回长期 Token。商户确认 PSP 通知后,使用响应中的实际 psp 和原订单号调用
/api/order/report,通过 token_save 决定是否创建长期 Token;创建成功后保存响应的 token 用于续费。
一小时未上报的处理与其他通道一致,详见数据上报。支付编排规则见备用渠道与编排。
首次下单响应
KeepPay 使用临时 Token 向 PSP 下单,并将 PSP 结果放在 params 返回给商户。
无论下单成功还是失败,此时均不主动创建长期 Token,响应中也不包含 token 字段。
{
"status": "ok",
"error_code": "",
"error_message": "",
"appid": "appid",
"psp": "stripe",
"trace_id": "11111111111111111111111111111111",
"params": {
"id": "pi_example",
"object": "payment_intent",
"status": "succeeded"
}
}
/api/order/report,
通过 token_save 决定是否创建长期 Token。成功下单包括 PSP 已接受但需要 3DS 或仍在处理中的订单,
不等同于最终付款成功;商户仍须以 PSP 异步通知或查询结果判断资金状态。③ 后续扣款
后续扣款到期后,将数据上报创建后保存的长期 token 作为请求字段 token_id 发起离线后续扣款(MIT),用户无需再次输入卡信息。
当前后续扣款仍调用统一的 /api/payments,并通过 action:"recurring_charge" 标识后续扣款。本文档的 PayerMax
接入仅适用首次下单,续费不适用。
token_id,Stripe 参数传 confirm:true、off_session:true,
不需要提交 NTID,不传 setup_future_usage 或 CVC。
请确保已取得持卡人的有效续费授权;离线扣款仍可能要求额外验证,应按 Stripe 官方流程请持卡人完成验证,不能承诺免除 3DS。stripe单一通道
curl https://payment-test.keeppay.net/api/payments \ -H "X-KEEP-VERSION: 2.0.1" \ -H "X-KEEP-SIGN: keep_uhmwb1t87…" \ -H "Authorization: Bearer sk_test_…" \ -H "Content-Type: application/json" \ --data-raw '{ "action": "recurring_charge", "appid": "appid", "order_no": "ord_renew_2026_07", "token_id": "294270e5-2be1-4974-9188-96b3a1c8f25f", "payment": "card", "psp": "stripe", "stripe": { "amount": 1990, "currency": "usd", "confirm": true, "off_session": true, "payment_method_data": { "type": "card" }, "automatic_payment_methods": { "enabled": true, "allow_redirects": "never" }, "metadata": { "trade_no": "ord_renew_2026_07" } } }'
stripe和antom多通道
curl https://payment-test.keeppay.net/api/payments \ -H "X-KEEP-VERSION: 2.0.1" \ -H "X-KEEP-SIGN: keep_uhmwb1t87…" \ -H "Authorization: Bearer sk_test_…" \ -H "Content-Type: application/json" \ --data-raw '{ "action": "recurring_charge", "appid": "appid", "order_no": "ord_renew_2026_07", "token_id": "294270e5-2be1-4974-9188-96b3a1c8f25f", "payment": "card", "psp": "stripe", "stripe": { "amount": 1990, "currency": "usd", "confirm": true, "off_session": true, "payment_method_data": { "type": "card" }, "automatic_payment_methods": { "enabled": true, "allow_redirects": "never" }, "metadata": { "trade_no": "ord_renew_2026_07" } }, "antom": { "paymentRequestId": "ord_renew_2026_07", "productCode": "CASHIER_PAYMENT", "paymentAmount": { "currency": "USD", "value": "1990" }, "settlementStrategy": { "settlementCurrency": "USD" }, "paymentMethod": { "paymentMethodType": "CARD", "paymentMethodMetaData": { "isCardOnFile": true, "is3DSAuthentication": false, "enableAuthenticationUpgrade": false, "recurringType": "SCHEDULED", "networkTransactionId": "386217311034070" } }, "paymentFactor": { "isAuthorization": true }, "order": { "referenceOrderId": "ORDER_RENEWAL_20260813_000001", "orderDescription": "Monthly subscription renewal", "orderAmount": { "currency": "USD", "value": "1990" }, "buyer": { "referenceBuyerId": "USER_10001", "buyerEmail": "[email protected]" } }, "env": { "terminalType": "WEB" }, "paymentRedirectUrl": "https://merchant.example.com/payment/return", "paymentNotifyUrl": "https://merchant.example.com/api/payment/notify/antom" } }'
psp 指定的已开通渠道;编排开启时先尝试该渠道,
只有 PSP 明确返回下单失败,才按优先级尝试其他已开通、配置完整且提供了参数的渠道。
Stripe 续费无需 NTID;Antom 的 networkTransactionId 必须来自已核实的原成功交易,
通知字段路径见 NTID 说明。3DS、已受理、超时或结果未知均停止切换。
后续扣款通道与首次扣款通道
首次扣款通道与后续扣款通道没有必然的关系。例如,首次扣款是stripe完成,后续扣款可以由antom完成。请求的 psp 决定实际后续扣款通道:设置为
stripe 时读取 stripe;设置为 antom 时读取
antom。
token_id、confirm:true 和 off_session:true,无需
NTID;续费不传 setup_future_usage 或 CVC。Antom 续费必须在
antom.paymentMethod.paymentMethodMetaData.networkTransactionId 中传入首次成功支付后保存的 NTID。
通知字段路径见 各 PSP 的 NTID 说明。
④ 数据上报
token_save;KeepPay 按当前项目的订单号查询首次下单记录,
获取其中的临时 Token,无需商户再次提交 token_intent_id 或 token_id。
token_save:true 时创建长期 Token并返回 token;
token_save:false 时不创建长期 Token。该参数必须使用 JSON 布尔值。
下单超过 1 小时,仍未上报,且下单返回数据表明成功下单系统会自动创建长期token。长期token会增加商户的费用,商户务必及时上报并设置是否需要创建长期token。
自动创建后,后续上报
token_save:true 将返回已有 Token,不重复创建;创建完成后再传 false
无法撤销此前创建及费用。续费订单不会重复创建长期 Token。
商户收到 PSP 异步通知后,调用数据上报接口同步订单结果,并通过 token_save 选择是否创建长期 Token。该接口接收订单关联信息,
不提交卡号、有效期或 CVC 等敏感卡数据。
请求示例
以下示例上报首次订单的 PSP 支付结果,并传 token_save: true 创建长期 Token。订单号必须与首次下单的 order_no 一致。
curl https://payment-test.keeppay.net/api/order/report \ -X POST \ -H "X-KEEP-SIGN: keep_uhmwb1t87…" \ -H "Content-Type: application/json" \ --data-raw '{ "appid": "appid", "psp": "stripe", "order_no": "ord_first_250612_5521", "order_status": "paid", "order_amount": 1999, "order_at": "2024-06-10 12:34:21", "ntid": "466292250988681", "token_save": true }'
请求参数
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| appid | string | 是 | KeepPay 分配的项目应用ID,KeepPay后台获取。 |
| psp | string | 是 | 该订单实际使用的 PSP 名称,例如 stripe 或 antom。 |
| order_no | string | 是 | 与首次下单记录一致的商户订单号,用于在当前项目内关联临时 Token。 |
| order_status | string | 是 |
unpay:未支付;paying:支付中;paid:支付成功;failed:支付失败。仅接受这四个值,其他值返回
HTTP 400、invalid_report。 |
| order_amount | integer | 是 | 交易金额(币种最小货币单位),示例值为 1999。 |
| order_at | string | 是 | UTC 交易时间,格式为 YYYY-MM-DD HH:mm:ss,例如 2026-10-20 12:23:21。 |
| token_save | boolean | 是 | true:根据该订单记录的临时 Token 创建长期 Token;false:不创建。
使用 JSON 布尔值,不要传字符串。 |
| ntid | string | 否 | PSP 首次成功支付通知中的网络交易标识。最多 128 字符,非必填;按原值上报,不进行 Visa/Mastercard 长度判断。KeepPay 记录该上报值,不验证其真实性,也不将其当作长期 Token。获取路径见 NTID 说明。 |
长期 Token 创建规则
| 场景 | 处理方式 |
|---|---|
已上报,token_save:true |
创建长期 Token,上报响应返回 token,由商户自行保存。 |
已上报,token_save:false |
不创建长期 Token。 |
| 下单超过 1 小时,仍未上报,且下单返回数据表明成功下单 | KeepPay 自动创建长期 Token。 |
| 下单超过 1 小时,仍未上报,且下单返回数据表明下单失败 | 不创建长期 Token。 |
“超过 1 小时”的自动处理规则仅用于尚未上报的订单,依据首次下单返回的数据判断是否成功下单, 不表示 KeepPay 已确认最终付款成功。商户仍应及时上报 PSP 异步通知的结果。
创建成功后的上报响应
{
"status": "ok",
"error_code": "",
"error_message": "",
"appid": "appid",
"psp": "stripe",
"order_no": "ord_first_250612_5521",
"trace_id": "11111111111111111111111111111111",
"token_save": true,
"token": "294270e5-2be1-4974-9188-96b3a1c8f25f",
"token_state": "created"
}
上报的所有 JSON 响应均包含 psp。已关联订单时,取该订单实际受理成功的通道;
未通过鉴权或没有关联到订单时,保留请求中的 PSP(缺失时为空),不会虚构已成功通道。
创建成功或复用已创建 Token 时 token_state="created";未创建时通常为 skipped;续费复用原 Token 时为
existing。
token_save:false 返回 token:null,但不能撤销此前已经创建的 Token。
error_code="token_writeback_failed"、token_state="created",即使
token 暂为空也表示已经创建,
应修复回写或重试原订单上报,不能生成第二个 Token。token_conversion_failed 时查看 error_message,
只重试上报,不重新下单;conversion_recovery_required 应联系支持核实转换结果。
超时自动创建后,可对原订单上报 token_save:true 取回已有 Token,不会再次扣款。
商户自行保存创建成功后返回的长期 token,续费时将它作为 /api/payments 的
token_id 提交。无需创建时,将同一上报请求中的 token_save 改为 false,响应返回
token:null。上报失败应只重试上报,不重复下单。
令牌管理
令牌查询和删除都必须在请求头中传入 X-KEEP-SIGN。接口只接收或返回 Token 标识及安全的掩码信息,
不会把完整卡号或 CVC 明文返回给你的服务器;可返回品牌、末四位及有效期等安全摘要。
读取令牌
此接口返回商户可见的 Token 摘要,包含卡片品牌、末四位及有效期等保留字段;不返回原始卡数据、租户信息、指纹和内部项目标识。
token_id、appid 是必填查询参数,token_id 请填写已创建并保存的长期
token,不要填写临时 Token Intent ID。
删除令牌
请求体中的 token_id、appid 必填。接口成功表示KeepPay已登记删除并立即停用该Token,后续查询及续费返回
token_deleted(HTTP 410)。对外响应:删除申请成功时 status="ok"、error_code="";失败时
status="error",error_code 返回简短错误码,error_message 返回详细原因。不返回内部删除状态或处理时间。
curl https://payment-test.keeppay.net/api/token/delete \ -H "X-KEEP-SIGN: keep_uhmwb1t87…" \ -H "Content-Type: application/json" \ --data-raw '{ "appid": "appid", "token_id": "294270e5-2be1-4974-9188-96b3a1c8f25f" }'
⑥ 备用渠道
X-Airwallex-Access-Token。响应 psp 是实际受理通道,商户上报
/api/order/report 必须使用该值。同一订单号的重复请求不会重新编排,业务再次支付应使用新订单号。空中云汇参数及 3DS 说明见云汇接入。
同一业务订单在交易列表只显示一条,详情保留各次 PSP 尝试。下单接口不创建长期 Token;首次订单仍由上报 token_save 或一小时自动任务决定创建。KeepPay 内部编排不替代商户自己的备用渠道。
API 参考
当前对外提供以下四个 API。点击端点可跳转到对应的参数或使用说明。
| 方法 | 端点 | 说明 |
|---|---|---|
| POST | /api/payments | 首次扣款与后续扣款 |
| POST | /api/order/report | 上报订单状态与交易信息 |
| GET | /api/token/info?appid=appid&token_id=xxx | 查询令牌信息 |
| POST | /api/token/delete | 删除令牌 |
POST /api/payments
统一的首单和续费接口,当前支持 stripe、antom、payermax 和 airwallex,
各自使用同名原生参数对象。本接入文档中 PayerMax 仅适用首次下单,续费不适用;续费请选择 stripe、Antom 或 Airwallex。编排关闭时只调用 psp
指定的已开通通道;编排开启时先尝试该通道,
仅在 PSP 明确告知下单失败后,按优先级尝试其他已开通、配置完整且本次提供了参数的通道。
需要 3DS、已受理、超时或结果未知时不会继续切换,详见 备用渠道与编排。
订单幂等:同一环境、appid、order_no 对应一笔业务订单。
原请求重复提交会复用已有结果,不会重复扣款;更换初始 PSP 或修改参数不能让同一订单再次扣款。
真正的新业务扣款应使用新的订单号。请求超时或结果未知时,先通过 PSP 查询或 Webhook 核实原订单,避免重复扣款。
请求头
| 参数 | 必填 | 说明 |
|---|---|---|
| X-KEEP-SIGN | 是 | KeepPay 商户请求签名。 |
| X-KEEP-VERSION | 建议携带 | 当前约定版本号 2.0.1,示例统一携带;当前接口未对缺失此头实施强制拦截。 |
| Content-Type | 是 | 固定为 application/json。 |
| Authorization | Stripe 必填 | Stripe 示例使用 Bearer sk_test_…;具体鉴权值由商户保管并提供。 |
| X-Airwallex-Access-Token | 云汇必填 | 商户服务端获取的云汇访问令牌,原值提交,不加 Bearer 前缀。首次、续费及云汇编排候选均适用;
请提供有效令牌,失效时由商户服务端重新获取。
不用于数据上报或 BT Token 管理接口。 |
公共请求参数
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| action | string | 是 | 首次扣款传 first_charge;后续扣款传 recurring_charge。 |
| appid | string | 是 | KeepPay 项目应用ID,用于加载商户配置。 |
| order_no | string | 是 | 商户订单号,每笔业务订单必须唯一。 |
| token_intent_id | string | 首次下单必填 | 前端 tokenIntents.create 返回的临时凭证 id。仅用于 first_charge
的卡支付,首次下单时仅使用此字段,提交的 token_id 会被忽略。
|
| token_id | string | 续费必填 | 商户已保存的长期 Token,例如 /api/order/report 创建成功后返回的 token;续费时使用
recurring_charge,提交的
token_intent_id 会被忽略。
|
| payment | string | 否 | Card 场景固定传 card;未传时 Keeppay默认使用 card。 |
| psp | string | 是 | 本次实际支付通道:stripe、antom、payermax 或 airwallex。 |
| {{psp_name}} | object | 必填 | psp_name是各个渠道的名称 stripe通道:stripe antom通道:antom PayerMax通道:payermax 空中云汇通道:airwallex 其他通道:联系客服 |
支付响应与长期 Token
下单和上报响应中的 trace_id 是服务端生成的32位请求日志标识,可用于查询本次请求。重复请求各有独立标识,但复用原订单结果,不重复扣款或创建 Token。
/api/payments 的 params 保留 PSP 官方响应,响应不包含 token 字段。
首次下单只使用临时 token_intent_id,此时不主动创建长期 Token。
长期 Token 的创建由后续 /api/order/report 的 token_save 或超过 1 小时未上报的规则决定,
详见 数据上报。
stripe通道参数示例
该对象按照 Stripe PaymentIntent 创建参数传递。完整字段、类型和约束以 Stripe Create a PaymentIntent 官方文档为准。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| amount | integer | 是 | 最小货币单位金额,例如 1990 表示 19.90 USD。 |
| currency | string | 是 | 三位小写货币代码,例如 usd。 |
| confirm | boolean | 是 | 示例固定为 true,创建后立即确认支付。 |
| payment_method_data.type | string | Token 支付必填 | Card Token 场景固定为 card;不要在该对象内提交卡号明文。 |
| setup_future_usage | string | 首次扣款后续扣款场景必填 | 首次扣款传 off_session,表示计划后续离线使用。 |
| off_session | boolean | 后续扣款必填 | 后续扣款传 true,表示持卡人不在场。 |
| automatic_payment_methods | object | 否 | 后续扣款可设置 enabled:true、allow_redirects:"never"。 |
| metadata | object | 否 | 业务元数据,建议传唯一交易号 trade_no。 |
| receipt_email | string | 否 | Stripe 支付收据邮箱。 |
| return_url | string | 按支付方式 | 仅在所选支付方式可能发生跳转时提供。 |
Stripe 接口文档入口:
查看
Stripe Create a PaymentIntent 官方接口文档 ↗
对应支付接口:POST /v1/payment_intents。
续费说明:Stripe 官方离线扣款与重新验证处理 ↗;
Stripe off_session 参数说明 ↗。
Stripe 续费使用已保存的长期 Token,confirm:true、off_session:true;无需 NTID。
续费授权、拒付及需要持卡人重新验证时的处理,以 Stripe 官方要求和账户能力为准。
antom参数示例
该对象按照 Antom Pay 请求参数传递。完整字段、类型和地区差异以 Antom Pay 官方文档及 Antom Card/MIT 集成文档为准。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| productCode | string | 是 | 当前 Card 支付固定使用 CASHIER_PAYMENT。 |
| paymentRequestId | string | 是 | 商户生成的唯一支付请求号,也是 Antom 侧幂等标识。 |
| paymentAmount | object | 是 | {currency,value};币种使用大写,金额为最小货币单位字符串。 |
| settlementStrategy | object | 按商户配置 | 结算币种对象,例如 {settlementCurrency:"USD"}。 |
| paymentMethod | object | 是 | paymentMethodType 使用 CARD,卡数据由 Token 注入。 |
| paymentMethod.paymentMethodMetaData.isCardOnFile | string | 是 | 首次扣款通常传 "false";后续扣款传 "true"。 |
| paymentMethod.paymentMethodMetaData.tokenize | string | 首次扣款按需 | 需要 Antom 保存后续使用信息时传 "true"。 |
| paymentMethod.paymentMethodMetaData.recurringType | string | 后续扣款必填 | 定期后续扣款传 SCHEDULED,非固定周期可按官方规则使用 UNSCHEDULED。 |
| paymentMethod.paymentMethodMetaData.networkTransactionId | string | 后续扣款必填 | 首次成功支付后从 PSP Webhook 提取并保存的 NTID,详见 通知字段路径;不要填写 PSP 订单号。 |
| order | object | 是 | 包含 referenceOrderId、orderDescription、orderAmount,可包含
buyer。
|
| paymentFactor | object | 是 | 当前 Card 授权请求使用 {isAuthorization:"true"}。 |
| env | object | 是 | 终端环境,例如 {terminalType:"WEB"},可按需传 clientIp。 |
| paymentRedirectUrl | string | 按支付流程 | 支付完成后的商户结果页地址。 |
| paymentNotifyUrl | string | 否 | Antom 异步支付结果通知地址。 |
Antom 接口文档入口:
查看 Antom pay
(One-time Payments) 官方接口文档 ↗
对应支付接口:POST /ams/api/v1/payments/pay。
NTID 续费参数:Antom 官方 More features:MIT 续费说明 ↗,
重点查看
paymentMethod.paymentMethodMetaData.networkTransactionId、isCardOnFile、recurringType;
NTID 来自首次成功交易的通知或查询结果。完整字段、适用产品与要求以官方文档及账户配置为准。
空中云汇 Airwallex 参数示例
使用 psp:"airwallex" 和同名 airwallex 对象,首单和续费都调用 /api/payments。
请求头传 Content-Type: application/json、X-Keep-Sign、X-KEEP-VERSION: 2.0.1,
以及商户服务端取得的 X-Airwallex-Access-Token。云汇 Client ID/API Key 由商户服务端保管,不能放进公开前端。
完整鉴权、设备指纹和 3DS 流程见 云汇接入说明。
| 参数 | 必填 | 说明 |
|---|---|---|
| amount | 是 | 主货币单位,0.59 表示 0.59 USD;与 Stripe、Antom 的最小货币单位不同。 |
| currency | 是 | 当前使用 USD。 |
| merchant_order_id | 是 | 与外层 order_no 一致,最长 64 字符。 |
| return_url | 首单必填 | 商户 HTTPS 回跳地址,回跳不代表支付成功,也不是 Webhook 地址。 |
| device_data | 首单按风控要求 | device_id 与前端设备指纹会话一致;ip_address 使用支付用户的真实公网 IP。 |
| external_recurring_data | 续费必填 | 首单 initial_payment=true、triggered_by=customer;续费 initial_payment=false、triggered_by=merchant。 |
| external_recurring_data.original_transaction_id | 续费必填 | 原成功交易的 NTID,非空字符串;KeepPay 不校验固定长度,由云汇判断有效性。 |
| external_recurring_data.lifecycle_id | 按账户与卡组织要求 | 按实际 API 版本和官方规则提供。 |
| payment_method_options.card.auto_capture | 当前固定 true | 自动捕获。卡数据由安全 Token 提供,续费不提交 CVC。 |
云汇首单请求体
{
"appid": "demo_app",
"order_no": "KP-AIRWALLEX-FIRST-001",
"psp": "airwallex",
"action": "first_charge",
"payment": "card",
"token_intent_id": "00000000-0000-4000-8000-000000000001",
"airwallex": {
"amount": 0.59,
"currency": "USD",
"merchant_order_id": "KP-AIRWALLEX-FIRST-001",
"return_url": "https://merchant.example/payment/airwallex-return",
"device_data": {
"device_id": "与前端指纹脚本一致的随机UUID",
"ip_address": "支付用户的真实公网IP"
},
"external_recurring_data": {
"initial_payment": true,
"triggered_by": "customer"
},
"payment_method_options": {
"card": {
"auto_capture": true
}
},
"metadata": {
"plan_name": "月度套餐"
}
}
}
示例中的 appid、Token UUID、设备标识和 IP 需替换为实际值;根据云汇账户风控要求补齐 customer、持卡人姓名和账单信息。
首单若返回验证要求,请按 params.next_action 完成 3DS。异步通知地址在云汇后台配置,不能用 return_url 代替。
云汇续费请求体
{
"appid": "demo_app",
"order_no": "KP-AIRWALLEX-RECURRING-001",
"psp": "airwallex",
"action": "recurring_charge",
"payment": "card",
"token_id": "00000000-0000-4000-8000-000000000002",
"airwallex": {
"amount": 0.59,
"currency": "USD",
"merchant_order_id": "KP-AIRWALLEX-RECURRING-001",
"external_recurring_data": {
"initial_payment": false,
"triggered_by": "merchant",
"merchant_trigger_reason": "scheduled",
"original_transaction_id": "485224265480053"
},
"payment_method_options": {
"card": {
"auto_capture": true
}
},
"metadata": {
"plan_name": "月度套餐续费"
}
}
}
token_id 使用商户已保存的长期 Token;NTID 应来自同一张卡的原成功交易,并满足有效续费授权。
不传 CVC;merchant_trigger_reason 按授权场景使用 scheduled、unscheduled 或 installments。
下单响应不创建长期 Token,仍按 数据上报规则处理。
云汇官方接口与续费说明: 首次下单:Create a PaymentIntent ↗; 确认支付及 external_recurring_data 参数 ↗; 续费:外部存卡与再次扣款 ↗。 重点参考 initial_payment、triggered_by、merchant_trigger_reason、original_transaction_id。示例仅供参考,具体以官方文档、账户能力和实际 API 版本为准。
PayerMax:首次下单参数
本接入文档的 PayerMax 适用首次下单,续费参数不适用;跨通道续费请使用其他已开通通道。首次提交前端获取的 token_intent_id,不在请求中填写卡号、有效期或 CVV。以下为
KeepPay 的 payermax 对象字段,完整规则以官方文档为准。
| 字段 | 类型 | 说明 |
|---|---|---|
| outTradeNo | string | 商户订单号,与外层 order_no 一致。 |
| integrate | string | Direct_Payment。 |
| subject | string | 购买内容或订单标题。 |
| totalAmount / currency | number / string | 金额以主要货币单位提交,例如 0.59 USD,使用 JSON 数字;不是分。 |
| country / userId / terminalType | string | 付款国家、商户用户标识及终端类型,例如 US / user_001 / WEB。 |
| frontCallbackUrl / notifyUrl | string | 浏览器回跳及商户异步通知接收地址。回跳不代表付款成功。 |
| paymentDetail.paymentMethodType | string | CARD。 |
| paymentDetail.buyerInfo | object | 买家 email、真实 clientIp 和 userAgent,按官方账户要求提交。 |
官方参考:PayerMax Direct API 参数、支付结果通知。PayerMax 自有支付 Token 与 KeepPay 长期 Token 不同。
curl https://payment-test.keeppay.net/api/payments \
-X POST \
-H "Content-Type: application/json" \
-H "X-KEEP-SIGN: YOUR_KEEP_SIGN" \
-H "X-KEEP-VERSION: 2.0.1" \
--data-raw '{
"appid": "YOUR_APPID",
"order_no": "ord_payermax_001",
"psp": "payermax",
"action": "first_charge",
"payment": "card",
"token_intent_id": "TOKEN_INTENT_ID_FROM_FRONTEND",
"payermax": {
"outTradeNo": "ord_payermax_001",
"integrate": "Direct_Payment",
"subject": "Monthly membership",
"totalAmount": 0.59,
"currency": "USD",
"country": "US",
"userId": "user_001",
"terminalType": "WEB",
"frontCallbackUrl": "https://merchant.example/payment/return",
"notifyUrl": "https://merchant.example/payment/notify",
"paymentDetail": {
"paymentMethodType": "CARD",
"buyerInfo": {
"email": "[email protected]",
"clientIp": "203.0.113.10",
"userAgent": "REPLACE_WITH_BUYER_USER_AGENT"
}
}
}
}'
下单响应不会创建或返回长期 Token。收到并验证 PSP 通知后,上报原订单号和 token_save;需要续费时同时保存通知中的 NTID,详见数据上报。
POST /api/order/report
长期 Token 创建时机:下单接口不创建或返回长期 Token。
不同首次订单默认分别创建长期 Token;同一订单重试复用已有 Token,续费继续使用已保存 Token。
上报传 token_save:true 时创建并返回 token,传 false 时不创建。
超过 1 小时仍未上报时,按首次下单返回数据判断:成功下单自动创建,下单失败不创建。
完整规则见 数据上报注意事项。
上报商户通过 KeepPay 通道产生的订单状态、金额、交易时间及 Token 保存决策。传入下单响应中的实际 psp,
请求无需再次传临时或长期 Token ID。完整 cURL 示例及统一响应字段见
④ 数据上报和上报响应说明。
该接口是商户主动同步:商户应先从 PSP 获得订单结果,再将结果同步给 KeepPay,用于订单、Token 和通道信息关联。 KeepPay 不会通过该接口向 PSP 查询订单,也不会验证商户上报状态与 PSP 实际状态是否一致;最终支付状态以商户自己的 PSP Webhook 或 PSP 查询结果为准。
数据上报请求头
| 参数 | 必填 | 说明 |
|---|---|---|
| X-KEEP-SIGN | 是 | KeepPay 商户请求签名。 |
| Content-Type | 是 | 固定为 application/json。 |
数据上报请求体
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| appid | string | 是 | KeepPay 项目应用ID。 |
| psp | string | 是 | 订单实际使用的 PSP 名称。 |
| order_no | string | 是 | 与首次下单一致的商户订单号。KeepPay 按当前项目的订单记录获取临时 Token,无需再次提交 Token ID。 |
| order_status | string | 是 | unpay(未支付)、paying(支付中)、paid(支付成功)或
failed(支付失败)。仅接受这四个值。 |
| order_amount | integer | 是 | 交易金额(分)。 |
| order_at | string | 是 | UTC 交易时间,格式为 YYYY-MM-DD HH:mm:ss,例如 2026-10-20 12:23:21。 |
| token_save | boolean | 是 | true:根据该订单记录的临时 Token 创建长期 Token;false:不创建。
使用 JSON 布尔值,不要传字符串。 |
| ntid | string | 否 | PSP 首次成功支付通知中的网络交易标识。最多 128 字符,非必填;按原值上报,不进行 Visa/Mastercard 长度判断。KeepPay 记录该上报值,不验证其真实性,也不将其当作长期 Token。获取路径见 NTID 说明。 |
错误处理
所有 JSON 响应都会包含顶层字段 status 和 error_code。支付接口仍会在
params 中原样返回 Stripe 或 Antom 的官方响应,客户端应先检查顶层状态,再处理
params 中的支付状态或 3DS 信息。
Worker 的所有 JSON 响应还统一包含 error_message:成功时 status="ok",
error_code、error_message 均为空字符串;失败时 status="error",
error_code 是简短错误码,error_message 是详细说明。删除 Token 接口也使用相同规则。
HTTP 状态码来自 HTTP 响应,并不是 JSON 中的 http_status 字段;OPTIONS 预检返回空响应体。
商户错误文案统一使用 KeepPay,已有嵌套 error、message 等业务字段继续保留。
{
"status": "error",
"error_code": "payment_failed",
"error_message": "Your card was declined.",
"trace_id": "11111111111111111111111111111111",
"appid": "appid",
"psp": "stripe",
"params": {
"error": {
"message": "Your card was declined."
}
}
}
| HTTP | status | error_code | 含义 |
|---|---|---|---|
| 400 / 422 | error |
invalid_request / invalid_payment / invalid_report |
请求或通道参数不符合要求,详细原因见 error_message |
| 402 | error |
payment_failed |
PSP 明确返回下单失败 |
| 403 | error |
project_error / sign_error |
项目配置、归属或商户接口密钥错误 |
| 404 | error |
not_found / token_not_found / order_not_found |
接口、Token 或关联订单不存在 |
| 409 | error |
order_conflict / order_processing / legacy_order_exists |
订单参数冲突、正在处理或历史订单已存在,不得重复扣款 |
| 409 | error |
order_not_selected / order_not_accepted |
上报通道不是实际受理通道,或订单尚未确认受理 |
| 409 | error |
token_processing / conversion_recovery_required |
Token 正在处理或需要核实转换结果 |
| 410 | error |
token_deleted |
Token 已停用,不能查询或续费 |
| 422 | error |
no_available_channel |
没有可执行的支付通道 |
| 429 | error |
rate_limited |
请求触发限流 |
| 502 | error |
airwallex_auth_failed |
云汇鉴权失败,请核对令牌及环境 |
| 500 / 502 | error |
token_conversion_failed |
长期 Token 创建失败,只重试上报,不重新下单 |
| 502 | error |
token_writeback_failed |
Token 已创建但回写待补偿,不要创建第二个 Token |
| 503 | error |
channel_config_missing / token_state_unavailable |
通道配置缺失或无法确认 Token 状态 |
| 503 | error |
payment_outcome_unknown |
支付结果未知,先核实原订单,不能直接重新扣款 |
| 500 | error |
server_error |
内部异常,请携带 trace_id 联系支持(如响应包含该字段) |
正常下单和需要进行 3DS 验证的下单通常返回 HTTP 200、status:"ok"、
error_code:""、error_message:""。需要 3DS 时,请继续读取 params 中 PSP 官方返回的
status、next_action、跳转地址或验证表单。
status: ok 的含义:KeepPay 返回的 status 仅表示转发请求是否被 KeepPay 正常处理,
不表示支付已经成功、扣款已经完成或订单可以发货。商户必须根据 PSP 返回的官方响应、PSP Webhook 或 PSP 查询接口判断最终支付结果。- 商户调用 KeepPay。
- KeepPay 转发请求到 PSP。
- KeepPay 返回 PSP 的
next_action、redirect、client_secret等数据。 - 商户根据 PSP 官方要求完成 3DS。
- 商户等待 PSP Webhook 或自行查询 PSP 最终状态。