Python
兼容性
Python 3.6+。仅依赖 requests,其他都是标准库。
安装依赖
bash
pip install requests客户端实现
python
import time, hmac, hashlib, secrets, json
import requests
APP_ID = "cp_a1b2c3d4e5f6071829304a5b6c7d8e9f"
SECRET = "f1e2d3c4b5a69788776655443322110099aabbccddeeff00112233445566778899"
BASE = "https://api.coinepay.net"
def call(method: str, path: str, body_dict: dict | None = None,
idempotency_key: str | None = None) -> dict:
"""统一调用 OpenAPI"""
body = json.dumps(body_dict or {}, separators=(",", ":")).encode("utf-8")
body_hash = hashlib.sha256(body).hexdigest()
ts = str(int(time.time()))
nonce = secrets.token_hex(16)
sign_input = f"{method}\n{path}\n\n{ts}\n{nonce}\n{body_hash}"
sig = hmac.new(SECRET.encode(), sign_input.encode(), hashlib.sha256).hexdigest()
headers = {
"X-App-Id": APP_ID,
"X-Timestamp": ts,
"X-Nonce": nonce,
"X-Signature": sig,
"Content-Type": "application/json",
}
if idempotency_key:
headers["Idempotency-Key"] = idempotency_key
return requests.request(method, BASE + path, headers=headers, data=body, timeout=30).json()
# Webhook 验签
def verify_webhook(signature_header: str, timestamp_header: str,
raw_body: bytes, webhook_secret: str) -> bool:
if not signature_header.startswith("v1,"):
return False
sig = signature_header[3:]
expected = hmac.new(
webhook_secret.encode(),
f"{timestamp_header}.".encode() + raw_body,
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(sig, expected)使用示例
python
import uuid
# 1. 列卡头
resp = call("POST", "/api/v1/openapi/card_headers/list", {"page": 1, "page_size": 20})
print(resp["data"]["list"][0]["header_id"])
# 2. 列卡配置
resp = call("POST", "/api/v1/openapi/card_configs/list", {"header_id": "hdr_5"})
package_id = resp["data"]["list"][0]["package_id"]
# 3. 申请虚拟卡(带幂等键)
resp = call("POST", "/api/v1/openapi/card/apply",
{"header_id": "hdr_5", "package_id": package_id,
"first_name": "John", "last_name": "Doe"},
idempotency_key=str(uuid.uuid4()))
card_id = resp["data"]["card_id"]
print(f"Created card: {card_id}, status={resp['data']['status']}")
# 4. 创建充值
resp = call("POST", "/api/v1/openapi/card/recharge",
{"card_id": card_id, "amount": "100.00"},
idempotency_key=str(uuid.uuid4()))
print(f"Transaction: {resp['data']['transaction_id']}")Webhook 接收(FastAPI)
python
from fastapi import FastAPI, Request, HTTPException
import os
app = FastAPI()
WEBHOOK_SECRET = os.environ["COINEPAY_WEBHOOK_SECRET"]
@app.post("/webhook")
async def webhook(request: Request):
raw = await request.body()
sig = request.headers.get("Webhook-Signature", "")
ts = request.headers.get("Webhook-Timestamp", "")
event_id = request.headers.get("Webhook-Id", "")
event_type = request.headers.get("Webhook-Type", "")
if not verify_webhook(sig, ts, raw, WEBHOOK_SECRET):
raise HTTPException(401, "invalid signature")
# 时间戳 ±5 分钟内
if abs(int(time.time()) - int(ts)) > 300:
raise HTTPException(401, "timestamp expired")
# 基于 event_id 去重
if already_processed(event_id):
return {"ok": True}
payload = json.loads(raw)
handle_event(event_type, payload)
mark_processed(event_id)
return {"ok": True}重活丢队列
handle_event 应只做"持久化 + 入队"。不要在 webhook handler 里做耗时操作(数据库慢查询、外部调用),5 秒不返回 2xx 就会重投。
错误处理与重试
python
import time
class coinepayError(Exception): ...
class RateLimited(coinepayError): ...
class IdempotencyConflict(coinepayError): ...
def call_with_retry(method, path, body=None, idempotency_key=None,
max_attempts=4):
for attempt in range(max_attempts):
resp = call(method, path, body, idempotency_key)
code = resp.get("code", 500)
if code == 200:
return resp["data"]
if code == 429 and attempt < max_attempts - 1:
time.sleep(min(2 ** attempt, 30))
continue
if code >= 500 and attempt < max_attempts - 1:
time.sleep(2 ** attempt)
continue
if code == 409:
raise IdempotencyConflict(resp.get("message"))
raise coinepayError(f"{code}: {resp.get('message')}")
raise coinepayError("max attempts exceeded")