Install

Python

Charge per request in Flask (or any Python framework) with four HTTP calls to Penni. No SDK needed.

Terminal
pip install flask requests

There is no Python package to install: the integration is four HTTP calls, shown below as a Flask hook in about 60 lines. Set PENNI_SITE_ID and PENNI_API_KEY in the environment and every route on the app is priced by the dashboard.

app.py
import base64, json, os

import requests
from flask import Flask, Response, g, request

API = "https://api.pennipay.eu/v1"
SITE_ID = os.environ["PENNI_SITE_ID"]
AUTH = {"Authorization": f"Bearer {os.environ['PENNI_API_KEY']}"}
FIELDS = ("scheme", "network", "asset", "payTo", "amount")

app = Flask(__name__)


def b64(obj):
    return base64.b64encode(json.dumps(obj).encode()).decode()


def payment_required(required, error):
    body = {**required, "error": error}
    return Response(json.dumps(body), 402, {"PAYMENT-REQUIRED": b64(body), "Content-Type": "application/json"})


@app.before_request
def penni():
    # 1. Price of this path (cache it for up to 60 seconds in production)
    config = requests.get(f"{API}/sites/{SITE_ID}/config", params={"path": request.path}, headers=AUTH, timeout=10).json()
    if config.get("blocked"):
        return Response('{"error": "Payments are temporarily unavailable"}', 503, {"Retry-After": "300"})
    if config["match"]["free"] or not config["accepts"]:
        return None  # free path, or nothing to pay with: serve as usual

    # 2. No payment yet: answer 402 with the price
    required = {"x402Version": 2, "resource": {"url": request.url}, "accepts": config["accepts"]}
    header = request.headers.get("PAYMENT-SIGNATURE")
    if not header:
        return payment_required(required, "PAYMENT-SIGNATURE header is required")

    # 3. The payment must match one of your current prices exactly
    payment = json.loads(base64.b64decode(header))
    accepted = payment.get("accepted") or {}
    chosen = next((a for a in config["accepts"] if all(a.get(k) == accepted.get(k) for k in FIELDS)), None)
    if chosen is None:
        return payment_required(required, "Payment does not match the current price")

    # 4. Verify, then settle on-chain
    body = {"x402Version": 2, "paymentPayload": payment, "paymentRequirements": chosen}
    verified = requests.post(f"{API}/x402/verify", json=body, headers=AUTH, timeout=10).json()
    if not verified.get("isValid"):
        return payment_required(required, verified.get("invalidReason", "invalid_payment"))
    for _ in range(2):  # Cardano may answer settlement_pending once: send the same body again
        settled = requests.post(f"{API}/x402/settle", json=body, headers=AUTH, timeout=90).json()
        if settled.get("errorReason") != "settlement_pending":
            break
    if not settled.get("success"):
        return payment_required(required, settled.get("errorReason", "settle_failed"))
    g.payment_response = b64(settled)


@app.after_request
def receipt(response):
    if "payment_response" in g:
        response.headers["PAYMENT-RESPONSE"] = g.payment_response
    return response


@app.get("/premium/report")
def report():
    return {"report": "..."}

What it does

  1. Asks Penni for the price of the request path. Free paths, and sites that can't take payments, are served as usual.
  2. Without a payment it answers 402 with the price in the PAYMENT-REQUIRED header.
  3. With a payment in PAYMENT-SIGNATURE it checks that the payment matches one of your current prices exactly. Never trust the amount the client sends.
  4. It verifies and settles the payment through Penni and adds the receipt as PAYMENT-RESPONSE.

For production

  • Cache the config per path for up to 60 seconds; the config API supports ETags.
  • Behind a proxy, build the resource URL from your public host instead of request.url.
  • FastAPI or Django: the same four steps fit in a middleware; use httpx for async code.

Every call and field is in the HTTP walkthrough and the API reference.