Pagamenti con Stripe in Python

Stripe è una piattaforma di pagamento che consente di accettare carte, wallet e metodi locali con un’API coerente. In questa guida vedrai un’implementazione pratica in Python, con esempi pronti per ambienti di sviluppo e produzione: Checkout (il modo più rapido e sicuro), PaymentIntent (massimo controllo), webhook (per conferme affidabili), gestione di errori e idempotenza, consigli di sicurezza e test.

Prerequisiti

  • Python 3.10+ (consigliato)
  • Un account Stripe e le chiavi API (test e live)
  • Un backend web (qui useremo Flask per semplicità, ma i concetti valgono per Django/FastAPI)

Modelli di integrazione: cosa scegliere

In Stripe hai due approcci principali lato backend. La scelta dipende da quanto controllo ti serve sul flusso e da quanto vuoi delegare a Stripe.

Approccio Quando usarlo Pro Contro
Stripe Checkout E-commerce, pagamenti one-shot, abbonamenti standard UI pronta, SCA/3DS gestiti, meno codice e rischi Meno personalizzazione del checkout
PaymentIntent + Stripe.js Esperienze custom (marketplace, UI proprietaria, flussi complessi) Massimo controllo e personalizzazione Più responsabilità (SCA, UI, gestione stati)

In entrambi i casi, la regola d’oro è la stessa: la conferma definitiva del pagamento deve arrivare dal webhook, non solo dal browser. La UI può fallire o essere manipolata; i webhook sono la fonte affidabile degli eventi.

Installazione e configurazione

Installa la libreria ufficiale Stripe per Python:

python -m pip install --upgrade stripe flask python-dotenv

Per sicurezza, tieni le chiavi in variabili d’ambiente. In locale puoi usare un file .env (da non committare) con questi valori:

STRIPE_SECRET_KEY=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
STRIPE_PUBLISHABLE_KEY=pk_test_...
BASE_URL=http://localhost:4242

STRIPE_SECRET_KEY è la chiave segreta (server-side) e non deve mai finire nel frontend. STRIPE_PUBLISHABLE_KEY è la chiave pubblicabile (client-side). STRIPE_WEBHOOK_SECRET serve per verificare la firma dei webhook.

Progetto di esempio con Flask

Struttura minima: un endpoint che crea una sessione di Checkout (o un PaymentIntent), un endpoint di webhook, e pagine di successo/annullamento.

from __future__ import annotations

import os
from typing import Any

from flask import Flask, jsonify, redirect, request
from dotenv import load_dotenv
import stripe

load_dotenv()

app = Flask(__name__)

stripe.api_key = os.environ["STRIPE_SECRET_KEY"]
BASE_URL = os.environ.get("BASE_URL", "http://localhost:4242")

# Facoltativo ma consigliato: specifica una versione API fissa sul backend,
# così eviti cambiamenti imprevisti se Stripe aggiorna la versione di default del tuo account.
# stripe.api_version = "2024-04-10"  # esempio

Metodo 1: Pagamenti rapidi con Stripe Checkout

Creare una Checkout Session

Checkout Session prepara una pagina di pagamento ospitata da Stripe. Tu specifichi cosa vendi, importi e valuta, e Stripe si occupa di UI, metodi di pagamento e autenticazione forte (SCA/3D Secure) quando necessario.

@app.post("/create-checkout-session")
def create_checkout_session():
    # In uno scenario reale, i prezzi dovrebbero provenire dal tuo backend
    # o da Stripe Price (oggetti prezzo configurati nel dashboard).
    try:
        session = stripe.checkout.Session.create(
            mode="payment",
            success_url=f"{BASE_URL}/success?session_id={{CHECKOUT_SESSION_ID}}",
            cancel_url=f"{BASE_URL}/cancel",
            line_items=[
                {
                    "quantity": 1,
                    "price_data": {
                        "currency": "eur",
                        "unit_amount": 1990,  # 19,90 EUR in centesimi
                        "product_data": {
                            "name": "Corso Python - accesso 30 giorni",
                        },
                    },
                }
            ],
            # Metadati utili per correlare pagamenti e oggetti del tuo DB
            metadata={
                "order_id": "ORD_12345",
                "user_id": "USR_999",
            },
        )
        return jsonify({"url": session.url})
    except stripe.error.StripeError as e:
        # StripeError include dettagli utili (status code, message).
        # Non esporre dettagli sensibili al client.
        return jsonify({"error": str(e)}), 400

Dal frontend, chiami questo endpoint e reindirizzi l’utente alla session.url. Anche senza frontend, puoi provare con curl e aprire l’URL nel browser:

curl -X POST http://localhost:4242/create-checkout-session

Confermare il pagamento in modo affidabile

La pagina /success indica che l’utente è tornato dal flusso di pagamento, ma non garantisce che il denaro sia stato effettivamente catturato. Devi affidarti ai webhook: checkout.session.completed e/o eventi legati al PaymentIntent sottostante.

Webhook: la parte più importante

I webhook sono chiamate HTTP inviate da Stripe al tuo server quando accade qualcosa (pagamento riuscito, fallito, rimborso, contestazione, ecc.). In produzione sono indispensabili per:

  • confermare pagamenti e aggiornare ordini
  • inviare email o abilitare accessi a contenuti
  • gestire rimborsi e chargeback
  • riconciliare stati anche se il client non ritorna (chiusura browser, rete instabile)

Endpoint webhook con verifica firma

Verificare la firma impedisce a terzi di inviare eventi falsi. Stripe firma ogni webhook con un segreto dedicato al tuo endpoint.

@app.post("/webhook")
def webhook():
    payload = request.get_data(as_text=True)
    sig_header = request.headers.get("Stripe-Signature", "")
    webhook_secret = os.environ.get("STRIPE_WEBHOOK_SECRET", "")

    try:
        event = stripe.Webhook.construct_event(
            payload=payload,
            sig_header=sig_header,
            secret=webhook_secret,
        )
    except ValueError:
        # Payload non valido
        return "Invalid payload", 400
    except stripe.error.SignatureVerificationError:
        # Firma non valida
        return "Invalid signature", 400

    # Gestione eventi principali
    event_type = event["type"]
    data_object = event["data"]["object"]

    if event_type == "checkout.session.completed":
        # La sessione è completata; spesso è il punto giusto per marcare ordine pagato.
        # Nota: per 'mode=payment' la Session contiene un payment_intent.
        session = data_object
        order_id = session.get("metadata", {}).get("order_id")
        payment_intent_id = session.get("payment_intent")
        # TODO: recupera il tuo ordine dal DB usando order_id e aggiorna stato
        # TODO: rendi idempotente (vedi sezione dedicata)
        print("Checkout completed:", order_id, payment_intent_id)

    elif event_type == "payment_intent.succeeded":
        # Pagamento effettivamente riuscito
        pi = data_object
        print("PaymentIntent succeeded:", pi["id"], pi.get("metadata", {}))

    elif event_type == "payment_intent.payment_failed":
        pi = data_object
        print("PaymentIntent failed:", pi["id"])

    # Per altri eventi: rimborso, dispute, chargeback, ecc.
    return "OK", 200

Test dei webhook in locale

Per sviluppare in locale, normalmente si usa la Stripe CLI per inoltrare webhook dal cloud al tuo computer, ottenendo anche il webhook secret corretto per la sessione di test. In alternativa, puoi testare in ambiente staging con un endpoint pubblico.

Idempotenza: evita doppi addebiti e doppi aggiornamenti

In pagamenti e webhook, i retry sono normali: una richiesta può essere ritentata dal client o da Stripe se il tuo server non risponde in tempo. Per questo:

  • usa idempotency keys quando crei oggetti “pagamento” (Session, PaymentIntent, ecc.)
  • rendi idempotente la logica webhook nel tuo database (event_id unico, stato ordine, ecc.)

Esempio di idempotency key in creazione sessione:

import uuid

@app.post("/create-checkout-session-safe")
def create_checkout_session_safe():
    try:
        idem_key = f"checkout_{uuid.uuid4()}"
        session = stripe.checkout.Session.create(
            mode="payment",
            success_url=f"{BASE_URL}/success?session_id={{CHECKOUT_SESSION_ID}}",
            cancel_url=f"{BASE_URL}/cancel",
            line_items=[
                {
                    "quantity": 1,
                    "price_data": {
                        "currency": "eur",
                        "unit_amount": 1990,
                        "product_data": {"name": "Corso Python - accesso 30 giorni"},
                    },
                }
            ],
            metadata={"order_id": "ORD_12345"},
            idempotency_key=idem_key,
        )
        return jsonify({"url": session.url, "idempotency_key": idem_key})
    except stripe.error.StripeError as e:
        return jsonify({"error": str(e)}), 400

Sul webhook, conserva event["id"] in una tabella e ignora eventi già processati. Questo evita di abilitare due volte lo stesso ordine.

Metodo 2: Flusso custom con PaymentIntent

Se vuoi un checkout completamente personalizzato, Stripe consiglia di usare l’oggetto PaymentIntent. Il backend crea un PaymentIntent e restituisce al client il client_secret, che viene confermato con Stripe.js. Il pagamento può richiedere passaggi extra (SCA) e cambiare stato.

Creare un PaymentIntent (server-side)

@app.post("/create-payment-intent")
def create_payment_intent():
    try:
        payload: dict[str, Any] = request.get_json(force=True) or {}
        amount = int(payload.get("amount", 1990))  # centesimi
        currency = payload.get("currency", "eur")

        pi = stripe.PaymentIntent.create(
            amount=amount,
            currency=currency,
            automatic_payment_methods={"enabled": True},
            metadata={"order_id": payload.get("order_id", "ORD_12345")},
        )

        return jsonify({"clientSecret": pi.client_secret})
    except (ValueError, TypeError):
        return jsonify({"error": "Importo non valido"}), 400
    except stripe.error.StripeError as e:
        return jsonify({"error": str(e)}), 400

Con automatic_payment_methods, Stripe seleziona metodi compatibili. In alternativa puoi specificare manualmente. Sul frontend, userai Stripe Elements per raccogliere i dati di pagamento e chiamare stripe.confirmPayment con il client_secret.

Stati tipici del PaymentIntent

  • requires_payment_method: serve un metodo di pagamento
  • requires_confirmation: pronto per essere confermato
  • requires_action: l’utente deve completare un’autenticazione (SCA/3DS)
  • processing: in elaborazione
  • succeeded: pagamento riuscito
  • canceled: annullato

Anche qui, il webhook payment_intent.succeeded è il momento più affidabile per sbloccare un ordine.

Rimborsi

Un rimborso si fa tipicamente partendo dal payment_intent o dalla charge. Esempio:

@app.post("/refund")
def refund():
    try:
        payload = request.get_json(force=True) or {}
        payment_intent_id = payload["payment_intent_id"]

        refund = stripe.Refund.create(
            payment_intent=payment_intent_id,
            # amount=500,  # facoltativo: rimborso parziale in centesimi
            reason="requested_by_customer",
        )
        return jsonify({"refund_id": refund["id"], "status": refund["status"]})
    except KeyError:
        return jsonify({"error": "payment_intent_id mancante"}), 400
    except stripe.error.StripeError as e:
        return jsonify({"error": str(e)}), 400

Gestisci anche gli eventi webhook legati ai rimborsi (es. charge.refunded) per mantenere allineato il tuo database.

Sicurezza e buone pratiche

  • Non fidarti del client: prezzi e quantità devono essere validati dal backend.
  • Verifica i webhook con la firma e conserva l’event_id per l’idempotenza.
  • Usa HTTPS in produzione e proteggi i secret con un secret manager o variabili d’ambiente.
  • Non salvare dati carta sul tuo server: usa Stripe.js/Checkout per la raccolta sicura.
  • Log minimali: non loggare segreti, client_secret o PII inutilmente.
  • Versiona l’API: fissa una stripe.api_version o imposta la versione per richiesta per evitare regressioni.

Gestione errori: cosa aspettarsi

Stripe può restituire errori di validazione, autenticazione, rate limit, o errori temporanei. Gestisci sempre eccezioni di tipo stripe.error.StripeError e rispondi al client con messaggi generici.

def stripe_error_to_response(e: stripe.error.StripeError):
    # Esempio di normalizzazione errori per API interne
    body = {
        "type": e.__class__.__name__,
        "message": getattr(e, "user_message", None) or str(e),
    }
    status = getattr(e, "http_status", 400) or 400
    return body, status

Esecuzione dell’app di esempio

Chiudi con un run minimale:

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=4242, debug=True)

In produzione disabilita debug=True e metti Flask dietro a un server WSGI (es. Gunicorn) e un reverse proxy.

Checklist finale

  1. Chiavi e segreti solo in variabili d’ambiente
  2. Prezzi validati dal backend
  3. Checkout o PaymentIntent scelto in base al livello di personalizzazione
  4. Webhook con verifica firma e logica idempotente
  5. Test in modalità test, poi passaggio a live con verifica end-to-end

Con questi elementi puoi integrare pagamenti con Stripe in Python in modo robusto e scalabile, riducendo i rischi di incongruenze e garantendo una conferma affidabile dei pagamenti tramite webhook.

Torna su