MercanPay Blog

Python Crypto Payment Integration with Flask and Django

A Python crypto payment integration with pip install mercanpay: create USDT/TRX invoices, verify webhooks and fulfil orders in Flask and Django apps.

Published: September 30, 20265 min readTürkçe oku
Python Crypto Payment Integration with Flask and Django

A Python crypto payment integration takes just a few lines with the official mercanpay package. You create an invoice at checkout, redirect the customer to the hosted payment page, and confirm the order from a signed webhook. This guide connects USDT and TRX (TRC20) payments to Flask and Django apps step by step.

Requirements

  • Python 3 and pip
  • an approved MercanPay merchant account (apply here)
  • an API key with the invoices scope, created in the panel
  • your webhook signing secret (whsec_...) from the Settings page

1. Install the package

pip install mercanpay

The package depends only on requests. Keep your secrets in environment variables:

export MERCANPAY_API_KEY="mp_..."
export MERCANPAY_WEBHOOK_SECRET="whsec_..."

2. Create the client

import os
from mercanpay import MercanPay, MercanPayError, verify_webhook

zp = MercanPay("https://mercanpay.com", os.environ["MERCANPAY_API_KEY"])

The client uses a 15-second timeout by default. You can change it with MercanPay(base_url, api_key, timeout=10).

3. Create a crypto payment invoice in Python

create_invoice supports two styles:

# USD-priced: the customer picks TRX or USDT on the payment page, and the rate is locked
inv = zp.create_invoice(
    price_usd="49.90",
    order_id="order-1001",
    description="Pro licence (1 year)",
    success_url="https://myshop.com/order/1001/thanks",
    return_url="https://myshop.com/cart",
    metadata={"customer_id": 42},
)

# Fixed coin: exactly 49.90 USDT
inv = zp.create_invoice("USDT", "49.90", order_id="order-1002")

print(inv["status"], inv["address"], inv["payment_url"])

Limit the coin choice with tokens=["USDT"], and set the lifetime with expires_in_minutes (5–1440).

Rules worth remembering:

  • Amounts are strings, such as "49.90". Never floats.
  • order_id makes retries safe. A second call with the same value returns the existing invoice.
  • The minimum invoice is $10. USDT invoices include a 1 USDT network fee paid by the customer.
  • Invoices stay open for at most 24 hours.

4. End-to-end example with Flask

import json
import os
from flask import Flask, abort, redirect, request
from mercanpay import MercanPay, MercanPayError, verify_webhook

app = Flask(__name__)
zp = MercanPay("https://mercanpay.com", os.environ["MERCANPAY_API_KEY"])
SECRET = os.environ["MERCANPAY_WEBHOOK_SECRET"]


@app.post("/checkout/<order_id>")
def checkout(order_id):
    order = load_order(order_id)  # your own database lookup
    try:
        inv = zp.create_invoice(
            price_usd=order.total_usd,          # e.g. "49.90"
            order_id=order.id,
            callback_url="https://myshop.com/mercanpay/webhook",
            success_url=f"https://myshop.com/order/{order.id}/thanks",
        )
    except MercanPayError as e:
        app.logger.error("MercanPay %s %s", e.code, e.message)
        abort(502)
    return redirect(inv["payment_url"])


@app.post("/mercanpay/webhook")
def webhook():
    raw = request.get_data()  # raw body: the signature covers these exact bytes
    header = request.headers.get("X-MercanPay-Signature", "")
    if not verify_webhook(SECRET, raw, header):
        abort(401)

    event = json.loads(raw)
    invoice = event["data"].get("invoice") or {}

    if event["type"] in ("invoice.paid", "invoice.overpaid"):
        fulfil_once(event["id"], invoice["order_id"], invoice["token"], invoice["paid_amount"])
    elif event["type"] == "invoice.partial":
        notify_customer_remaining(invoice["order_id"])
    elif event["type"] in ("invoice.expired", "invoice.cancelled"):
        release_stock(invoice["order_id"])

    return "", 200

verify_webhook parses the t=<unix>,v1=<hex> value of the X-MercanPay-Signature header. It rejects timestamps older than 5 minutes and compares signatures in constant time with hmac.compare_digest. The details are in How to verify webhook signatures.

The signature is computed over the raw body. Don't verify against data you parsed with request.get_json() and serialised again, because the bytes may differ.

Watch out for duplicates

If your server doesn't return a 2xx within 10 seconds, the webhook is retried with growing delays, so the same event can arrive more than once. Your fulfil_once function should store the event id (event["id"]) in a unique column and skip events it has already seen. Hand heavy work such as emails or PDF invoices to a job queue and return 200 quickly.

5. Webhook in Django

import json
import os
from django.http import HttpResponse
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST
from mercanpay import verify_webhook


@csrf_exempt
@require_POST
def mercanpay_webhook(request):
    header = request.headers.get("X-MercanPay-Signature", "")
    if not verify_webhook(os.environ["MERCANPAY_WEBHOOK_SECRET"], request.body, header):
        return HttpResponse(status=401)

    event = json.loads(request.body)
    if event["type"] in ("invoice.paid", "invoice.overpaid"):
        fulfil_once(event["id"], event["data"]["invoice"]["order_id"])
    return HttpResponse(status=200)

Webhook requests don't come from a browser, so you need to exempt this view from CSRF checks. The signature check is what protects the endpoint.

6. Querying and reconciliation

Besides webhooks, you can read invoice status from the API. This helps on a thank-you page or in a nightly reconciliation job:

inv = zp.get_invoice_by_order("order-1001")
if inv["status"] in ("paid", "overpaid"):
    ...

page = zp.list_invoices(status="paid", limit=20)
for inv in page["data"]:
    reconcile(inv)
next_page = zp.list_invoices(status="paid", limit=20, before=page["next_before"])

zp.rates() returns current USD prices as a dictionary like {"TRX": "0.3342", "USDT": "1"}.

USD pricing or a fixed coin?

The first decision in any integration is how to price your invoices:

price_usd (USD-priced) token + amount (fixed coin)
Customer experience Picks TRX or USDT on the payment page Pays in one coin
Exchange rate Locked when the customer picks a coin No conversion; the amount is fixed
Best for Stores with dollar prices Stores that already price in USDT
Late payment TRX on a USD-priced invoice is re-priced at the current rate The invoiced amount applies

For a dollar-priced store, price_usd is usually the lowest-friction option: customers pay with whichever coin they hold, and you never touch exchange-rate maths. We cover rate risk and the rate lock in detail in Crypto payment exchange rate risk.

Go-live checklist

  • The API key lives only on the server, in an environment variable. It never enters source control or front-end code.
  • The key has only the scopes it needs. A web app that creates invoices doesn't need withdrawals.
  • The webhook URL uses HTTPS, and verify_webhook runs on every request.
  • Every order has a unique order_id, so retries never create duplicate invoices.
  • invoice.partial, invoice.overpaid and invoice.expired are handled.
  • Orders are fulfilled from the webhook, not the success_url redirect.
  • The whole flow has been tested end to end with a small real payment.

For more on protecting keys and withdrawal addresses, read Crypto merchant account security.

Error handling

Any non-2xx response raises MercanPayError:

Attribute Meaning
e.status The HTTP status
e.code A machine-readable code, e.g. validation_error, unauthorized, insufficient_funds
e.message A human-readable description

On 429 and 5xx, wait briefly and retry the same call. The default rate limit is 120 requests per minute per IP.

FAQ

Can I use it in an asyncio app?

The client uses synchronous requests. In async apps, run calls in a thread pool, or call the REST API directly with your own async HTTP client.

Can I send payouts?

Yes. With a key that has the withdrawals scope, call zp.create_withdrawal("USDT", "50", "T...", idempotency_key="payout-001"). Retrying with the same idempotency_key never creates a second withdrawal. API withdrawals can only go to addresses marked as trusted in the panel.

How should I test?

The "Send test" button on the Webhooks page sends you a test.ping event. Before going live, test the whole flow end to end with a small real payment.

Get started with MercanPay

A Python crypto payment integration starts with a single pip install. On PHP instead? See the PHP integration guide. Create your merchant account and take your first USDT payment today.

#Python#Flask#Django#API#Integration

Related posts

Start accepting crypto payments in minutes

Accept USDT and TRX (TRC20). Fees from 0.4%, a hosted payment page, API and webhooks.