Keep Pay 开发者文档
控制台

KeepPay 开发者文档

在用户浏览器侧令牌化,明文永不落你的服务器;用令牌向任意能收卡的通道发起扣款,一套卡库对接谁都行。

Keep your data · Keep your choices · Keep your revenue

🧭
平台职责边界:KeepPay 是支付编排和请求转发平台。KeepPay 不持有商户在 PSP 侧的密钥, 不代替商户查询 PSP 订单,不接收商户 PSP Webhook,也不判断商户订单的最终资金状态。商户必须自行完成 PSP 鉴权、 3DS 处理、Webhook 配置、订单查询、退款/撤销处理以及最终发货判断。

概述

KeepPay 是基于 PCI Level 1 合规底座的支付编排平台。集成只有三步:

  1. 前端采集 —— 用采集组件在用户浏览器获取临时 Token Intent ID,明文不经过你的前后端。
  2. 首次扣款 —— 后端将临时 Token Intent ID提交到 /api/payments 下单;下单成功后,商户自行保存响应中的长期 token。
  3. 用令牌扣款 —— 用已保存的长期 Token 提交到 /api/payments发起续费扣款,明文在合规环境内还原注入,你的系统不碰 PAN。
🔐
合规要点:明文 PAN 始终在 PCI Level 1 合规底座内,你的 PCI 范围收敛到 SAQ A。控制台与你的服务器全程只接触令牌。

API 基址:

# 测试环境 https://payment-test.keeppay.net # 正式环境 https://payment.keeppay.net
🔀
商户必须准备自己的备用支付渠道。当 KeepPay 无法使用时,商户应尽快切换回自己的支付渠道完成下单, 并提前完成备用渠道的接入和切换验证。KeepPay 请求超时或结果未知时,应先通过 PSP 查询或 Webhook 核实原订单状态, 确认不会重复扣款后再切换;备用渠道应使用其自身支持的支付凭证。

快速开始

从获取测试卡的临时 Token Intent 到首次下单、保存长期 Token,再发起续费扣款:

前端 · 完整 HTML(复制即可运行)
⚠️
示例代码必须通过 HTTPS 访问才能运行。请将页面部署到项目已授权的 HTTPS 域名, 替换前端公钥后访问;不要直接双击 HTML 文件。

使用 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 内。临时凭证过期后,应重新获取。
后端 · cURL

示例使用 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头中传入 后端私钥

X-Keep-Sign: keep_uhmwb1t87…
⚠️
后端私钥绝不下发到前端。前端只使用项目的前端公钥,权限仅用于创建临时 Token Intent。
商户根据PSP要求,传入对应的鉴权参数。例如:stripe鉴权,是在header头中加入请求参数:Authorization: Bearer sk_test_…。keeppay不参与商户与PSP之间的鉴权参数配置。

① 前端采集组件

Card 采集组件是由 Keeppay 托管的嵌入式 iframe。你的页面只负责加载 SDK、使用 前端公钥 初始化组件并指定挂载位置;卡号、有效期和 CVC 不会进入你的页面 DOM 或服务器。

完整 HTML · 引用 Card 组件
<!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>
🌐
浏览器中只能使用 前端公钥。示例代码必须通过 HTTPS 访问才能运行,不要直接使用 file:// 打开;完整 HTML 页面需声明 <meta charset="UTF-8">。获取临时 token_intent_id、提交后端并发起支付的完整流程请参见 快速开始;采集组件只在已配置的授权域名加载。

② 首次扣款

前端取得临时 Token Intent 的 id 后,由你的后端以 token_intent_id 字段调用 KeepPay 下单。首次扣款属于持卡人发起交易(CIT), 示例选择 Stripe,并通过 setup_future_usage:"off_session" 为后续离线后续扣款建立使用条件。

POST/api/payments

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"
  }
}'
🔀
多通道请求同时携带 Stripe 和 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" 标识后续扣款。

POST/api/payments

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。

⚠️
Stripe 后续扣款需要 off_session:true。Antom 后续扣款必须在 antom.paymentMethod.paymentMethodMetaData.networkTransactionId 中传入首次扣款保存的 NTID。

④ 数据上报

商户在支付、退款等订单状态确定后,可调用数据上报接口将订单结果同步给 KeepPay。该接口只接收订单与令牌关联信息, 不提交卡号、有效期或 CVC 等敏感卡数据。

🧾
商户主动同步:该接口用于商户将已从 PSP 获得的订单结果同步给 KeepPay,便于订单、Token 和通道信息关联。 KeepPay 不会通过该接口向 PSP 查询订单,也不会验证商户上报的订单状态是否与 PSP 实际状态一致。最终支付状态以商户自己的 PSP Webhook 或 PSP 查询结果为准。
POST/api/order/report

请求示例

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 明文返回给你的服务器。

读取令牌

GET/api/token/info?appid=appid&token_id=xxx

token_id、appid 是必填查询参数,token_id 请填写首次下单响应中已保存的长期 token,不要填写临时 Token Intent ID。

curl --get https://payment-test.keeppay.net/api/token/info \ -H "X-KEEP-SIGN: keep_uhmwb1t87…" \ --data-urlencode "token_id=294270e5-2be1-4974-9188-96b3a1c8f25f"\ --data-urlencode "appid=appid"

删除令牌

POST/api/token/delete

请求体中的 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 恢复。删除前请确认该 Token 不再关联有效后续扣款;删除后令牌不可继续扣款,相关审计记录仍可保留。

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 查询接口判断最终支付结果。
🔁
3DS 流程:KeepPay 转发,商户执行。
  1. 商户调用 KeepPay。
  2. KeepPay 转发请求到 PSP。
  3. KeepPay 返回 PSP 的 next_action、redirect、client_secret 等数据。
  4. 商户根据 PSP 官方要求完成 3DS。
  5. 商户等待 PSP Webhook 或自行查询 PSP 最终状态。
KeepPay 开发者文档