Skip to content

Python — API Checkout

REPLACE EXAMPLE CREDENTIALS

Replace all placeholder values before running:

  • "your-api-secret" → Your actual API Secret from the merchant portal
  • "MCH_20240101_ABC123" → Your actual Merchant ID

API Checkout does not use X-Api-Key. Only X-Timestamp and X-Signature are required.

PERMISSION REQUIRED

API Checkout must be explicitly enabled for your account. Contact support to activate it.

Dependencies

bash
pip install requests flask

Signature Helper

API Checkout uses the same signing algorithm as Hosted Payments but without X-Api-Key.

python
import base64
import hashlib
import hmac
import json
import time
import requests

BASE_URL    = "https://api.paystablecoin.global"
MERCHANT_ID = "MCH_20240101_ABC123"
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 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

Step 1: Fetch Available Payment Methods

Call this at runtime before rendering your payment UI. Never hardcode currency/network combinations.

python
def get_payment_methods() -> dict:
    timestamp = str(int(time.time() * 1000))
    path = f"/api/v1/merchants/{MERCHANT_ID}/checkout/payment-methods"

    headers = {
        "X-Timestamp": timestamp,
        "X-Signature": sign_get(timestamp, path)
    }

    response = requests.get(BASE_URL + path, headers=headers)
    return response.json()


if __name__ == "__main__":
    methods = get_payment_methods()
    print(json.dumps(methods, indent=2))
    # Each method includes:
    #   cryptoCurrency, network, networkDisplayName,
    #   minAmount, maxAmount, estimatedConfirmationTimeSec, displayOrder
    # Sort by displayOrder (ascending) and render options to the user.

Step 2: Create a Checkout Order

After the customer selects a currency and network, create the order immediately.

python
def create_checkout_order(network: str, currency: str, amount: str) -> dict:
    timestamp = str(int(time.time() * 1000))
    path = f"/api/v1/merchants/{MERCHANT_ID}/checkout/orders"

    # orderAmount.currency must be a stablecoin (USDC, USDT, or USD1)
    # network must match the customer's selected payment method
    body = json.dumps({
        "merchantOrderId": f"ORDER_{timestamp}",
        "orderAmount": {"value": amount, "currency": currency},
        "paymentMethodType": "ON_CHAIN_TRANSFER",
        "network": network,
        "expiresAt": "2026-12-31T23:59:59Z",
        "callbackUrl": "https://yoursite.com/webhook/checkout"
    }, separators=(",", ":"))

    headers = {
        "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_checkout_order(network="tron", currency="USDC", amount="100.50")
    print(json.dumps(result, indent=2))
    # Display to the customer:
    #   result["data"]["depositAddress"]              — text + QR code
    #   result["data"]["cryptoPaymentAmount"]["value"] — exact amount to send
    #   result["data"]["networkDisplayName"]           — PROMINENT WARNING
    #   result["data"]["expiresAt"]                   — countdown timer
    #
    # IMPORTANT: Show cryptoPaymentAmount, NOT orderAmount

Step 3: Query Checkout Order Status

Use as a fallback if the webhook is not received.

python
def query_checkout_order(merchant_order_id: str) -> dict:
    timestamp = str(int(time.time() * 1000))
    path = f"/api/v1/merchants/{MERCHANT_ID}/checkout/orders/{merchant_order_id}"

    headers = {
        "X-Timestamp": timestamp,
        "X-Signature": sign_get(timestamp, path)
    }

    response = requests.get(BASE_URL + path, headers=headers)
    return response.json()


if __name__ == "__main__":
    # Poll every 5–10 seconds until status is terminal
    result = query_checkout_order("ORDER_1738112345000")
    status = result.get("data", {}).get("status")
    print(f"Status: {status}")
    # Terminal states: SUCCEEDED, FAILED, CLOSED

Step 4: Verify Incoming Webhook

API Checkout webhooks require a strict response: HTTP 200 with {"code":"00000"} within 5 seconds.

python
from flask import Flask, request, jsonify

app = Flask(__name__)


@app.route("/webhook/checkout", methods=["POST"])
def checkout_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
    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 + {"code":"00000"} within 5 seconds (REQUIRED)
    # Process business logic asynchronously after responding

    event = request.get_json(force=True)
    acquiring_order_id = event.get("acquiringOrderId")
    merchant_order_id  = event.get("merchantOrderId")
    # Use either ID for deduplication — the same event may arrive more than once.

    return jsonify({"code": "00000"})

Released under the MIT License.