KeepPay 开发者文档
在用户浏览器侧令牌化,明文永不落你的服务器;用令牌向任意能收卡的通道发起扣款,一套卡库对接谁都行。
Keep your data · Keep your choices · Keep your revenue
概述
KeepPay 是基于 PCI Level 1 合规底座的支付编排平台。集成只有三步:
- 前端采集 —— 用采集组件在用户浏览器获取临时 Token Intent ID,明文不经过你的前后端。
- 首次扣款 —— 后端将临时 Token Intent ID提交到
/api/payments下单;下单成功后,商户自行保存响应中的长期token。 - 用令牌扣款 —— 用已保存的长期 Token 提交到
/api/payments发起续费扣款,明文在合规环境内还原注入,你的系统不碰 PAN。
API 基址:
快速开始
从获取测试卡的临时 Token Intent 到首次下单、保存长期 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-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-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-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-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": "RENEWAL_20260813_000001", "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,二者不能同时提交。另需
stripe 中的 amount、
currency、confirm。Stripe后续扣款传
off_session:true;Antom后续扣款在其官方参数中传 networkTransactionId。
token、
network_transaction_id 和实际 psp。首次扣款由 Stripe 或 Antom 完成均可继续后续扣款。
核心概念
| 概念 | 说明 |
|---|---|
| 项目(应用) | 你的一个 App,一一对应。每个项目 = 一套前端公钥 / 后端私钥 / 授权域名;令牌、通道、计费都按项目归集。 |
| 临时 Token Intent(token_intent_id) | 前端采集组件返回的临时凭证,用于首次下单,不用于续费。 |
| 长期 Token(响应 token / 请求 token_id) | 首次下单成功后返回的卡引用。商户自行保存响应的 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-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-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。然后再根据后台配置的PSP优先级顺序,依次尝试下单,遇到成功即返回。尝试顺序:顶层
psp > 优先级列表(优先级越高越靠前使用)
X-KEEP-SIGN 用于 KeepPay 鉴权;Stripe 示例还需要
Authorization: Bearer sk_test_…。token_intent_id 来自前端
tokenIntents.create 返回的 id,
action 固定为 first_charge。
token、
network_transaction_id。无论首次扣款由 Stripe 还是 Antom 完成,
这些数据都用于后续扣款;切换到 Antom 后续扣款时必须提供首次扣款 NTID(network_transaction_id)。
首次下单响应与 Token 保存
首次下单成功后,响应顶层的 token 是用于后续续费的长期 Token,商户必须自行在后端保存,
并关联到对应的项目、用户及后续扣款。下单失败时,不返回可保存的长期 Token(token 为空或不存在)。
请勿保存临时 token_intent_id 作为续费凭证。
{
"status": "ok",
"error_code": "",
"appid": "appid",
"psp": "stripe",
"params": {
"id": "pi_example",
"object": "payment_intent",
"status": "succeeded"
},
"token": "294270e5-2be1-4974-9188-96b3a1c8f25f"
}
token 后由商户自行保存;下一次续费将该值填入请求的 token_id。
PSP 接受订单但要求 3DS 或仍在处理中,也可能返回长期 Token;商户仍须完成验证并确认最终支付结果。
若 token 为空,不能据此认定订单失败或直接重新扣款,应先核实 params 中的 PSP 订单状态。③ 后续扣款
后续扣款到期后,将首次下单响应中保存的长期 token 作为请求字段 token_id 发起离线后续扣款(MIT),用户无需再次输入卡信息。
当前后续扣款仍调用统一的 /api/payments,并通过 action:"recurring_charge" 标识后续扣款。
stripe单一通道
curl https://payment-test.keeppay.net/api/payments \ -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-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": "RENEWAL_20260813_000001", "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 指定的渠道。设置为
stripe 时使用 stripe;改为 antom 时使用
antom,其中 networkTransactionId 必须来自首次成功支付。同样,多通道时,尝试顺序:顶层
psp > 优先级列表(优先级越高越靠前使用)
后续扣款通道与首次扣款通道
首次扣款通道与后续扣款通道没有必然的关系。例如,首次扣款是stripe完成,后续扣款可以由antom完成。请求的 psp 决定实际后续扣款通道:设置为
stripe 时读取 stripe;设置为 antom 时读取
antom。
off_session:true。Antom 后续扣款必须在
antom.paymentMethod.paymentMethodMetaData.networkTransactionId 中传入首次扣款保存的 NTID。
④ 数据上报
商户在支付、退款等订单状态确定后,可调用数据上报接口将订单结果同步给 KeepPay。该接口只接收订单与令牌关联信息, 不提交卡号、有效期或 CVC 等敏感卡数据。
请求示例
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": "antom", "order_no": "ord_renew_2026_07", "order_status": "paid", "order_amount": 1999, "order_at": "2024-06-10 12:34:21", "token_id": "token_err..." }'
请求参数
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| appid | string | 是 | KeepPay 分配的项目应用ID,KeepPay后台获取。 |
| psp | string | 是 | 该订单实际使用的 PSP 名称,例如 stripe 或 antom。 |
| order_no | string | 是 | 商户侧唯一订单号。 |
| order_status | string | 是 | unpay:未支付;paid:支付成功;refund:已退款。 |
| order_amount | integer | 是 | 交易金额(分),示例值为 1999。 |
| order_at | string | 是 | 交易时间,格式为 YYYY-MM-DD HH:mm:ss。 |
| token_id | string | 是 | 该订单对应的 KeepPay 令牌 ID。 |
令牌管理
令牌查询和删除都必须在请求头中传入 X-KEEP-SIGN。接口只接收或返回 Token 标识及安全的掩码信息,
不会把卡号、有效期或 CVC 明文返回给你的服务器。
读取令牌
token_id、appid 是必填查询参数,token_id 请填写首次下单响应中已保存的长期
token,不要填写临时 Token Intent ID。
删除令牌
请求体中的 token_id、appid 必填。删除成功后,该 Token 不能再用于首次扣款或后续扣款。
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" }'
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
统一的首次扣款和后续扣款接口。Keeppay读取 psp 后,只把对应的 PSP 原生参数对象发送到该通道:
psp:"stripe" 使用 stripe,psp:"antom" 使用
antom。
请求头
| 参数 | 必填 | 说明 |
|---|---|---|
| X-KEEP-SIGN | 是 | KeepPay 商户请求签名。 |
| X-KEEP-VERSION | 是 | 版本号,固定 2.0.1。 |
| Content-Type | 是 | 固定为 application/json。 |
| Authorization | Stripe 必填 | Stripe 示例使用 Bearer sk_test_…;具体鉴权值由商户保管并提供。 |
公共请求参数
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| 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,即首次下单响应中的 token;续费时使用 recurring_charge。 |
| payment | string | 否 | Card 场景固定传 card;未传时 Keeppay默认使用 card。 |
| psp | string | 是 | 本次实际支付通道:stripe 或 antom。 |
| {{psp_name}} | object | 必填 | psp_name是各个渠道的名称 stripe通道:stripe antom通道:antom pingpong通道:pingpong 空中云汇通道:airwallex 其他通道:联系客服 |
支付响应中的 Token
params 保留 PSP 官方响应;顶层 token 为长期 Token。
首次下单成功且返回非空 token 时,商户自行保存,后续以 token_id 提交续费请求。
下单失败时不返回可保存的长期 Token。临时 token_intent_id 与响应 token 的用途不同。
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 | 按支付方式 | 仅在所选支付方式可能发生跳转时提供。 |
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 | 后续扣款必填 | 首次扣款返回并保存的 NTID。 |
| order | object | 是 | 包含 referenceOrderId、orderDescription、orderAmount,可包含
buyer。
|
| paymentFactor | object | 是 | 当前 Card 授权请求使用 {isAuthorization:"true"}。 |
| env | object | 是 | 终端环境,例如 {terminalType:"WEB"},可按需传 clientIp。 |
| paymentRedirectUrl | string | 按支付流程 | 支付完成后的商户结果页地址。 |
| paymentNotifyUrl | string | 否 | Antom 异步支付结果通知地址。 |
POST /api/order/report
上报商户通过 KeepPay 通道产生的订单状态、金额、交易时间及对应令牌。完整 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 | 是 | 商户订单号。 |
| order_status | string | 是 | unpay、paid 或 refund。 |
| order_amount | integer | 是 | 交易金额(分)。 |
| order_at | string | 是 | 交易时间,格式为 YYYY-MM-DD HH:mm:ss。 |
| token_id | string | 是 | 订单对应的 KeepPay 令牌 ID。 |
错误处理
所有 JSON 响应都会包含顶层字段 status 和 error_code。支付接口仍会在
params 中原样返回 Stripe 或 Antom 的官方响应,客户端应先检查顶层状态,再处理
params 中的支付状态或 3DS 信息。
{
"status": "error",
"error_code": "payment_failed",
"appid": "appid",
"psp": "stripe",
"params": {
"error": {
"message": "Your card was declined."
}
},
"token": null
}
| HTTP | status | error_code | 含义 |
|---|---|---|---|
400 |
error |
具体参数校验错误信息 | 请求参数校验失败 |
402 |
error |
payment_failed |
PSP 返回支付失败;具体原因保留在 params |
403 |
error |
merchant_error / sign_error |
商户号有误或者密钥验证失败 |
404 |
error |
not_found |
接口或路由不存在 |
429 |
error |
rate_limited |
触发限流 |
500 |
error |
server_error |
内部异常 |
正常下单和需要进行 3DS 验证的下单通常返回 HTTP 200、status:"ok"、
error_code:""。需要 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 最终状态。