Skip to content

Python — 託管支付

請替換示例憑證

運行前請替換所有佔位符:

  • "your-api-key" → 商戶後台的實際 API Key
  • "your-api-secret" → 商戶後台的實際 API Secret
  • "1000" → 您的實際商戶號

依賴

bash
pip install requests

簽名函數

所有託管支付請求需要 X-Api-KeyX-TimestampX-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

相關文檔

Released under the MIT License.