StablePay Docs
产品开发者联系我们
首页产品文档快速开始APISaaS平台
首页
产品文档
支付产品能力自动货币转换
快速开始
快速开始身份认证支付会话商户交易退款
API
API 参考版本控制身份认证请求与响应分页频率限制错误处理支付服务退款服务Webhook 通知
SaaS平台
ShopifyWooCommerceShoplazza 店匠
  • API 参考
  • 版本控制
  • 身份认证
  • 请求与响应
  • 分页
  • 频率限制
  • 错误处理

支付平台 API

  • 支付服务
  • 退款服务
  • Webhook 通知
StablePay 开发者文档
Home
Product
Quick Start
API
SaaS
Login
Register
GitHub
Home
Product
Quick Start
API
SaaS
Login
Register
GitHub
  • API 概览 / Overview

    • API 参考 / Reference
    • 版本控制 / Versioning
    • 身份认证 / Authentication
    • 请求与响应 / Requests & Responses
    • 分页 / Pagination
    • 频率限制 / Rate Limits
    • 错误处理 / Errors
  • 支付平台 API / Payment API

    • 支付服务 / Payment Service
    • 退款服务 / Refund Service
    • Webhook 通知 / Webhook

Webhook 通知

概述

当支付状态发生变化时(如支付完成、支付失败、退款成功等),StablePay 会向商户配置的 Webhook URL 发送 HTTP POST 请求通知商户。商户需要正确接收并验证这些通知,以便更新订单状态。

基础信息

项目值
请求方法POST
Content-Typeapplication/json
字符编码UTF-8
超时时间30 秒

When payment status changes (such as payment completed, payment failed, refund succeeded, etc.), StablePay will send HTTP POST requests to the merchant's configured Webhook URL. Merchants need to correctly receive and verify these notifications to update order status.

Basic Information

ItemValue
Request MethodPOST
Content-Typeapplication/json
Character EncodingUTF-8
Timeout30 seconds

请求头

Header类型必填说明
Content-Typestring是固定值 application/json
X-StablePay-Signaturestring是HMAC-SHA256 签名(十六进制编码)
X-StablePay-Timestampstring是Unix 时间戳(秒),签名生成时间
X-StablePay-Noncestring是UUID v4 随机数,用于防重放攻击
X-StablePay-Event-Typestring是事件类型(如 payment.completed)
X-StablePay-Event-IDstring是事件唯一标识,用于幂等性检查
User-Agentstring是固定值 StablePay-Webhook/1.0
HeaderTypeRequiredDescription
Content-TypestringYesFixed value application/json
X-StablePay-SignaturestringYesHMAC-SHA256 signature (hex encoded)
X-StablePay-TimestampstringYesUnix timestamp (seconds), signature generation time
X-StablePay-NoncestringYesUUID v4 random string for replay attack prevention
X-StablePay-Event-TypestringYesEvent type (e.g., payment.completed)
X-StablePay-Event-IDstringYesUnique event identifier for idempotency check
User-AgentstringYesFixed value StablePay-Webhook/1.0

请求头示例

POST /webhook/stablepay HTTP/1.1
Host: your-server.com
Content-Type: application/json
X-StablePay-Signature: 2bb80d537b1da3e38bd30361aa855686bde0eacd7162fef6a25fe97bf527a25b
X-StablePay-Timestamp: 1765786800
X-StablePay-Nonce: 550e8400-e29b-41d4-a716-446655440000
X-StablePay-Event-Type: payment.completed
X-StablePay-Event-ID: rec_abc123def456
User-Agent: StablePay-Webhook/1.0

签名验证

为确保 Webhook 请求的真实性和完整性,所有请求都包含 HMAC-SHA256 签名。商户必须验证签名后再处理请求。

签名算法

1. 获取请求头中的 timestamp、nonce 和原始请求体 (request_body)
2. 拼接签名字符串: sign_string = timestamp + "." + nonce + "." + request_body
3. 使用商户的 Secret Key 进行 HMAC-SHA256 计算
4. 将结果进行十六进制编码
5. 与请求头中的 X-StablePay-Signature 进行比对

签名验证步骤

  1. 获取签名参数:从请求头获取 X-StablePay-Signature、X-StablePay-Timestamp、X-StablePay-Nonce
  2. 验证时间戳:检查 timestamp 与当前时间差是否在 5 分钟以内(防重放攻击)
  3. 获取原始请求体:必须使用原始字节流,不能先解析再序列化
  4. 计算期望签名:按上述算法计算签名
  5. 安全比对:使用时间安全的比较函数(如 hmac.compare_digest)

To ensure the authenticity and integrity of Webhook requests, all requests include an HMAC-SHA256 signature. Merchants must verify the signature before processing the request.

Signature Algorithm

1. Get timestamp, nonce, and raw request body from request headers
2. Concatenate signature string: sign_string = timestamp + "." + nonce + "." + request_body
3. Calculate HMAC-SHA256 using the merchant's Secret Key
4. Hex encode the result
5. Compare with X-StablePay-Signature in the request header

Signature Verification Steps

  1. Get signature parameters: Extract X-StablePay-Signature, X-StablePay-Timestamp, X-StablePay-Nonce from request headers
  2. Validate timestamp: Check if the timestamp difference from current time is within 5 minutes (replay attack prevention)
  3. Get raw request body: Must use raw byte stream, do not parse and re-serialize
  4. Calculate expected signature: Calculate signature using the algorithm above
  5. Secure comparison: Use timing-safe comparison function (e.g., hmac.compare_digest)

签名验证代码示例

JavaScript/Node.js:

const crypto = require('crypto');

function verifyWebhookSignature(secretKey, signature, timestamp, nonce, requestBody) {
    // 1. 验证时间戳(5分钟有效期)/ Validate timestamp (5-minute validity)
    const currentTime = Math.floor(Date.now() / 1000);
    if (Math.abs(currentTime - parseInt(timestamp)) > 300) {
        return false;
    }
    
    // 2. 构建签名字符串 / Build signature string
    const signString = `${timestamp}.${nonce}.${requestBody}`;
    
    // 3. 计算期望签名 / Calculate expected signature
    const expectedSignature = crypto
        .createHmac('sha256', secretKey)
        .update(signString)
        .digest('hex');
    
    // 4. 时间安全比较 / Timing-safe comparison
    return crypto.timingSafeEqual(
        Buffer.from(signature),
        Buffer.from(expectedSignature)
    );
}

// Express.js 中间件示例 / Express.js middleware example
app.post('/webhook/stablepay', express.raw({ type: 'application/json' }), (req, res) => {
    const signature = req.headers['x-stablepay-signature'];
    const timestamp = req.headers['x-stablepay-timestamp'];
    const nonce = req.headers['x-stablepay-nonce'];
    const requestBody = req.body.toString();
    
    if (!verifyWebhookSignature(SECRET_KEY, signature, timestamp, nonce, requestBody)) {
        return res.status(401).json({ error: 'Invalid signature' });
    }
    
    // 处理 webhook 事件 / Process webhook event
    const event = JSON.parse(requestBody);
    // ...
    
    res.status(200).json({ received: true });
});

Python:

import hmac
import hashlib
import time

def verify_webhook_signature(secret_key, signature, timestamp, nonce, request_body):
    """验证 Webhook 签名 / Verify Webhook signature"""
    # 1. 验证时间戳(5分钟有效期)/ Validate timestamp (5-minute validity)
    current_time = int(time.time())
    if abs(current_time - int(timestamp)) > 300:
        return False
    
    # 2. 构建签名字符串 / Build signature string
    sign_string = f"{timestamp}.{nonce}.{request_body}"
    
    # 3. 计算期望签名 / Calculate expected signature
    expected_signature = hmac.new(
        secret_key.encode('utf-8'),
        sign_string.encode('utf-8'),
        hashlib.sha256
    ).hexdigest()
    
    # 4. 时间安全比较 / Timing-safe comparison
    return hmac.compare_digest(expected_signature, signature)


# Flask 示例 / Flask example
from flask import Flask, request, jsonify

@app.route('/webhook/stablepay', methods=['POST'])
def webhook_handler():
    signature = request.headers.get('X-StablePay-Signature')
    timestamp = request.headers.get('X-StablePay-Timestamp')
    nonce = request.headers.get('X-StablePay-Nonce')
    request_body = request.get_data(as_text=True)
    
    if not verify_webhook_signature(SECRET_KEY, signature, timestamp, nonce, request_body):
        return jsonify({'error': 'Invalid signature'}), 401
    
    # 处理 webhook 事件 / Process webhook event
    event = request.get_json()
    # ...
    
    return jsonify({'received': True}), 200

Go:

package main

import (
    "crypto/hmac"
    "crypto/sha256"
    "crypto/subtle"
    "encoding/hex"
    "fmt"
    "io"
    "math"
    "net/http"
    "strconv"
    "time"
)

func verifyWebhookSignature(secretKey, signature, timestamp, nonce, requestBody string) bool {
    // 1. 验证时间戳(5分钟有效期)/ Validate timestamp (5-minute validity)
    ts, err := strconv.ParseInt(timestamp, 10, 64)
    if err != nil {
        return false
    }
    if math.Abs(float64(time.Now().Unix()-ts)) > 300 {
        return false
    }
    
    // 2. 构建签名字符串 / Build signature string
    signString := fmt.Sprintf("%s.%s.%s", timestamp, nonce, requestBody)
    
    // 3. 计算期望签名 / Calculate expected signature
    mac := hmac.New(sha256.New, []byte(secretKey))
    mac.Write([]byte(signString))
    expectedSignature := hex.EncodeToString(mac.Sum(nil))
    
    // 4. 时间安全比较 / Timing-safe comparison
    return subtle.ConstantTimeCompare([]byte(signature), []byte(expectedSignature)) == 1
}

func webhookHandler(w http.ResponseWriter, r *http.Request) {
    signature := r.Header.Get("X-StablePay-Signature")
    timestamp := r.Header.Get("X-StablePay-Timestamp")
    nonce := r.Header.Get("X-StablePay-Nonce")
    
    body, _ := io.ReadAll(r.Body)
    requestBody := string(body)
    
    if !verifyWebhookSignature(secretKey, signature, timestamp, nonce, requestBody) {
        http.Error(w, `{"error": "Invalid signature"}`, http.StatusUnauthorized)
        return
    }
    
    // 处理 webhook 事件 / Process webhook event
    // ...
    
    w.WriteHeader(http.StatusOK)
    w.Write([]byte(`{"received": true}`))
}

PHP:

<?php
function verifyWebhookSignature($secretKey, $signature, $timestamp, $nonce, $requestBody) {
    // 1. 验证时间戳(5分钟有效期)/ Validate timestamp (5-minute validity)
    if (abs(time() - intval($timestamp)) > 300) {
        return false;
    }
    
    // 2. 构建签名字符串 / Build signature string
    $signString = $timestamp . '.' . $nonce . '.' . $requestBody;
    
    // 3. 计算期望签名 / Calculate expected signature
    $expectedSignature = hash_hmac('sha256', $signString, $secretKey);
    
    // 4. 时间安全比较 / Timing-safe comparison
    return hash_equals($expectedSignature, $signature);
}

// 使用示例 / Usage example
$signature = $_SERVER['HTTP_X_STABLEPAY_SIGNATURE'] ?? '';
$timestamp = $_SERVER['HTTP_X_STABLEPAY_TIMESTAMP'] ?? '';
$nonce = $_SERVER['HTTP_X_STABLEPAY_NONCE'] ?? '';
$requestBody = file_get_contents('php://input');

if (!verifyWebhookSignature($secretKey, $signature, $timestamp, $nonce, $requestBody)) {
    http_response_code(401);
    echo json_encode(['error' => 'Invalid signature']);
    exit;
}

// 处理 webhook 事件 / Process webhook event
$event = json_decode($requestBody, true);
// ...

http_response_code(200);
echo json_encode(['received' => true]);

事件类型

支付事件

事件类型说明触发条件
payment.completed支付成功用户完成支付,链上确认成功
payment.failed支付失败支付过程中发生错误
payment.expired支付过期支付会话超时未完成
payment.cancelled支付取消商户主动取消支付会话
Event TypeDescriptionTrigger Condition
payment.completedPayment succeededUser completed payment, on-chain confirmation successful
payment.failedPayment failedError occurred during payment process
payment.expiredPayment expiredPayment session timed out
payment.cancelledPayment cancelledMerchant actively cancelled payment session

退款事件

事件类型说明触发条件
refund.succeeded退款成功退款处理完成,资金已退回
refund.failed退款失败退款处理失败
Event TypeDescriptionTrigger Condition
refund.succeededRefund succeededRefund processing completed, funds returned
refund.failedRefund failedRefund processing failed

请求体格式

支付事件请求体

{
    "id": "evt_1765786800547928039",
    "type": "payment.completed",
    "created_at": 1765786800,
    "data": {
        "object": {
            "amount": "100.00",
            "status": "completed",
            "currency": "USDT",
            "exchange_rate": "7.25000000",
            "order_id": "ORDER-20250101-001",
            "session_id": "sess_abc123def456",
            "source": "api"
        }
    }
}

支付字段说明

字段路径类型说明
idstring事件 ID,格式 evt_{纳秒时间戳}
typestring事件类型
created_atint64事件创建时间(Unix 时间戳,秒)
data.object.amountstring支付金额(失败/过期时可能为空)
data.object.statusstring状态:completed / failed / expired / canceled
data.object.currencystring支付货币:USDT / USDC(失败/过期时可能为空)
data.object.exchange_ratestring汇率值
data.object.order_idstring商户订单 ID
data.object.session_idstring支付会话 ID
data.object.sourcestring来源系统:api
Field PathTypeDescription
idstringEvent ID, format evt_{nanosecond_timestamp}
typestringEvent type
created_atint64Event creation time (Unix timestamp, seconds)
data.object.amountstringPayment amount (may be empty for failed/expired)
data.object.statusstringStatus: completed / failed / expired / canceled
data.object.currencystringPayment currency: USDT / USDC (may be empty for failed/expired)
data.object.exchange_ratestringExchange rate value
data.object.order_idstringMerchant order ID
data.object.session_idstringPayment session ID
data.object.sourcestringSource system: api

退款事件请求体

{
    "id": "evt_1765786800547928040",
    "type": "refund.succeeded",
    "created_at": 1765786800,
    "data": {
        "object": {
            "session_id": "sess_abc123def456",
            "order_id": "ORDER-20250101-001",
            "refund_id": "ref_xyz789",
            "refund_amount": "50.00",
            "refund_currency": "USDT",
            "status": "completed",
            "source": "api"
        }
    }
}

退款字段说明

字段路径类型说明
idstring事件 ID
typestring事件类型
created_atint64事件创建时间
data.object.session_idstring原支付会话 ID
data.object.order_idstring商户订单 ID
data.object.refund_idstring退款 ID
data.object.refund_amountstring退款金额
data.object.refund_currencystring退款货币
data.object.statusstring退款状态:completed / failed
data.object.sourcestring来源系统
Field PathTypeDescription
idstringEvent ID
typestringEvent type
created_atint64Event creation time
data.object.session_idstringOriginal payment session ID
data.object.order_idstringMerchant order ID
data.object.refund_idstringRefund ID
data.object.refund_amountstringRefund amount
data.object.refund_currencystringRefund currency
data.object.statusstringRefund status: completed / failed
data.object.sourcestringSource system

响应要求

商户接收 Webhook 后,需要返回 HTTP 响应告知 StablePay 是否成功接收。

成功响应

返回 HTTP 2xx 状态码表示成功接收:

HTTP/1.1 200 OK
Content-Type: application/json

{
    "received": true
}

失败响应

HTTP 状态码说明StablePay 行为
2xx成功不再重试
429请求过多进入重试队列
5xx服务器错误进入重试队列
其他 4xx客户端错误不再重试

After receiving the Webhook, merchants need to return an HTTP response to inform StablePay whether it was successfully received.

Success Response

Return HTTP 2xx status code to indicate successful receipt:

HTTP/1.1 200 OK
Content-Type: application/json

{
    "received": true
}

Failure Response

HTTP Status CodeDescriptionStablePay Behavior
2xxSuccessNo retry
429Too many requestsEnter retry queue
5xxServer errorEnter retry queue
Other 4xxClient errorNo retry

重试策略

当 Webhook 发送失败(网络错误或非 2xx 响应)时,StablePay 会自动重试。

配置项值
最大重试次数5 次
重试间隔指数退避(1s, 2s, 4s, 8s, 16s)
超时时间30 秒
可重试状态码429, 5xx

When Webhook delivery fails (network error or non-2xx response), StablePay will automatically retry.

ConfigurationValue
Max Retry Attempts5 times
Retry IntervalExponential backoff (1s, 2s, 4s, 8s, 16s)
Timeout30 seconds
Retryable Status Codes429, 5xx

幂等性处理

由于网络问题或重试机制,商户可能收到重复的 Webhook 通知。商户必须实现幂等性处理。

建议实现方式

  1. 使用 Event ID:将 X-StablePay-Event-ID 或请求体中的 id 字段作为唯一键
  2. 记录已处理事件:在数据库中记录已处理的事件 ID
  3. 检查重复:处理前检查事件是否已处理过
  4. 定期清理:建议保留 7-30 天的处理记录后清理

Due to network issues or retry mechanisms, merchants may receive duplicate Webhook notifications. Merchants must implement idempotency handling.

Recommended Implementation

  1. Use Event ID: Use X-StablePay-Event-ID or the id field in request body as unique key
  2. Record processed events: Store processed event IDs in database
  3. Check for duplicates: Check if event has been processed before processing
  4. Regular cleanup: Recommend retaining processing records for 7-30 days before cleanup

幂等性示例(SQL)

-- 创建事件记录表 / Create event record table
CREATE TABLE webhook_events (
    event_id VARCHAR(64) PRIMARY KEY,
    event_type VARCHAR(50) NOT NULL,
    processed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_processed_at (processed_at)
);

-- 处理事件前检查 / Check before processing event
INSERT INTO webhook_events (event_id, event_type)
VALUES ('evt_xxx', 'payment.completed')
ON DUPLICATE KEY UPDATE event_id = event_id;

-- 如果 affected_rows = 1,表示首次处理
-- If affected_rows = 1, it's first-time processing
-- 如果 affected_rows = 0,表示已处理过,跳过
-- If affected_rows = 0, already processed, skip

安全建议

  1. 始终验证签名:不要跳过签名验证步骤
  2. 使用 HTTPS:Webhook URL 必须使用 HTTPS
  3. 验证时间戳:拒绝时间戳偏差过大的请求(建议 5 分钟)
  4. 保护 Secret Key:妥善保管密钥,不要泄露到客户端或日志
  5. 使用安全比较:使用时间安全的比较函数防止时序攻击
  6. 快速响应:在 30 秒内完成处理并返回响应
  7. 异步处理:对于耗时操作,先返回成功响应,后台异步处理
  1. Always verify signature: Do not skip signature verification step
  2. Use HTTPS: Webhook URL must use HTTPS
  3. Validate timestamp: Reject requests with excessive timestamp deviation (recommend 5 minutes)
  4. Protect Secret Key: Safeguard your key, do not expose to client or logs
  5. Use secure comparison: Use timing-safe comparison functions to prevent timing attacks
  6. Respond quickly: Complete processing and return response within 30 seconds
  7. Async processing: For time-consuming operations, return success response first, process asynchronously in background

完整请求示例

POST /webhook/stablepay HTTP/1.1
Host: your-server.com
Content-Type: application/json
X-StablePay-Signature: 2bb80d537b1da3e38bd30361aa855686bde0eacd7162fef6a25fe97bf527a25b
X-StablePay-Timestamp: 1765786800
X-StablePay-Nonce: 550e8400-e29b-41d4-a716-446655440000
X-StablePay-Event-Type: payment.completed
X-StablePay-Event-ID: rec_abc123def456
User-Agent: StablePay-Webhook/1.0
Content-Length: 285

{
    "id": "evt_1765786800547928039",
    "type": "payment.completed",
    "created_at": 1765786800,
    "data": {
        "object": {
            "amount": "100.00",
            "status": "completed",
            "currency": "USDT",
            "exchange_rate": "7.25000000",
            "order_id": "ORDER-20250101-001",
            "session_id": "sess_abc123def456",
            "source": "api"
        }
    }
}
最后更新 / Last Updated: 2025/12/16 08:50
Prev
退款服务 / Refund Service