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 pagamentorequires_confirmation: pronto per essere confermatorequires_action: l’utente deve completare un’autenticazione (SCA/3DS)processing: in elaborazionesucceeded: pagamento riuscitocanceled: 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_idper 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_secreto PII inutilmente. - Versiona l’API: fissa una
stripe.api_versiono 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
- Chiavi e segreti solo in variabili d’ambiente
- Prezzi validati dal backend
- Checkout o PaymentIntent scelto in base al livello di personalizzazione
- Webhook con verifica firma e logica idempotente
- 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.