PonponPay
中级15 分钟阅读

API Key 模式 - HTTP API 集成

使用 API Key 在服务端通过 HTTP API 创建和管理订单,适合需要完整控制支付流程的场景。

获取 API Key

  1. 登录 PonponPay 商户后台
  2. 进入「API 密钥」页面
  3. 点击「创建密钥」
  4. 安全保存生成的 API Key

⚠️ API Key 只会显示一次,请立即保存。如果丢失,需要重新生成。永远不要在前端代码、Git 仓库或日志中暴露 API Key。

API 接口文档

基础信息

Base URLhttps://api.ponponpay.com/pay
认证方式Bearer Token (API Key)
数据格式JSON

认证方式

所有 API 请求需要在 Header 中携带 API Key:

Header
AuthorizationBearer YOUR_API_KEY
Content-Typeapplication/json

创建订单

POST /order/add

请求参数

参数类型必填说明
currencystring币种 (USDT/USDC/BUSD)
networkstring网络 (tron/ethereum/bsc/polygon/solana)
amountnumber金额
mch_order_idstring商户订单号(最长32位,未传则自动生成)
notify_urlstringWebhook 回调地址
redirect_urlstring支付完成跳转地址

响应参数

参数类型说明
trade_idstringPonponPay 交易号
currencystring币种
networkstring区块链网络
amountnumber订单金额
actual_amountnumber实际需支付金额(保留4位小数)
addressstring收款钱包地址
expiration_timenumber过期时间戳(秒)
payment_urlstring支付页面 URL

代码示例

curl -X POST https://api.ponponpay.com/pay/order/add \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "currency": "USDT",
    "network": "tron",
    "amount": 100.00,
    "mch_order_id": "ORDER_123456",
    "notify_url": "https://your-site.com/webhook",
    "redirect_url": "https://your-site.com/success"
  }'

响应示例

{
  "code": 0,
  "message": "success",
  "data": {
    "trade_id": "PP202412110001",
    "currency": "USDT",
    "network": "tron",
    "amount": 100.00,
    "actual_amount": 100.0001,
    "address": "TXxx...xxx",
    "expiration_time": 1704067200,
    "payment_url": "https://checkout.ponponpay.com/status/PP202412110001"
  }
}

查询订单

POST /order/detail

请求参数

参数类型必填说明
trade_idstring*PonponPay 交易号
mch_order_idstring*商户订单号

* trade_id 和 mch_order_id 任意传其中一个

响应示例

curl -X POST https://api.ponponpay.com/pay/order/detail \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "trade_id": "PP202412110001"
  }'

Webhook 回调

当订单状态变化时,我们会向你配置的 Webhook URL 发送 HTTP POST 请求。

状态类型

status 值描述
1等待支付
2支付成功
3订单已过期
4订单已取消
5人工充值(按支付成功处理)

验证 Webhook 签名

回调验签请以本文示例与官方最新文档为准。协议后续可能升级,升级时请同步更新服务端验签模块。请勿在前端保存 API Key 或将验签密钥暴露到客户端。

推荐验证流程(服务端)

  1. 只允许服务端接收回调,前端不参与鉴权。
  2. 先做来源与时效校验,过期或重复请求直接拒绝。
  3. 使用官方插件或内部封装方法完成请求完整性校验。
  4. 验证通过后再处理业务,并按订单号做幂等(重复回调只处理一次)。
  5. 校验失败返回 401,格式错误返回 400,成功返回 200/OK。
import crypto from 'crypto';
import express from 'express';
import { markEventProcessed } from './idempotency-store';

const app = express();
const apiKey = process.env.PONPONPAY_API_KEY || '';
const nonceStore = new Map(); // 生产建议换 Redis

app.use(express.json({
  type: 'application/json',
  verify: (req, _res, buf) => {
    req.rawBody = buf.toString('utf8');
  }
}));

function verifyPonponPayWebhook(req) {
  const prefix = String(req.headers['x-key-prefix'] || '');
  const timestamp = String(req.headers['x-timestamp'] || '');
  const nonce = String(req.headers['x-nonce'] || '');
  const signature = String(req.headers['x-signature'] || '').toLowerCase();
  const rawBody = req.rawBody || '';

  if (!apiKey || !prefix || !timestamp || !nonce || !signature) return false;
  if (!/^\d+$/.test(timestamp)) return false;

  const ts = Number(timestamp);
  const now = Math.floor(Date.now() / 1000);
  if (Math.abs(now - ts) > 300) return false;
  if (prefix !== apiKey.slice(0, 12)) return false;
  if (!/^[A-Za-z0-9]{16,128}$/.test(nonce)) return false;

  const nonceKey = `${timestamp}:${nonce}`;
  if (nonceStore.has(nonceKey)) return false;
  nonceStore.set(nonceKey, Date.now());
  setTimeout(() => nonceStore.delete(nonceKey), 10 * 60 * 1000);

  const keyHash = crypto.createHash('sha256').update(apiKey).digest('hex');
  const payload = `${timestamp}\n${nonce}\n${rawBody}`;
  const expected = crypto.createHmac('sha256', keyHash).update(payload).digest('hex');

  const a = Buffer.from(signature, 'utf8');
  const b = Buffer.from(expected, 'utf8');
  if (a.length !== b.length) return false;
  return crypto.timingSafeEqual(a, b);
}

app.post('/webhook', express.json({ type: 'application/json' }), async (req, res) => {
  if (!verifyPonponPayWebhook(req)) {
    return res.status(401).send('Unauthorized');
  }

  const event = req.body;
  if (!event?.order_no || event?.status === undefined) {
    return res.status(400).send('Bad Request');
  }

  // 幂等保护:相同订单事件只处理一次
  const firstTime = await markEventProcessed(event.order_no, event.status);
  if (!firstTime) {
    return res.status(200).send('OK');
  }

  switch (Number(event.status)) {
    case 2:
    case 5:
      await handleOrderPaid(event.data);
      break;
    case 3:
      await handleOrderExpired(event.data);
      break;
    case 4:
      await handleOrderCancelled(event.data);
      break;
    default:
      break;
  }

  res.status(200).send('OK');
});

错误码

错误码说明
0成功
10001参数错误
10002签名错误
10003订单不存在
10004商户已禁用
10005API Key 无效

安全最佳实践

  • 保护 API Key:永远不要在前端代码、Git 仓库或日志中暴露 API Key
  • 使用环境变量:将 API Key 存储在环境变量中
  • 验证 Webhook 签名:始终验证 Webhook 请求的签名
  • 使用 HTTPS:确保你的 Webhook 端点使用 HTTPS
  • 实现幂等性:Webhook 可能会重复发送,处理逻辑要保证幂等性