Salta al contenuto principale

04 - Stripe + Italia: setup pagamenti SaaS

Pagamenti con Stripe in Italia: chi è il venditore, commissioni di listino a confronto, checkout costruito in locale e webhook con firma verificata.

Cosa ne pensi di questo articolo?

04 - Stripe + Italia: setup pagamenti SaaS
Indie Maker ItaliaFoto:SiddePexels

In Breve

Pagamenti con Stripe in Italia: chi è il venditore, commissioni di listino a confronto, checkout costruito in locale e webhook con firma verificata.

Chi è il venditore: la domanda che precede il checkout

Prima di scrivere una riga di integrazione con Stripe devi decidere una cosa che non è tecnica: quando un cliente paga, chi vende il prodotto? Se sei tu, l'IVA, la fattura e il rapporto con il consumatore sono tuoi. Se è un intermediario che si pone come «merchant of record», cioè come venditore ufficiale, quei compiti passano a lui, in cambio di una commissione più alta. La documentazione Stripe lo scrive in una tabella: con «Managed Payments» il merchant of record è Stripe, con gli altri prodotti Stripe è la tua attività. Quasi tutto il resto dell'articolo discende da questa scelta.

Per un'attività nel regime forfettario, che non addebita IVA sulle vendite nazionali, la scelta ha un peso in più, perché l'IVA sulle vendite ai consumatori di altri Paesi europei dipende dal luogo del cliente (art. 58 della direttiva 2006/112/CE). L'articolo sul regime forfettario spiega cosa ho verificato e cosa no; qui vediamo quanto costa ciascuna strada e come costruire un checkout che non si fida di nessuno.

Quanto costano le tre strade

I listini sono pubblici e cambiano, quindi ogni cifra seguente ha la data di consultazione (2026-11-13). Sul listino italiano di Stripe, le carte standard dello Spazio economico europeo costano 1,5% più 0,25 euro; le carte premium dello stesso spazio 2,8% più 0,25 euro; le carte del Regno Unito 2,5% più 0,25 euro, e quelle internazionali 3,15% più 0,25 euro, più 2% se serve conversione di valuta. Stripe Tax, che calcola le imposte, costa 0,5% per transazione se la tua impresa è registrata per riscuotere le imposte. Paddle e Lemon Squeezy dichiarano entrambi, sulle pagine dei prezzi, 5% più 50 centesimi per transazione, con funzione di merchant of record.

Il listato seguente trasforma queste cifre in un confronto per prezzo. Attenzione a un'approssimazione dichiarata nel commento: le tariffe di Paddle e Lemon Squeezy sono in dollari e qui il 0,50 è trattato come euro per vedere l'ordine di grandezza, senza convertire la valuta. Le tre funzioni sono volutamente separate, così puoi cambiare una sola cifra quando cambia il listino.

# Costo percentuale di un incasso, per tre modi di vendere.
# I listini sono quelli letti il 2026-11-13. Attenzione: Paddle e Lemon Squeezy
# indicano 50 centesimi di dollaro; qui trattiamo 0,50 come euro SOLO per
# vedere l'ordine di grandezza, senza convertire la valuta.
def stripe(prezzo):
    return prezzo * 0.015 + 0.25                 # carte standard SEE

def stripe_con_tax(prezzo):
    return stripe(prezzo) + prezzo * 0.005       # + Stripe Tax, 0,5% a transazione

def merchant_of_record(prezzo):
    return prezzo * 0.05 + 0.50                  # 5% + 50 centesimi

for prezzo in (5, 9, 29, 99):
    riga = [f"{prezzo:>3} EUR"]
    for nome, f in (("Stripe", stripe), ("Stripe+Tax", stripe_con_tax), ("MoR", merchant_of_record)):
        costo = f(prezzo)
        riga.append(f"{nome} {costo:5.2f} ({costo / prezzo * 100:4.1f}%)")
    print(" | ".join(riga))

# Output:
#   5 EUR | Stripe  0.33 ( 6.5%) | Stripe+Tax  0.35 ( 7.0%) | MoR  0.75 (15.0%)
#   9 EUR | Stripe  0.39 ( 4.3%) | Stripe+Tax  0.43 ( 4.8%) | MoR  0.95 (10.6%)
#  29 EUR | Stripe  0.69 ( 2.4%) | Stripe+Tax  0.83 ( 2.9%) | MoR  1.95 ( 6.7%)
#  99 EUR | Stripe  1.73 ( 1.8%) | Stripe+Tax  2.23 ( 2.3%) | MoR  5.45 ( 5.5%)

Il risultato mostra la forma del problema. Su un prodotto da 9 euro Stripe costa circa il 4,3% e un merchant of record circa il 10,6%; su un prodotto da 99 euro le percentuali scendono a 1,8% e 5,5%. La quota fissa di 25 o 50 centesimi pesa quindi molto sui prezzi bassi, ed è un buon motivo per preferire abbonamenti o licenze piuttosto che micropagamenti. La differenza tra le due strade, però, non è solo un costo: il merchant of record si assume compiti che per te hanno un valore, e per un prodotto digitale venduto in molti Paesi quel valore può superare la differenza. Sono confronti con numeri di listino, non una raccomandazione: la scelta dipende da dove vendi e dal tuo commercialista.

Il checkout ospitato: cosa vuol dire e cosa invii

Per iniziare il modo meno rischioso è il Checkout ospitato da Stripe: il cliente viene portato su una pagina di Stripe, dove inserisce i dati della carta, e tu non li vedi né li conservi. Crei una «sessione» dal server e rimandi il cliente all'indirizzo che ti restituisce. La documentazione dell'interfaccia di creazione indica i parametri essenziali: mode, che può essere pagamento singolo o abbonamento, gli elementi dell'ordine e gli indirizzi di ritorno.

Il listato seguente mostra la richiesta che la libreria ufficiale invierebbe, senza contattare Stripe: un client HTTP finto registra metodo, indirizzo e campi, e la chiave è palesemente falsa. L'importo è espresso in centesimi (900 per 9,00 euro), perché gli importi decimali sono una fonte classica di errori. Con mode=payment si incassa una volta; per un abbonamento si usa subscription e un prezzo ricorrente. La libreria è la versione 17.0.0 da PyPI in un ambiente isolato, e la richiesta è stata solo costruita, non inviata.

# Costruisce una Checkout Session SENZA contattare Stripe: un client HTTP finto
# registra la richiesta che la libreria ufficiale invierebbe. La chiave e' falsa.
import json
import stripe

class ClienteFinto(stripe.HTTPClient):
    def __init__(self):
        super().__init__()
        self.richieste = []
        self.name = "cliente-finto"
    def request(self, method, url, headers, post_data=None, **kwargs):
        self.richieste.append((method, url, post_data))
        corpo = json.dumps({"id": "cs_test_finta", "object": "checkout.session",
                            "url": "https://checkout.example/finta"})
        return corpo, 200, {}

cliente_http = ClienteFinto()
client = stripe.StripeClient("sk_test_chiave_falsa", http_client=cliente_http)
sessione = client.v1.checkout.sessions.create({
    "mode": "payment",                       # pagamento una tantum; "subscription" per gli abbonamenti
    "line_items": [{"price_data": {"currency": "eur",
                                   "unit_amount": 900,      # centesimi: 9,00 EUR
                                   "product_data": {"name": "Licenza strumento"}},
                    "quantity": 1}],
    "success_url": "https://esempio.test/grazie",
    "cancel_url": "https://esempio.test/annullato",
})
metodo, url, dati = cliente_http.richieste[0]
print(metodo, url)
for campo in sorted(dati.split("&")):
    print("  ", campo)
print("risposta finta:", sessione.id)

# Output:
# post https://api.stripe.com/v1/checkout/sessions
#    cancel_url=https%3A%2F%2Fesempio.test%2Fannullato
#    line_items[0][price_data][currency]=eur
#    line_items[0][price_data][product_data][name]=Licenza+strumento
#    line_items[0][price_data][unit_amount]=900
#    line_items[0][quantity]=1
#    mode=payment
#    success_url=https%3A%2F%2Fesempio.test%2Fgrazie
# risposta finta: cs_test_finta

I campi sono ordinati nell'uscita per rendere il risultato deterministico: la libreria non garantisce l'ordine. In un'applicazione reale non scrivi il client finto: lo usi solo per provare che l'integrazione invia ciò che pensi. Lo schema da adattare è il blocco client.v1.checkout.sessions.create con la tua chiave segreta, presa da una variabile d'ambiente, mai dal codice. Non l'ho eseguito contro Stripe, perché serve un account e una chiave: ciò che segue dopo il pagamento, cioè il webhook, è la parte dove si sbaglia davvero.

Il webhook: non fidarti del cliente, fidati della firma

Il cliente torna sulla tua pagina di conferma, ma quella pagina non prova che abbia pagato: chiunque può digitare l'indirizzo. La prova arriva dal server di Stripe, che chiama un tuo indirizzo con un evento firmato. La documentazione dice di verificare l'intestazione Stripe-Signature con il segreto del webhook, di usare il corpo grezzo della richiesta e di rispondere con un 2xx prima di qualunque elaborazione lunga. Raccomanda anche di registrare gli identificativi degli eventi, perché lo stesso evento può arrivare più di una volta.

Il programma seguente applica queste regole con la funzione ufficiale. La funzione gestisci verifica la firma, scarta i duplicati (l'insieme gia_visti sostituisce una tabella con vincolo di unicità) e distingue gli eventi. Il segreto è una stringa di prova, l'intestazione la costruisce la funzione di test della libreria. Le cinque prove coprono il caso felice, il duplicato, un corpo alterato, una firma vecchia di un'ora e l'assenza della firma. L'insieme in memoria non regge un riavvio né più processi: in produzione serve un archivio condiviso.

# Verifica di un webhook Stripe, in locale e senza rete.
# Il segreto e' una stringa di prova inventata: quello vero (whsec_...) sta
# nella tua dashboard e non va mai scritto nel codice.
import json
import time
import stripe

SEGRETO = "whsec_prova_locale_non_reale"
gia_visti = set()  # in produzione: una tabella con vincolo di unicita' sull'id evento

def gestisci(corpo_grezzo: str, intestazione: str):
    """Restituisce (codice HTTP, messaggio). Il corpo deve essere quello GREZZO."""
    try:
        evento = stripe.Webhook.construct_event(corpo_grezzo, intestazione, SEGRETO)
    except stripe.SignatureVerificationError as errore:
        return 400, "firma rifiutata: " + str(errore).split(" (")[0].split(".")[0]
    if evento["id"] in gia_visti:
        return 200, "duplicato ignorato"      # rispondere 2xx, altrimenti Stripe riprova
    gia_visti.add(evento["id"])
    if evento["type"] == "checkout.session.completed":
        return 200, "pagamento registrato: " + evento["data"]["object"]["id"]
    return 200, "evento non gestito: " + evento["type"]

corpo = json.dumps({"id": "evt_prova_1", "object": "event",
                    "type": "checkout.session.completed",
                    "data": {"object": {"id": "cs_prova_1", "object": "checkout.session"}}})
firma = stripe.WebhookSignature.generate_signature_header(corpo, SEGRETO)
vecchia = stripe.WebhookSignature.generate_signature_header(corpo, SEGRETO, int(time.time()) - 3600)

print("1 valido        ->", gestisci(corpo, firma))
print("2 stesso evento ->", gestisci(corpo, firma))
print("3 corpo toccato ->", gestisci(corpo.replace("cs_prova_1", "cs_prova_2"), firma))
print("4 firma di 1 ora fa ->", gestisci(corpo, vecchia))
print("5 senza firma   ->", gestisci(corpo, ""))

# Output:
# 1 valido        -> (200, 'pagamento registrato: cs_prova_1')
# 2 stesso evento -> (200, 'duplicato ignorato')
# 3 corpo toccato -> (400, 'firma rifiutata: No signatures found matching the expected signature for payload')
# 4 firma di 1 ora fa -> (400, 'firma rifiutata: Timestamp outside the tolerance zone')
# 5 senza firma   -> (400, 'firma rifiutata: No Stripe-Signature header value was provided')

Nel risultato, il secondo evento risponde 200 anche se è un duplicato: Stripe riprova le consegne non riuscite, quindi rispondere con un errore a un duplicato ne genererebbe altri. Il terzo caso conferma che cambiare anche un carattere del corpo invalida la firma, il quarto che una firma vecchia viene respinta con una tolleranza predefinita di cinque minuti nella libreria, come scrive la documentazione. La verifica fallisce anche se un framework riscrive il corpo prima che lo leggi: è l'errore più frequente, e Stripe lo ricorda esplicitamente. Un rifiuto con codice 400 è la risposta giusta a una firma sbagliata.

Obblighi verso il consumatore nel checkout

Se vendi a consumatori con un contratto a distanza, il Codice del consumo prevede il diritto di recesso di quattordici giorni (art. 52, d.lgs. 206/2005). Per i contenuti digitali forniti senza supporto materiale l'art. 59, comma 1, lettera o), prevede un'eccezione: l'esecuzione deve essere iniziata e devono ricorrere tre condizioni, cioè il consenso espresso del consumatore all'avvio durante il periodo di recesso, il suo riconoscimento di perdere così il diritto, e la conferma del professionista prevista dagli articoli 50, comma 2, o 51, comma 7. Per questo molti checkout aggiungono due caselle, una per ciascuna dichiarazione.

Non è una formula magica: l'eccezione riguarda il contenuto digitale la cui esecuzione è già iniziata, e il testo del consenso va scritto bene. Il testo da mostrare, il momento della conferma e la gestione dei servizi in abbonamento sono questioni per un legale o per il commercialista. Quello che posso dirti è dove guardare.

Domande frequenti

Stripe emette fatture al posto mio?

Dipende da chi è il venditore. Secondo la tabella di confronto della documentazione, con i prodotti Stripe diversi da Managed Payments il merchant of record è la tua attività, perciò gli obblighi di fatturazione restano tuoi. Cosa fare in pratica dipende dal tuo regime fiscale e va chiesto al commercialista.

Posso provare l'integrazione senza soldi veri?

Sì: Stripe fornisce chiavi e carte di prova in modalità test, ma non le ho usate qui perché richiedono un account. Il programma sopra prova la parte più delicata, la firma, senza rete.

Meglio un abbonamento o un pagamento unico?

Dipende dal prodotto. L'abbonamento rende il ricavo più prevedibile ma richiede gestione di rinnovi, carte scadute e rimborsi; il pagamento unico è più semplice. Con le commissioni viste sopra, prezzi molto bassi sono i più penalizzati.

Che cosa sai fare adesso

Sai distinguere chi è il venditore con Stripe e con un merchant of record, confrontare le commissioni a parità di prezzo con un programma che puoi aggiornare, costruire una sessione di checkout senza inviare dati, e verificare un webhook con firma, tolleranza e protezione dai duplicati. Il passo successivo è il posizionamento del prodotto, che trovi nell'articolo sul marketing organico; per il costo complessivo dell'infrastruttura torna a stack e costi, e per i rischi di lancio alla pre-mortem.

Quando i pagamenti funzionano arrivano le domande dei clienti: come organizzarle lo spiega l'articolo sul supporto da solo, mentre i numeri di un piano di lancio si leggono con più cautela dopo l'articolo sui casi di successo.

Fonti

Prova Questi Strumenti

Strumenti gratuiti correlati a questo argomento.

Commenti

Caricamento commenti...

Accedi per commentare

Discuti questo articolo nella community

Unisciti al gruppo Telegram per discutere con altri sviluppatori, fare domande e condividere le tue esperienze.

Ti è piaciuto questo articolo?

Esplora altri contenuti sul blog o scopri i miei progetti