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; KeepPay 返回 PSP 下单结果,此时不创建长期 Token,响应不包含 token 字段。
  3. 数据上报 —— 商户收到 PSP 异步通知后调用 /api/order/report, 提交订单号及 token_save。传 true 时创建长期 Token,商户自行保存上报响应的 token; 传 false 时不创建。
  4. 用令牌扣款 —— 用已保存的长期 Token 提交到 /api/payments发起续费扣款,明文在合规环境内还原注入,你的系统不碰 PAN。
🔐
合规要点:明文 PAN 始终在 PCI Level 1 合规底座内,你的 PCI 范围收敛到 SAQ A。控制台与你的服务器全程只接触令牌。

API 基址:

# 测试环境 https://payment-test.keeppay.net # 正式环境 https://payment.keeppay.net

快速开始

开发前准备:创建项目与申请支付渠道

新商户开通管理后台账号后,请先完成项目申请和支付渠道申请。两项申请均审核通过后,再获取密钥并开始接入开发。

  1. 创建项目:登录商户管理后台,点击左上角项目选择框,在下拉菜单中选择“增加项目”; 填写项目名称、部署区域及备注,点击“保存”,提交项目申请并等待审核。项目用于区分不同业务的支付数据和接入配置。
  2. 申请支付渠道:在左上角选择需要接入的项目,进入“设置 → 支付通道”,点击“申请接入通道”; 按页面要求填写申请信息、选择所需 PSP,点击“提交申请”。在“申请记录”中查看审核状态和意见; 审核通过并接通后,该渠道会显示为“已开通”。需要接入多个 PSP 时,分别申请相应渠道。
  3. 获取密钥并开始开发:确认项目审核通过、所需支付渠道已开通后,选择对应项目和运行环境, 进入“设置 → 密钥配置”,获取项目(应用)ID(appid)、前端密钥和接口密钥,替换到下方接入示例中。 前端密钥用于浏览器采集组件,接口密钥由商户服务端保管并通过 X-Keep-Sign 请求头提交;测试与正式环境的密钥请分别使用。
⚠️
项目与支付渠道均需审核通过后才能进入接入开发。尚未通过或未显示可用密钥时,请联系 KeepPay 完成开通, 不要使用其他项目或其他环境的密钥代替。

前端与服务端接入

从获取临时 Token Intent 到首次下单、接收 PSP 异步通知并上报,再保存长期 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-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头中传入 后端私钥

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-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"
  }
}'
🔀
多通道请求同时携带 Stripe 和 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。
💾
首次下单后必须处理:保存商户订单号;按 PSP 要求完成 3DS, 从支付成功的 Webhook 中取得并保存 NTID,再调用 /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 或尚未支付成功时,应等待成功通知或服务端查单确认后再保存。

⚠️
字段缺失时: 不要使用商户订单号、Stripe 的 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"
  }
}
🧾
收到 PSP 异步通知后,商户使用原订单号调用 /api/order/report, 通过 token_save 决定是否创建长期 Token。成功下单包括 PSP 已接受但需要 3DS 或仍在处理中的订单, 不等同于最终付款成功;商户仍须以 PSP 异步通知或查询结果判断资金状态。

③ 后续扣款

后续扣款到期后,将数据上报创建后保存的长期 token 作为请求字段 token_id 发起离线后续扣款(MIT),用户无需再次输入卡信息。 当前后续扣款仍调用统一的 /api/payments,并通过 action:"recurring_charge" 标识后续扣款。本文档的 PayerMax 接入仅适用首次下单,续费不适用。

POST/api/payments
⚠️
Stripe 续费: 使用已保存的长期 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。

⚠️
Stripe 续费使用长期 token_id、confirm:true 和 off_session:true,无需 NTID;续费不传 setup_future_usage 或 CVC。Antom 续费必须在 antom.paymentMethod.paymentMethodMetaData.networkTransactionId 中传入首次成功支付后保存的 NTID。 通知字段路径见 各 PSP 的 NTID 说明。

④ 数据上报

⚠️
注意:长期 Token 在数据上报时决定是否创建。首次下单不会创建或返回长期 Token。 商户收到 PSP 异步通知后,应上报原订单号和 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 获得的订单结果同步给 KeepPay,便于订单、Token 和通道信息关联。 KeepPay 不会通过该接口向 PSP 查询订单,也不会验证商户上报的订单状态是否与 PSP 实际状态一致。最终支付状态以商户自己的 PSP Webhook 或 PSP 查询结果为准。
POST/api/order/report

请求示例

以下示例上报首次订单的 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 摘要,包含卡片品牌、末四位及有效期等保留字段;不返回原始卡数据、租户信息、指纹和内部项目标识。

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 必填。接口成功表示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"
}'
🗂️
删除操作不可通过 API 恢复。申请前请确认该Token不再关联有效续费;申请后不可继续使用,当月存储费仍计收,下月不再计费,历史订单及审计记录保留。申请不会撤销已受理或在途的支付。

⑥ 备用渠道

KeepPay 内部支付编排:在支付通道页面按项目、环境开启并设置优先级。首次下单可同时提供 stripe、antom、payermax、airwallex;续费示例仅使用 stripe、antom、airwallex 对象,订单号、USD 金额必须一致;缺少参数、未开通或未实现的通道不参与。各通道使用同一临时 Token(首次)或长期 Token(续费),每个 PSP 最多尝试一次。Stripe 参与时需提供其独立 Authorization Bearer;云汇参与时须提供独立且有效的 X-Airwallex-Access-Token。响应 psp 是实际受理通道,商户上报 /api/order/report 必须使用该值。同一订单号的重复请求不会重新编排,业务再次支付应使用新订单号。空中云汇参数及 3DS 说明见云汇接入。

同一业务订单在交易列表只显示一条,详情保留各次 PSP 尝试。下单接口不创建长期 Token;首次订单仍由上报 token_save 或一小时自动任务决定创建。KeepPay 内部编排不替代商户自己的备用渠道。

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

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