支付引擎 30 分钟接入实战
通过一条完整的实战链路快速跑通支付引擎:创建订单、拉起收银台、接收回调与状态查询。
本指南帮助您在 30 分钟内跑通一条完整的支付引擎链路:创建订单 → 跳转收银台或展示收款地址 → 接收订单回调 → 查询订单终态。
验收目标
完成本文后,您应满足以下 4 个验收条件:
- 能成功调用
/api/v2/checkout创建订单 - 能拿到
cregis_id、checkout_url和payment_info[x].payment_address,并引导用户跳转收银台或以地址收款 - 回调服务能验证
sign并返回success - 能基于回调或
/api/v2/order/info判定订单状态
第 0 步:准备参数(约 5 分钟)
请先准备以下信息:
BASE_URL:项目专属 Base URL(请在 Cregis 客户端 > 支付引擎 > 项目设置页面 > Base URL 复制)PID:支付引擎项目 IDAPI_KEY:支付引擎项目 API KeyCALLBACK_URL:可公网访问的订单回调地址
Base URL 为项目专属 Base URL(请在 Cregis 客户端 > 支付引擎 > 项目设置页面 > Base URL 复制)。
不同项目的 Base URL 可能不同,请勿在项目间复用。
建议在接入时把当前项目的 Base URL 配成环境变量 BASE_URL,并与该项目的 PID、API_KEY 绑定管理。
相关文档:
第 1 步:准备签名函数(约 5 分钟)
所有请求都需要 nonce、timestamp、sign。下面提供 Node.js / Python / PHP 三种可复用签名函数:
import crypto from 'crypto';
export function buildSignedBody(apiKey, bizBody) {
const body = {
...bizBody,
nonce: crypto.randomUUID().replaceAll('-', '').slice(0, 8),
timestamp: Date.now(),
};
const canonical = Object.keys(body)
.filter((k) => body[k] !== '' && body[k] !== null && body[k] !== undefined)
.sort()
.map((k) => `${k}${body[k]}`)
.join('');
const sign = crypto
.createHash('md5')
.update(`${apiKey}${canonical}`)
.digest('hex')
.toLowerCase();
return { ...body, sign };
}import hashlib
import secrets
import time
def build_signed_body(api_key, biz_body):
body = {
**biz_body,
'nonce': secrets.token_hex(4),
'timestamp': int(time.time() * 1000),
}
canonical = ''.join(
f'{key}{body[key]}'
for key in sorted(body)
if body[key] not in ('', None)
)
body['sign'] = hashlib.md5(f'{api_key}{canonical}'.encode('utf-8')).hexdigest().lower()
return body<?php
function buildSignedBody(string $apiKey, array $bizBody): array
{
$body = $bizBody;
$body['nonce'] = bin2hex(random_bytes(4));
$body['timestamp'] = (int) round(microtime(true) * 1000);
$sorted = $body;
ksort($sorted, SORT_STRING);
$canonical = '';
foreach ($sorted as $key => $value) {
if ($value === '' || $value === null) {
continue;
}
$canonical .= $key . $value;
}
$body['sign'] = strtolower(md5($apiKey . $canonical));
return $body;
}timestamp 必须为发请求当下的毫秒时间戳,不能长期复用历史值。第 2 步:创建订单(约 8 分钟)
调用 /api/v2/checkout 创建订单,拿到收银台链接和收款地址。
curl -X POST "${BASE_URL}/api/v2/checkout" \
-H "Content-Type: application/json" \
-d '{
"pid": 1382528827416576,
"order_id": "merchant_order_20260417_0001",
"order_amount": "99.00",
"order_currency": "USD",
"payer_id": "user_10001",
"valid_time": 30,
"success_url": "https://merchant.example.com/pay/success",
"cancel_url": "https://merchant.example.com/pay/cancel",
"callback_url": "https://merchant.example.com/webhook/payment-engine",
"nonce": "n6k2q1",
"timestamp": 1776393000000,
"sign": "<按签名算法计算>"
}'import crypto from 'crypto';
const BASE_URL = process.env.CREGIS_BASE_URL;
const API_KEY = process.env.CREGIS_API_KEY;
function buildSignedBody(apiKey, bizBody) {
const body = {
...bizBody,
nonce: crypto.randomUUID().replaceAll('-', '').slice(0, 8),
timestamp: Date.now(),
};
const canonical = Object.keys(body)
.filter((k) => body[k] !== '' && body[k] !== null && body[k] !== undefined)
.sort()
.map((k) => `${k}${body[k]}`)
.join('');
const sign = crypto
.createHash('md5')
.update(`${apiKey}${canonical}`)
.digest('hex')
.toLowerCase();
return { ...body, sign };
}
const body = buildSignedBody(API_KEY, {
"pid": 1382528827416576,
"order_id": "merchant_order_20260417_0001",
"order_amount": "99.00",
"order_currency": "USD",
"payer_id": "user_10001",
"valid_time": 30,
"success_url": "https://merchant.example.com/pay/success",
"cancel_url": "https://merchant.example.com/pay/cancel",
"callback_url": "https://merchant.example.com/webhook/payment-engine"
});
const response = await fetch(`${BASE_URL}/api/v2/checkout`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
console.log(await response.json());import hashlib
import os
import secrets
import time
import requests
BASE_URL = os.environ['CREGIS_BASE_URL']
API_KEY = os.environ['CREGIS_API_KEY']
def build_signed_body(api_key, biz_body):
body = {
**biz_body,
'nonce': secrets.token_hex(4),
'timestamp': int(time.time() * 1000),
}
canonical = ''.join(
f'{key}{body[key]}'
for key in sorted(body)
if body[key] not in ('', None)
)
body['sign'] = hashlib.md5(f'{api_key}{canonical}'.encode('utf-8')).hexdigest().lower()
return body
body = build_signed_body(API_KEY, {
'pid': 1382528827416576,
'order_id': 'merchant_order_20260417_0001',
'order_amount': '99.00',
'order_currency': 'USD',
'payer_id': 'user_10001',
'valid_time': 30,
'success_url': 'https://merchant.example.com/pay/success',
'cancel_url': 'https://merchant.example.com/pay/cancel',
'callback_url': 'https://merchant.example.com/webhook/payment-engine',
})
headers = {'Content-Type': 'application/json'}
response = requests.post(f'{BASE_URL}/api/v2/checkout', json=body, headers=headers, timeout=10)
print(response.json())<?php
$baseUrl = getenv('CREGIS_BASE_URL');
$apiKey = getenv('CREGIS_API_KEY');
function buildSignedBody(string $apiKey, array $bizBody): array
{
$body = $bizBody;
$body['nonce'] = bin2hex(random_bytes(4));
$body['timestamp'] = (int) round(microtime(true) * 1000);
$sorted = $body;
ksort($sorted, SORT_STRING);
$canonical = '';
foreach ($sorted as $key => $value) {
if ($value === '' || $value === null) {
continue;
}
$canonical .= $key . $value;
}
$body['sign'] = strtolower(md5($apiKey . $canonical));
return $body;
}
$body = buildSignedBody($apiKey, [
'pid' => 1382528827416576,
'order_id' => 'merchant_order_20260417_0001',
'order_amount' => '99.00',
'order_currency' => 'USD',
'payer_id' => 'user_10001',
'valid_time' => 30,
'success_url' => 'https://merchant.example.com/pay/success',
'cancel_url' => 'https://merchant.example.com/pay/cancel',
'callback_url' => 'https://merchant.example.com/webhook/payment-engine',
]);
$context = stream_context_create([
'http' => [
'method' => 'POST',
'header' => "Content-Type: application/json\r\n",
'content' => json_encode($body, JSON_UNESCAPED_SLASHES),
'timeout' => 10,
],
]);
echo file_get_contents($baseUrl . '/api/v2/checkout', false, $context);成功响应里重点关注:
data.cregis_id:Cregis 侧订单流水号(后续查询与对账主键)data.checkout_url:收银台链接(给前端跳转或生成二维码)data.payment_info[x].payment_address:收款地址(可用于自建支付页展示地址收款)
第 3 步:拉起收银台或展示收款地址(约 3 分钟)
前端拿到 checkout_url 后,可以直接跳转 Cregis 收银台:
window.location.href = checkoutUrl;如要建立自家的支付页,可读取 payment_info[x].payment_address 作为收款地址,并展示对应币种、网络和应收金额,引导用户向该地址付款。
用户支付过程中,订单可能出现多种事件类型(如 paid、paid_partial、paid_over、expired)。
第 4 步:接收订单回调(约 7 分钟)
支付引擎会向您的 callback_url 推送订单状态变化。回调外层关键字段:
event_name:固定为orderevent_type:paid/paid_partial/paid_over/expired/refunded/paid_remaindata:订单与支付明细
示例(Node.js / Python / PHP 最小可运行):
import express from 'express';
import crypto from 'crypto';
const app = express();
app.use(express.json());
const API_KEY = process.env.CREGIS_API_KEY;
const processed = new Set(); // 生产环境请替换为数据库或 Redis 幂等键
function buildSign(apiKey, payload) {
const canonical = Object.keys(payload)
.filter((k) => k !== 'sign' && payload[k] !== '' && payload[k] !== null && payload[k] !== undefined)
.sort()
.map((k) => `${k}${payload[k]}`)
.join('');
return crypto
.createHash('md5')
.update(`${apiKey}${canonical}`)
.digest('hex')
.toLowerCase();
}
app.post('/webhook/payment-engine', (req, res) => {
const body = req.body || {};
if (buildSign(API_KEY, body) !== body.sign) {
return res.status(401).send('invalid sign');
}
const cregisId = body?.data?.cregis_id || 'no-cregis-id';
const txId = body?.data?.tx_id || body?.data?.refund_tx_id || 'no-tx';
const idemKey = `${body.event_name || 'order'}:${body.event_type || 'unknown'}:${cregisId}:${txId}`;
if (processed.has(idemKey)) {
return res.status(200).send('success');
}
processed.add(idemKey);
// TODO: 写入你的业务状态流转逻辑
// 例如:paid -> 订单完成;expired -> 订单关闭;refunded -> 标记退款完成
return res.status(200).send('success');
});
app.listen(8080, () => console.log('Payment Engine webhook server running on :8080'));import hashlib
import os
from flask import Flask, request
app = Flask(__name__)
API_KEY = os.environ['CREGIS_API_KEY']
processed = set() # Use a database/Redis idempotency key in production
def build_sign(api_key, payload):
canonical = ''.join(
f'{key}{payload[key]}'
for key in sorted(payload)
if key != 'sign' and payload[key] not in ('', None)
)
return hashlib.md5(f'{api_key}{canonical}'.encode('utf-8')).hexdigest().lower()
@app.post('/webhook/payment-engine')
def payment_engine_webhook():
body = request.get_json(silent=True) or {}
if build_sign(API_KEY, body) != body.get('sign'):
return 'invalid sign', 401
data = body.get('data') or {}
cregis_id = data.get('cregis_id', 'no-cregis-id')
tx_id = data.get('tx_id') or data.get('refund_tx_id') or 'no-tx'
idem_key = f"{body.get('event_name', 'order')}:{body.get('event_type', 'unknown')}:{cregis_id}:{tx_id}"
if idem_key in processed:
return 'success', 200
processed.add(idem_key)
# TODO: 写入你的业务状态流转逻辑
# 例如:paid -> 订单完成;expired -> 订单关闭;refunded -> 标记退款完成
return 'success', 200
if __name__ == '__main__':
app.run(host='0.0.0.0', port=8080)<?php
$apiKey = getenv('CREGIS_API_KEY');
$body = json_decode(file_get_contents('php://input'), true) ?: [];
function buildSign(string $apiKey, array $payload): string
{
$sorted = $payload;
unset($sorted['sign']);
ksort($sorted, SORT_STRING);
$canonical = '';
foreach ($sorted as $key => $value) {
if ($value === '' || $value === null) {
continue;
}
$canonical .= $key . $value;
}
return strtolower(md5($apiKey . $canonical));
}
if (buildSign($apiKey, $body) !== ($body['sign'] ?? null)) {
http_response_code(401);
echo 'invalid sign';
exit;
}
$data = $body['data'] ?? [];
$cregisId = $data['cregis_id'] ?? 'no-cregis-id';
$txId = $data['tx_id'] ?? $data['refund_tx_id'] ?? 'no-tx';
$idemKey = ($body['event_name'] ?? 'order') . ':' . ($body['event_type'] ?? 'unknown') . ':' . $cregisId . ':' . $txId;
// Production: store $idemKey in a database/Redis unique index for idempotency.
// TODO: 写入你的业务状态流转逻辑
// 例如:paid -> 订单完成;expired -> 订单关闭;refunded -> 标记退款完成
http_response_code(200);
echo 'success';回调成功判定建议遵循:HTTP 200 且响应体返回纯文本 success。第 5 步:查询订单状态(约 2 分钟)
建议采用“回调优先 + 查询兜底”策略。兜底查询接口为 /api/v2/order/info。
curl -X POST "${BASE_URL}/api/v2/order/info" \
-H "Content-Type: application/json" \
-d '{
"pid": 1382528827416576,
"cregis_id": "po1420761885130752",
"nonce": "a1b2c3",
"timestamp": 1776393120000,
"sign": "<按签名算法计算>"
}'import crypto from 'crypto';
const BASE_URL = process.env.CREGIS_BASE_URL;
const API_KEY = process.env.CREGIS_API_KEY;
function buildSignedBody(apiKey, bizBody) {
const body = {
...bizBody,
nonce: crypto.randomUUID().replaceAll('-', '').slice(0, 8),
timestamp: Date.now(),
};
const canonical = Object.keys(body)
.filter((k) => body[k] !== '' && body[k] !== null && body[k] !== undefined)
.sort()
.map((k) => `${k}${body[k]}`)
.join('');
const sign = crypto
.createHash('md5')
.update(`${apiKey}${canonical}`)
.digest('hex')
.toLowerCase();
return { ...body, sign };
}
const body = buildSignedBody(API_KEY, {
"pid": 1382528827416576,
"cregis_id": "po1420761885130752"
});
const response = await fetch(`${BASE_URL}/api/v2/order/info`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
console.log(await response.json());import hashlib
import os
import secrets
import time
import requests
BASE_URL = os.environ['CREGIS_BASE_URL']
API_KEY = os.environ['CREGIS_API_KEY']
def build_signed_body(api_key, biz_body):
body = {
**biz_body,
'nonce': secrets.token_hex(4),
'timestamp': int(time.time() * 1000),
}
canonical = ''.join(
f'{key}{body[key]}'
for key in sorted(body)
if body[key] not in ('', None)
)
body['sign'] = hashlib.md5(f'{api_key}{canonical}'.encode('utf-8')).hexdigest().lower()
return body
body = build_signed_body(API_KEY, {
'pid': 1382528827416576,
'cregis_id': 'po1420761885130752',
})
headers = {'Content-Type': 'application/json'}
response = requests.post(f'{BASE_URL}/api/v2/order/info', json=body, headers=headers, timeout=10)
print(response.json())<?php
$baseUrl = getenv('CREGIS_BASE_URL');
$apiKey = getenv('CREGIS_API_KEY');
function buildSignedBody(string $apiKey, array $bizBody): array
{
$body = $bizBody;
$body['nonce'] = bin2hex(random_bytes(4));
$body['timestamp'] = (int) round(microtime(true) * 1000);
$sorted = $body;
ksort($sorted, SORT_STRING);
$canonical = '';
foreach ($sorted as $key => $value) {
if ($value === '' || $value === null) {
continue;
}
$canonical .= $key . $value;
}
$body['sign'] = strtolower(md5($apiKey . $canonical));
return $body;
}
$body = buildSignedBody($apiKey, [
'pid' => 1382528827416576,
'cregis_id' => 'po1420761885130752',
]);
$context = stream_context_create([
'http' => [
'method' => 'POST',
'header' => "Content-Type: application/json\r\n",
'content' => json_encode($body, JSON_UNESCAPED_SLASHES),
'timeout' => 10,
],
]);
echo file_get_contents($baseUrl . '/api/v2/order/info', false, $context);重点读取 data.status:
new:订单初始状态paid:订单已成功支付expired:订单超时(超出 valid_time)paid_over:付款超出订单金额paid_partial:付款小于订单金额canceled:全额退款完成
上线前检查清单
- 已区分开发/生产环境的 PID 与 API Key
- 已实现回调验签、幂等、防重复处理
- 已保存
cregis_id作为平台订单主键 - 已建立
event_type与内部订单状态映射 - 回调失败监控已打通
常见问题(快速排障)
1) 创建订单成功但用户打不开收银台?
优先检查 checkout_url 是否被前端错误编码、是否被 CSP 或网关拦截。
2) 为什么会收到同一订单多次回调?
支付引擎允许同一订单触发多次 event_type(如先 paid_partial 后 paid_remain),请按事件流转处理,并做幂等。
3) 查询接口状态和回调事件名称不一致怎么办?
这是正常设计:回调表达“事件类型”,查询表达“当前状态”。请在业务侧维护统一状态机。
如果你已经跑通本篇,下一步建议继续阅读: