API Key Mode - HTTP API Integration
Use API Key on server-side to create and manage orders via HTTP API. Suitable for scenarios requiring full control over payment flow.
Get Your API Key
- Log in to PolyPay merchant dashboard
- Go to "API Keys" page
- Click "Create Key"
- Securely save the generated API Key
⚠️ API Key is only shown once. Save it immediately! If lost, you need to regenerate. Never expose API Key in frontend code, Git repos, or logs.
Plan Quotas and Billing Scope
- Subscription plans apply at the account level. All merchants under the account share monthly successful order, wallet address, notification, and notification channel quotas.
- Production order creation checks the account-level monthly successful order quota. Once the limit is reached, any merchant under the account must upgrade the plan before creating new orders.
- Payment Webhook callbacks are used for order status synchronization and do not count toward external alert notification quota.
- Telegram, Email, WhatsApp, notification Webhook, WeCom, Discord, and other merchant business alerts count toward the account-level plan quota; in-app notifications and system subscription expiry notices do not.
- Subscription expiring and expired notices are sent by PolyPay with fixed system copy and cannot be customized in notification templates.
API Reference
Basic Information
| Base URL | https://api.polypay.ai/api/v1/pay/sdk |
| Authentication | X-API-Key (recommended) or Bearer Token (legacy-compatible) |
| Data Format | JSON |
Authentication
All API requests must include the API Key in the header:
| Header | Value |
|---|---|
X-API-Key | YOUR_API_KEY (recommended) |
Authorization | Bearer YOUR_API_KEY (legacy-compatible) |
Content-Type | application/json |
Redirect to Hosted Checkout
If you want PolyPay to show the payment method selection page, do not call /order/add first. Use your API Key on the server to request the checkout_url below, then redirect the customer to that URL. Without currency/network, hosted checkout shows payment method selection. With both currency and network, it skips selection and opens the payment page directly. The same merchant order reuses an unexpired order only for the same payment method, so customers can return to selection and choose a different chain or currency.
POST /order/checkout
curl -X POST https://api.polypay.ai/api/v1/pay/sdk/order/checkout \
-H "Content-Type: application/json" \
-H "X-API-Key: YOUR_API_KEY" \
-d '{
"mch_order_id": "ORDER_001",
"amount": 10.00,
"notify_url": "https://your-site.com/webhook",
"redirect_url": "https://your-site.com/success",
"locale": "en"
}'Response example:
{
"code": 0,
"message": "",
"data": {
"checkout_url": "https://checkout.polypay.ai/checkout?cs=cs_xxx&return_url=https%3A%2F%2Fmerchant.example%2Fsuccess",
"payment_url": "https://checkout.polypay.ai/checkout?cs=cs_xxx&return_url=https%3A%2F%2Fmerchant.example%2Fsuccess"
}
}Create Order
POST /order/add
Request Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
currency | string | ✅ | Currency (USDT/USDC/BUSD) |
network | string | ✅ | Network (tron/ethereum/bsc/polygon/solana) |
amount | number | ✅ | Amount |
mch_order_id | string | ❌ | Merchant order ID (max 32 chars, auto-generated if not provided) |
notify_url | string | ❌ | Webhook callback URL |
redirect_url | string | ❌ | Redirect URL after payment |
Response Parameters
| Parameter | Type | Description |
|---|---|---|
trade_id | string | PolyPay transaction ID |
currency | string | Currency |
network | string | Blockchain network |
amount | number | Order amount |
actual_amount | number | Actual payment amount (4 decimals) |
address | string | Payment wallet address |
expiration_time | number | Expiration timestamp (seconds) |
payment_url | string | Payment page URL |
Code Examples
curl -X POST https://api.polypay.ai/api/v1/pay/sdk/order/add \
-H "X-API-Key: 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"
}'Response Example
{
"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.polypay.ai/status/PP202412110001"
}
}Query Order
POST /order/detail
Request Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
trade_id | string | * | PolyPay transaction ID |
mch_order_id | string | * | Merchant order ID |
* Either trade_id or mch_order_id is required
Response Example
curl -X POST https://api.polypay.ai/api/v1/pay/sdk/order/detail \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"trade_id": "PP202412110001"
}'Webhook Callbacks
We send HTTP POST requests to your configured Webhook URL when an order is created and when its status changes.
Webhook Payload Example
{
"order_no": "ORDER_123456",
"status": 2,
"amount": 100.0001,
"currency": "USDT",
"currency_name": "usdt",
"network": "Tron",
"contract_addr": "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
"hash": "abc123...",
"wallet_address": "TXxx...xxx",
"environment": "production",
"event_id": "evt_xxx"
}network is the payment chain; currency_name is the lowercase currency name such as usdt/usdc; hash is the on-chain transaction hash. contract_addr is an empty string for native assets or assets without a configured contract.
Status Types
| status Value | Description |
|---|---|
1 | Pending payment |
2 | Payment successful |
3 | Order expired |
4 | Order cancelled |
5 | Manual recharge (handled as paid) |
6 | On-chain confirming (treated as pending for merchants; no paid webhook is sent) |
7 | Marked paid (handled as paid) |
Verify Webhook Signature
Use the examples on this page together with the latest official docs as the source of truth. The protocol may evolve, so keep your server-side verifier updated. Never store API keys or verification secrets in frontend code.
Recommended Verification Flow (Server-side)
- Accept callbacks on server-side only. Do not verify in frontend code.
- Validate source and freshness first; reject expired or replayed requests.
- Use the official plugin or your internal wrapper to verify request integrity.
- Process business logic only after verification and enforce idempotency by order number.
- Return 401 for auth failures, 400 for bad payload, and 200/OK on success.
import crypto from 'crypto';
import express from 'express';
import { markEventProcessed } from './idempotency-store';
const app = express();
const apiKey = process.env.POLYPAY_API_KEY || '';
const nonceStore = new Map(); // 生产建议换 Redis
app.use(express.json({
type: 'application/json',
verify: (req, _res, buf) => {
req.rawBody = buf.toString('utf8');
}
}));
function verifyPolyPayWebhook(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 (!verifyPolyPayWebhook(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');
});Error Codes
| Code | Description |
|---|---|
0 | Success |
10001 | Invalid parameters |
10002 | Invalid signature |
10003 | Order not found |
10004 | Merchant disabled |
10005 | Invalid API Key |
Security Best Practices
- Protect API Key: Never expose API Key in frontend code, Git repos, or logs
- Use Environment Variables: Store API Key in environment variables
- Verify Webhook Signatures: Always verify Webhook request signatures
- Use HTTPS: Ensure your Webhook endpoint uses HTTPS
- Implement Idempotency: Webhooks may be sent multiple times, ensure idempotent logic