Python — Hosted Payments
REPLACE EXAMPLE CREDENTIALS
Replace all placeholder values before running:
"your-api-key"→ Your actual API Key from the merchant portal"your-api-secret"→ Your actual API Secret from the merchant portal"1000"→ Your actual Merchant ID
Dependencies
bash
pip install requestsSignature Helper
All Hosted Payments requests require X-Api-Key, X-Timestamp, and X-Signature.
- POST requests:
Base64(HMAC-SHA256(timestamp\nPOST\npath\nBase64(SHA256(body)), apiSecret)) - GET requests (no query string):
Base64(HMAC-SHA256(timestamp\nGET\npath, apiSecret)) - GET requests (with query string):
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:
"""Sign a POST request."""
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:
"""Sign a GET request with no query string."""
return _hmac_base64(f"{timestamp}\nGET\n{path}")
def sign_get_with_query(timestamp: str, path: str, query_string: str) -> str:
"""Sign a GET request that has a query string (e.g. refund query)."""
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:
"""Verify the signature of an incoming webhook."""
expected = _hmac_base64(f"{timestamp}\n{raw_body}")
return expected == signatureCreate Payment Order
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 must be minified (no extra whitespace)
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))
# On success: result["code"] == "00000"
# Redirect the customer to result["data"]["checkoutUrl"]Query Order
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))
# Check result["data"]["status"]:
# INIT / PROCESSING / SUCCEEDED / FAILED / CLOSEDVerify Incoming 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)
# Step 1: Verify signature before processing
if not verify_webhook(timestamp, signature, raw_body):
return jsonify({"error": "Invalid signature"}), 401
# Step 2: Verify timestamp is within 5 minutes
if abs(int(time.time() * 1000) - int(timestamp)) > 300_000:
return jsonify({"error": "Timestamp expired"}), 401
# Step 3: Return HTTP 200 immediately
# Process business logic asynchronously after responding
event = request.get_json(force=True)
# Dispatch to background task using status:
# PROCESSING, SUCCEEDED
# Use acquiringOrderId or merchantOrderId for deduplication
return "", 200Create Refund
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))
# On success: result["code"] == "00000"
# result["data"]["status"] will be "PROCESSING"Query Refund
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))
# Check result["data"]["status"]:
# PROCESSING / SUCCEEDED / FAILED / CLOSEDRelated Documentation
- Hosted Payments Integration Guide — Step-by-step integration walkthrough
- API Reference — Full endpoint and field specification
- Signature Algorithm — Signature calculation details