Questa guida mostra come integrare pagamenti con Stripe in un backend scritto in Go, con esempi pratici e attenzione a sicurezza, affidabilità e gestione degli eventi tramite webhook. Vedremo due approcci principali:
- Stripe Checkout: la via più rapida e “chiavi in mano” per accettare pagamenti.
- Payment Intents: integrazione più flessibile, utile quando vuoi controllare l’esperienza di pagamento sul tuo sito o app.
Prerequisiti
- Account Stripe e chiavi API (test e live).
- Go 1.20+ (consigliato).
- Una base di HTTP server in Go (net/http) e un endpoint pubblico per i webhook (in sviluppo puoi usare Stripe CLI).
Installazione SDK Stripe per Go
Stripe mantiene un SDK ufficiale per Go. Installalo con:
go get github.com/stripe/stripe-go/v76
Import tipico:
import (
"github.com/stripe/stripe-go/v76"
)
Gestione delle chiavi e configurazione
Non hardcodare mai le chiavi. Usa variabili d’ambiente e differenzia chiaramente test e live. Un pattern comune:
package config
import (
"log"
"os"
)
type StripeConfig struct {
SecretKey string
WebhookSecret string
SuccessURL string
CancelURL string
FrontendBaseURL string
}
func LoadStripeConfig() StripeConfig {
c := StripeConfig{
SecretKey: os.Getenv("STRIPE_SECRET_KEY"),
WebhookSecret: os.Getenv("STRIPE_WEBHOOK_SECRET"),
SuccessURL: os.Getenv("STRIPE_SUCCESS_URL"),
CancelURL: os.Getenv("STRIPE_CANCEL_URL"),
FrontendBaseURL: os.Getenv("FRONTEND_BASE_URL"),
}
if c.SecretKey == "" {
log.Fatal("STRIPE_SECRET_KEY non impostata")
}
return c
}
Nel tuo main:
package main
import (
"net/http"
"github.com/stripe/stripe-go/v76"
"yourapp/config"
)
func main() {
cfg := config.LoadStripeConfig()
stripe.Key = cfg.SecretKey
mux := http.NewServeMux()
// registra handlers...
_ = mux
http.ListenAndServe(":8080", mux)
}
Approccio 1: Stripe Checkout (consigliato per partire)
Stripe Checkout ti permette di creare una sessione server-side e reindirizzare l’utente a una pagina di pagamento ospitata da Stripe. È spesso la soluzione migliore per partire velocemente e ridurre il perimetro di compliance.
Creare una Checkout Session
Scenario tipico: l’utente seleziona un prodotto/abbonamento, il backend crea la sessione e restituisce l’URL di Checkout.
package handlers
import (
"encoding/json"
"net/http"
"github.com/stripe/stripe-go/v76"
"github.com/stripe/stripe-go/v76/checkout/session"
)
type CreateCheckoutRequest struct {
// In produzione non fidarti del prezzo dal client.
// Passa un productId/priceId e risolvi lato server.
PriceID string `json:"priceId"`
Quantity int64 `json:"quantity"`
CustomerEmail string `json:"customerEmail,omitempty"`
}
type CreateCheckoutResponse struct {
URL string `json:"url"`
}
func CreateCheckoutSession(successURL, cancelURL string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
var req CreateCheckoutRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "invalid json", http.StatusBadRequest)
return
}
if req.PriceID == "" || req.Quantity <= 0 {
http.Error(w, "missing priceId or invalid quantity", http.StatusBadRequest)
return
}
params := &stripe.CheckoutSessionParams{
Mode: stripe.String(string(stripe.CheckoutSessionModePayment)),
SuccessURL: stripe.String(successURL),
CancelURL: stripe.String(cancelURL),
LineItems: []*stripe.CheckoutSessionLineItemParams{
{
Price: stripe.String(req.PriceID),
Quantity: stripe.Int64(req.Quantity),
},
},
// Opzionale: associa email cliente, oppure crea/usa un Customer.
CustomerEmail: stripe.String(req.CustomerEmail),
}
s, err := session.New(params)
if err != nil {
http.Error(w, "stripe error: "+err.Error(), http.StatusBadGateway)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(CreateCheckoutResponse{URL: s.URL})
}
}
Nota importante: non lasciare che il client invii prezzi arbitrari. Invia un identificatore (ad esempio priceId di Stripe o un tuo productId) e risolvi quantità e importi lato server.
Redirect lato client
Il backend restituisce un URL; il front-end può fare window.location = url. Se usi Stripe.js, puoi anche usare redirectToCheckout, ma spesso l’URL è sufficiente.
Approccio 2: Payment Intents (più controllo, più responsabilità)
Payment Intents è il flusso raccomandato quando vuoi un checkout integrato nel tuo sito/app o devi gestire pagamenti complessi (ad esempio salvataggio di metodi di pagamento, pagamenti in due fasi, SCA/3DS, ecc.). Il backend crea un PaymentIntent e il client lo conferma usando Stripe.js o SDK mobile.
Creare un PaymentIntent in Go
Esempio per pagamenti one-shot con importo in centesimi:
package handlers
import (
"encoding/json"
"net/http"
"github.com/stripe/stripe-go/v76"
"github.com/stripe/stripe-go/v76/paymentintent"
)
type CreatePIRequest struct {
Amount int64 `json:"amount"` // in centesimi
Currency string `json:"currency"` // es: "eur"
}
type CreatePIResponse struct {
ClientSecret string `json:"clientSecret"`
}
func CreatePaymentIntent() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
var req CreatePIRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "invalid json", http.StatusBadRequest)
return
}
if req.Amount <= 0 {
http.Error(w, "invalid amount", http.StatusBadRequest)
return
}
if req.Currency == "" {
req.Currency = "eur"
}
// In produzione: calcola l'importo dal carrello lato server.
params := &stripe.PaymentIntentParams{
Amount: stripe.Int64(req.Amount),
Currency: stripe.String(req.Currency),
AutomaticPaymentMethods: &stripe.PaymentIntentAutomaticPaymentMethodsParams{
Enabled: stripe.Bool(true),
},
}
pi, err := paymentintent.New(params)
if err != nil {
http.Error(w, "stripe error: "+err.Error(), http.StatusBadGateway)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(CreatePIResponse{ClientSecret: pi.ClientSecret})
}
}
Il client userà clientSecret per confermare il pagamento. Questo pezzo è tipicamente in JavaScript (Stripe.js) o nelle SDK mobile.
Quando preferire Payment Intents
- Vuoi un checkout completamente custom e non vuoi reindirizzare a Stripe Checkout.
- Devi gestire logiche avanzate (ad esempio autorizzazione e cattura differita, metodi di pagamento salvati, ecc.).
- Hai bisogno di controllare in modo fine lo stato del pagamento e i passaggi SCA.
Webhooks: la parte più importante
La conferma “vera” di un pagamento non dovrebbe basarsi sul redirect di successo o su una risposta del client. La fonte autorevole è l’evento webhook inviato da Stripe al tuo backend (ad esempio checkout.session.completed o payment_intent.succeeded).
Perché i webhook sono essenziali
- Il cliente può chiudere la pagina o perdere la connessione dopo aver pagato.
- I pagamenti possono richiedere azioni aggiuntive (SCA) e completarsi più tardi.
- Devi avere una registrazione server-side idempotente e verificabile.
Verifica della firma del webhook
Stripe firma ogni webhook. Devi verificare la firma usando il WebhookSecret del tuo endpoint webhook. Esempio in Go:
package handlers
import (
"io"
"net/http"
"github.com/stripe/stripe-go/v76"
"github.com/stripe/stripe-go/v76/webhook"
)
func StripeWebhook(webhookSecret string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
const maxBodyBytes = int64(65536)
r.Body = http.MaxBytesReader(w, r.Body, maxBodyBytes)
payload, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "read error", http.StatusBadRequest)
return
}
sigHeader := r.Header.Get("Stripe-Signature")
event, err := webhook.ConstructEvent(payload, sigHeader, webhookSecret)
if err != nil {
http.Error(w, "signature verification failed", http.StatusBadRequest)
return
}
// Gestione evento
switch event.Type {
case "checkout.session.completed":
// Parse dell'oggetto contenuto nell'evento
// (vedi sezione seguente)
case "payment_intent.succeeded":
// idem
default:
// eventi non gestiti
}
// Rispondi 200 rapidamente, dopo aver messo in coda/registrato il lavoro.
w.WriteHeader(http.StatusOK)
}
}
Deserializzare gli oggetti Stripe dall’evento
Stripe invia un payload generico. In Stripe Go, puoi usare stripe.Event e poi deserializzare event.Data.Raw nel tipo desiderato.
package handlers
import (
"encoding/json"
"log"
"github.com/stripe/stripe-go/v76"
)
func handleCheckoutCompleted(event stripe.Event) {
var s stripe.CheckoutSession
if err := json.Unmarshal(event.Data.Raw, &s); err != nil {
log.Printf("unmarshal checkout session: %v", err)
return
}
// Esempio: usa s.ID come riferimento; puoi leggere s.PaymentStatus, s.AmountTotal, ecc.
// In produzione: aggiorna il tuo DB, attiva l'ordine/abbonamento, ecc.
log.Printf("checkout completed session=%s payment_status=%s", s.ID, s.PaymentStatus)
}
Per un PaymentIntent:
func handlePaymentIntentSucceeded(event stripe.Event) {
var pi stripe.PaymentIntent
if err := json.Unmarshal(event.Data.Raw, &pi); err != nil {
log.Printf("unmarshal payment intent: %v", err)
return
}
// Usa pi.ID e pi.Metadata per correlare con il tuo ordine.
log.Printf("payment intent succeeded id=%s amount=%d", pi.ID, pi.Amount)
}
Correlare pagamenti e ordini con metadata
Un modo robusto per collegare un pagamento a un ordine nel tuo database è usare Metadata. Per esempio, quando crei un Checkout Session o un PaymentIntent, inserisci order_id.
params := &stripe.CheckoutSessionParams{
// ...
PaymentIntentData: &stripe.CheckoutSessionPaymentIntentDataParams{
Metadata: map[string]string{
"order_id": "ORD-12345",
"user_id": "USR-999",
},
},
}
Nel webhook, leggi la metadata dal PaymentIntent (o dalla sessione, a seconda del flusso) e aggiorna lo stato dell’ordine in modo deterministico.
Idempotenza: evitare doppie creazioni e doppie attivazioni
Nel mondo reale, richieste e webhook possono essere ritentati. Stripe ritenta i webhook se il tuo endpoint non risponde 2xx. Anche il client può ritentare una chiamata per creare un pagamento. Serve idempotenza su due livelli:
- Idempotency-Key per le chiamate Stripe: impedisce che la stessa operazione crei più oggetti su Stripe.
- Idempotenza applicativa nel tuo DB: impedisce di attivare due volte un ordine quando arrivano più eventi equivalenti.
Usare Idempotency-Key con Stripe Go
Stripe Go permette di impostare l’idempotency key tramite Params (es. stripe.Params). Un approccio comune è creare una chiave deterministica basata su orderId + tipo operazione.
import "github.com/stripe/stripe-go/v76"
// Esempio: impostare idempotency key in params
params := &stripe.PaymentIntentParams{
Amount: stripe.Int64(1999),
Currency: stripe.String("eur"),
}
params.Params.IdempotencyKey = "pi_create_ORD-12345"
pi, err := paymentintent.New(params)
Idempotenza nel webhook
Nel tuo database, salva l’event.id o l’identificatore dell’oggetto (es. payment_intent) come “processato”. Se arriva di nuovo, ignora in modo sicuro.
Sicurezza e best practice
- Mai fidarti del client per importi e valuta: calcola lato server dal carrello/prezzi in DB o dai PriceID Stripe.
- Verifica firma webhook sempre, e limita la dimensione del body.
- Usa HTTPS in produzione e proteggi le variabili d’ambiente (secret manager).
- Log puliti: non loggare segreti o payload completi con dati sensibili.
- Least privilege: se possibile separa ruoli/chiavi (ad esempio chiavi ristrette o ambienti distinti).
Testing in locale con Stripe CLI
Stripe CLI ti aiuta a inoltrare webhook al tuo computer e a generare eventi di test. Un flusso tipico:
stripe login
stripe listen --forward-to localhost:8080/webhooks/stripe
Il comando listen mostrerà un webhook signing secret dedicato per il listener, da usare come STRIPE_WEBHOOK_SECRET in locale.
Per triggerare un evento di test:
stripe trigger checkout.session.completed
Struttura consigliata del progetto
config/: caricamento configurazioni (Stripe, DB, ecc.).handlers/: HTTP handlers (create session, webhook, ecc.).payments/: logica di dominio per ordini, idempotenza, mapping eventi.store/: accesso al database (repository pattern).
Esempio di wiring nel router
Un esempio minimale di registrazione endpoint:
package main
import (
"net/http"
"github.com/stripe/stripe-go/v76"
"yourapp/config"
"yourapp/handlers"
)
func main() {
cfg := config.LoadStripeConfig()
stripe.Key = cfg.SecretKey
mux := http.NewServeMux()
mux.Handle("/api/checkout/session", handlers.CreateCheckoutSession(cfg.SuccessURL, cfg.CancelURL))
mux.Handle("/webhooks/stripe", handlers.StripeWebhook(cfg.WebhookSecret))
http.ListenAndServe(":8080", mux)
}
Gestione degli stati: cosa salvare nel database
Per un flusso robusto, salva almeno:
- Order ID (tuo identificatore).
- Stripe object IDs (es.
checkout_session_id,payment_intent_id). - Stato ordine (pending, paid, failed, refunded, ecc.).
- Eventi processati (es. tabella
stripe_eventsconevent_idunivoco). - Importo e valuta “congelati” al momento dell’acquisto.
Rimborsi e post-pagamento
I rimborsi possono essere avviati lato dashboard Stripe o tramite API. In genere, per un backend serve:
- Verificare che l’ordine sia rimborsabile secondo le tue regole.
- Creare un refund su Stripe associato al PaymentIntent o Charge.
- Aggiornare lo stato dell’ordine tramite webhook (es.
charge.refundedo eventi correlati).
import (
"github.com/stripe/stripe-go/v76"
"github.com/stripe/stripe-go/v76/refund"
)
func RefundPaymentIntent(paymentIntentID string) (*stripe.Refund, error) {
params := &stripe.RefundParams{
PaymentIntent: stripe.String(paymentIntentID),
}
return refund.New(params)
}
Errori comuni e come evitarli
- Usare il redirect di successo come conferma: usa sempre i webhook come fonte di verità.
- Importi dal client: calcola lato server e/o usa PriceID.
- Webhook non idempotenti: gestisci i retry, salva
event.id. - Firma webhook ignorata: verifica sempre la firma.
- Timeout troppo lunghi nel webhook: rispondi rapidamente e sposta lavorazioni pesanti su una coda.
Checklist per andare in produzione
- Chiavi live impostate correttamente e segreti conservati in modo sicuro.
- Endpoint webhook pubblico con HTTPS e firma verificata.
- Log e alerting sugli errori webhook e su pagamenti falliti.
- Idempotenza su creazione pagamenti e su attivazione ordini.
- Testing end-to-end con carte di test e scenari SCA.
Con questi elementi hai una base solida per accettare pagamenti con Stripe in Go, mantenendo un flusso affidabile e sicuro. Da qui puoi estendere verso abbonamenti, salvataggio metodi di pagamento, fatturazione e integrazioni più avanzate, mantenendo sempre il webhook come “single source of truth” per lo stato delle transazioni.