Python — 託管支付
請替換示例憑證
運行前請替換所有佔位符:
"your-api-key"→ 商戶後台的實際 API Key"your-api-secret"→ 商戶後台的實際 API Secret"1000"→ 您的實際商戶號
依賴
bash
pip install requests簽名函數
所有託管支付請求需要 X-Api-Key、X-Timestamp 和 X-Signature。
- POST 請求:
Base64(HMAC-SHA256(timestamp\nPOST\npath\nBase64(SHA256(body)), apiSecret)) - GET 請求(無查詢字符串):
Base64(HMAC-SHA256(timestamp\nGET\npath, apiSecret)) - GET 請求(有查詢字符串):
Base64(HMAC-SHA256(timestamp\nGET\npath\nBase64(SHA256(queryString)), apiSecret))
python
import base64
import hashlib
import hmac
import time
import json
import requests
BASE_URL = "https://api.paystablecoin.global"
MERCHANT_ID = "1000"
API_KEY = "your-api-key"
API_SECRET = "your-api-secret"
def _hmac_base64(data: str) -> str:
sig = hmac.new(API_SECRET.encode(), data.encode(), hashlib.sha256).digest()
return base64.b64encode(sig).decode()
def sign_post(timestamp: str, path: str, body: str) -> str:
"""為 POST 請求生成簽名。"""
body_hash = base64.b64encode(hashlib.sha256(body.encode()).digest()).decode()
return _hmac_base64(f"{timestamp}\nPOST\n{path}\n{body_hash}")
def sign_get(timestamp: str, path: str) -> str:
"""為無查詢字符串的 GET 請求生成簽名。"""
return _hmac_base64(f"{timestamp}\nGET\n{path}")
def sign_get_with_query(timestamp: str, path: str, query_string: str) -> str:
"""為帶查詢字符串的 GET 請求生成簽名(如退款查詢)。"""
qs_hash = base64.b64encode(hashlib.sha256(query_string.encode()).digest()).decode()
return _hmac_base64(f"{timestamp}\nGET\n{path}\n{qs_hash}")
def verify_webhook(timestamp: str, signature: str, raw_body: str) -> bool:
"""驗證 Webhook 簽名。"""
expected = _hmac_base64(f"{timestamp}\n{raw_body}")
return expected == signature創建支付訂單
python
def create_order(amount: str = "99.99", currency: str = "USD") -> dict:
timestamp = str(int(time.time() * 1000))
path = f"/api/v1/merchants/{MERCHANT_ID}/orders"
# 請求體必須是緊湊格式(無多餘空白)
body = json.dumps({
"merchantOrderId": f"ORDER_{timestamp}",
"orderAmount": {"value": amount, "currency": currency},
"expiresAt": "2026-12-31T23:59:59Z",
"returnUrl": "https://yoursite.com/payment/success",
"callbackUrl": "https://yoursite.com/webhook/payment"
}, separators=(",", ":"))
headers = {
"X-Api-Key": API_KEY,
"X-Timestamp": timestamp,
"X-Signature": sign_post(timestamp, path, body),
"Content-Type": "application/json"
}
response = requests.post(BASE_URL + path, data=body, headers=headers)
return response.json()
if __name__ == "__main__":
result = create_order()
print(json.dumps(result, indent=2))
# 成功時:result["code"] == "00000"
# 將客戶重定向至 result["data"]["checkoutUrl"]查詢訂單
python
def query_order(merchant_order_id: str) -> dict:
timestamp = str(int(time.time() * 1000))
path = f"/api/v1/merchants/{MERCHANT_ID}/orders/{merchant_order_id}"
headers = {
"X-Api-Key": API_KEY,
"X-Timestamp": timestamp,
"X-Signature": sign_get(timestamp, path)
}
response = requests.get(BASE_URL + path, headers=headers)
return response.json()
if __name__ == "__main__":
result = query_order("ORDER_1738112345000")
print(json.dumps(result, indent=2))
# 查看 result["data"]["status"]:
# INIT / PROCESSING / SUCCEEDED / FAILED / CLOSED驗證 Webhook 簽名
python
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.route("/webhook/payment", methods=["POST"])
def payment_webhook():
timestamp = request.headers.get("X-Timestamp", "")
signature = request.headers.get("X-Signature", "")
raw_body = request.get_data(as_text=True)
# 第一步:處理業務邏輯前先驗證簽名
if not verify_webhook(timestamp, signature, raw_body):
return jsonify({"error": "Invalid signature"}), 401
# 第二步:驗證時間戳在 5 分鐘內
if abs(int(time.time() * 1000) - int(timestamp)) > 300_000:
return jsonify({"error": "Timestamp expired"}), 401
# 第三步:立即返回 HTTP 200
# 異步處理業務邏輯
event = request.get_json(force=True)
# 按 status 分發處理:
# PROCESSING / SUCCEEDED
# 使用 acquiringOrderId 或 merchantOrderId 進行去重
return "", 200創建退款
python
def create_refund(acquiring_order_id: str, merchant_refund_order_id: str) -> dict:
timestamp = str(int(time.time() * 1000))
path = f"/api/v1/merchants/{MERCHANT_ID}/refunds"
# 請求體必須是緊湊格式(無多餘空白)
body = json.dumps({
"merchantRefundOrderId": merchant_refund_order_id,
"acquiringOrderId": acquiring_order_id,
"refundReason": "Customer request full refund"
}, separators=(",", ":"))
headers = {
"X-Api-Key": API_KEY,
"X-Timestamp": timestamp,
"X-Signature": sign_post(timestamp, path, body),
"Content-Type": "application/json"
}
response = requests.post(BASE_URL + path, data=body, headers=headers)
return response.json()
if __name__ == "__main__":
result = create_refund(
acquiring_order_id="ORDER_1738112345000",
merchant_refund_order_id=f"REFUND_{int(time.time() * 1000)}"
)
print(json.dumps(result, indent=2))
# 成功時:result["code"] == "00000"
# result["data"]["status"] 為 "PROCESSING"查詢退款
python
def query_refund(merchant_refund_order_id: str) -> dict:
timestamp = str(int(time.time() * 1000))
path = f"/api/v1/merchants/{MERCHANT_ID}/refunds"
query_string = f"merchantRefundOrderId={merchant_refund_order_id}"
headers = {
"X-Api-Key": API_KEY,
"X-Timestamp": timestamp,
"X-Signature": sign_get_with_query(timestamp, path, query_string)
}
response = requests.get(f"{BASE_URL}{path}?{query_string}", headers=headers)
return response.json()
if __name__ == "__main__":
result = query_refund("REFUND_1738112345000")
print(json.dumps(result, indent=2))
# 查看 result["data"]["status"]:
# PROCESSING / SUCCEEDED / FAILED / CLOSED