Skip to content

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 requests

Signature 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 == signature

Create 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 / CLOSED

Verify 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 "", 200

Create 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 / CLOSED

Released under the MIT License.