Stripe è una delle soluzioni più diffuse per accettare pagamenti online. In Node.js puoi integrare Stripe in due modi principali: Checkout (pagina di pagamento ospitata da Stripe, più veloce e sicura da implementare) oppure Payment Intents (flusso personalizzato, massima flessibilità). In questa guida vedrai entrambi gli approcci, con particolare attenzione a sicurezza, webhook, test e buone pratiche operative.
Prerequisiti e concetti chiave
- Account Stripe e chiavi API (test e live).
- Node.js 18+ e un server HTTP (Express è l'esempio più comune).
- Conoscenza di base di richieste HTTP e variabili d'ambiente.
Terminologia essenziale:
- PaymentIntent: oggetto che rappresenta un tentativo di pagamento e il suo stato (richiede azione, riuscito, fallito).
- Checkout Session: oggetto che crea una pagina di pagamento Stripe pronta all'uso.
- Webhook: chiamata server-to-server con cui Stripe notifica eventi (pagamento riuscito, refund, chargeback, ecc.).
- Idempotency: tecnica per prevenire doppie creazioni (ad esempio, doppio click su "Paga").
Installazione e configurazione sicura
Installa le dipendenze principali. Per semplicità useremo Express e la libreria ufficiale Stripe per Node:
npm init -y
npm install express stripe dotenv
npm install --save-dev nodemon
Crea un file .env (da non committare) e inserisci le chiavi:
STRIPE_SECRET_KEY=sk_test_xxx
STRIPE_WEBHOOK_SECRET=whsec_xxx
BASE_URL=http://localhost:4242
Note importanti:
- Non esporre mai
STRIPE_SECRET_KEYnel frontend. - Usa una chiave pubblicabile (
pk_...) solo nel client quando necessario (ad esempio Stripe.js per confermare un PaymentIntent). - In produzione, gestisci i segreti con un secret manager (ad esempio AWS Secrets Manager, GCP Secret Manager, Vault) o variabili d'ambiente del tuo provider.
Server di esempio con Express
Ecco uno scheletro di server. Gestiremo due flussi: Checkout e Payment Intents, più i webhook.
import express from "express";
import dotenv from "dotenv";
import Stripe from "stripe";
dotenv.config();
const app = express();
const port = process.env.PORT || 4242;
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
apiVersion: "2024-06-20",
});
// Per endpoint normali JSON
app.use(express.json());
// Endpoint healthcheck
app.get("/health", (_req, res) => {
res.json({ ok: true });
});
app.listen(port, () => {
console.log(`Server in ascolto su http://localhost:${port}`);
});
Importante: l'endpoint webhook richiede il corpo "raw" per verificare la firma. Quindi useremo un middleware diverso
solo per quel percorso, evitando di applicare globalmente express.json() a quel route.
Approccio 1: Stripe Checkout (consigliato per iniziare)
Checkout ti consente di avviare un pagamento in pochi minuti con una pagina ospitata. È robusto, gestisce in modo affidabile SCA e 3D Secure dove richiesti, e riduce il rischio di errori nel frontend.
Creare una Checkout Session
In questo esempio creiamo una sessione in modalità payment con un singolo articolo. In un caso reale,
calcola importi e prodotti lato server, usando prezzi dal database o da Stripe.
app.post("/create-checkout-session", async (req, res) => {
try {
// Esempio: importo e descrizione decisi lato server (non fidarti del client)
const { quantity = 1 } = req.body ?? {};
const safeQuantity = Math.max(1, Math.min(10, Number(quantity) || 1));
const session = await stripe.checkout.sessions.create({
mode: "payment",
success_url: `${process.env.BASE_URL}/success?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${process.env.BASE_URL}/cancel`,
line_items: [
{
quantity: safeQuantity,
price_data: {
currency: "eur",
unit_amount: 1999, // 19,99 EUR in centesimi
product_data: {
name: "Abbonamento mensile (esempio)",
description: "Esempio di prodotto per dimostrazione",
},
},
},
],
// Facoltativo ma utile: collega un utente interno
metadata: {
internalUserId: "user_123",
},
});
res.json({ url: session.url });
} catch (err) {
console.error(err);
res.status(500).json({ error: "Impossibile creare la sessione di pagamento." });
}
});
Dal client, reindirizzi l'utente all'URL restituito (ad esempio con window.location.href = url).
Confermare il pagamento in modo affidabile
Non considerare "pagato" un ordine solo perché l'utente è finito su success_url.
La conferma affidabile avviene tramite webhook: è Stripe a notificare al tuo server che il pagamento è andato a buon fine.
Approccio 2: Payment Intents (flusso personalizzato)
Se ti serve una UI totalmente custom (ad esempio un checkout integrato nel tuo sito con elementi Stripe), usa Payment Intents. Il pattern più comune è:
- Il server crea un PaymentIntent con importo e valuta calcolati in modo sicuro.
- Il client usa Stripe.js per confermare il pagamento con il
client_secret. - Il server riceve l'evento webhook e aggiorna lo stato dell'ordine.
Creare un PaymentIntent lato server
app.post("/create-payment-intent", async (req, res) => {
try {
// Nel mondo reale: calcola amount da carrello/ordine nel DB
const { orderId } = req.body ?? {};
if (!orderId) {
return res.status(400).json({ error: "orderId obbligatorio" });
}
const amount = 4990; // 49,90 EUR in centesimi
// Idempotency: evita duplicati se la richiesta viene ripetuta
const idempotencyKey = `pi_${orderId}`;
const paymentIntent = await stripe.paymentIntents.create(
{
amount,
currency: "eur",
automatic_payment_methods: { enabled: true },
metadata: { orderId },
},
{ idempotencyKey }
);
res.json({ clientSecret: paymentIntent.client_secret });
} catch (err) {
console.error(err);
res.status(500).json({ error: "Impossibile creare PaymentIntent" });
}
});
Il frontend (non incluso qui) userà clientSecret con Stripe.js per completare il pagamento.
Anche in questo flusso, la conferma definitiva dell'ordine deve avvenire con webhook.
Webhook: il cuore dell'integrazione affidabile
I webhook sono fondamentali perché:
- gestiscono casi in cui l'utente chiude il browser;
- sono server-to-server, quindi più affidabili del redirect;
- coprono eventi successivi (refund, contestazioni, aggiornamenti di stato).
Endpoint webhook con verifica firma
Per verificare la firma hai bisogno di STRIPE_WEBHOOK_SECRET. In locale puoi ottenerla con Stripe CLI.
L'endpoint deve ricevere il corpo grezzo (raw).
// Webhook: serve raw body, quindi NON usare express.json() su questa route.
app.post(
"/webhook",
express.raw({ type: "application/json" }),
(req, res) => {
const sig = req.headers["stripe-signature"];
let event;
try {
event = stripe.webhooks.constructEvent(
req.body,
sig,
process.env.STRIPE_WEBHOOK_SECRET
);
} catch (err) {
console.error("Firma webhook non valida:", err.message);
return res.status(400).send(`Webhook Error: ${err.message}`);
}
// Gestione eventi principali
switch (event.type) {
case "checkout.session.completed": {
const session = event.data.object;
// Qui: recupera session.id, session.payment_intent o session.metadata
// Aggiorna ordine come pagato nel tuo DB
console.log("Checkout completato:", session.id);
break;
}
case "payment_intent.succeeded": {
const paymentIntent = event.data.object;
console.log("PaymentIntent riuscito:", paymentIntent.id);
// Aggiorna ordine come pagato usando paymentIntent.metadata.orderId
break;
}
case "payment_intent.payment_failed": {
const paymentIntent = event.data.object;
console.log("PaymentIntent fallito:", paymentIntent.id);
// Logga motivo e notifica eventuali sistemi interni
break;
}
default:
console.log(`Evento non gestito: ${event.type}`);
}
// Rispondi 200 per confermare ricezione
res.json({ received: true });
}
);
Buone pratiche nella gestione webhook:
- Processa gli eventi in modo idempotente (stesso evento può arrivare più volte).
- Registra l'ID evento (
event.id) nel database per evitare doppie elaborazioni. - Non fare operazioni lente direttamente nel handler: se necessario, invia un job in coda (ad esempio BullMQ, SQS, RabbitMQ).
- Gestisci errori temporanei e retry.
Stripe CLI e test end-to-end
Stripe mette a disposizione una CLI utile per:
- inoltrare webhook in locale (forwarding);
- triggerare eventi di test;
- verificare rapidamente flussi di pagamento.
# Login (apre browser)
stripe login
# Ascolta eventi e inoltra al tuo server locale
stripe listen --forward-to localhost:4242/webhook
Il comando stripe listen mostrerà un valore whsec_... da mettere in STRIPE_WEBHOOK_SECRET.
Puoi poi triggerare eventi:
stripe trigger payment_intent.succeeded
stripe trigger checkout.session.completed
Sicurezza e robustezza: checklist essenziale
- Calcola importi lato server: non fidarti di prezzi inviati dal client.
- Verifica firma webhook: senza, chiunque potrebbe chiamare il tuo endpoint e simulare pagamenti.
- Usa HTTPS in produzione.
- Idempotency su creazione PaymentIntent e su aggiornamento ordine.
- Gestisci stati intermedi: un pagamento può richiedere azione (SCA), fallire o rimanere in sospeso.
- Log e monitoraggio: registra errori e latenza; imposta alert (ad esempio su webhook falliti).
Rimborsi, annulli e riconciliazione
In molti casi dovrai gestire rimborsi (totali o parziali). Un rimborso può essere avviato lato dashboard Stripe oppure via API.
app.post("/refund", async (req, res) => {
try {
const { paymentIntentId, amount } = req.body ?? {};
if (!paymentIntentId) {
return res.status(400).json({ error: "paymentIntentId obbligatorio" });
}
const refund = await stripe.refunds.create({
payment_intent: paymentIntentId,
// amount facoltativo: se omesso rimborsa tutto
amount: typeof amount === "number" ? amount : undefined,
});
res.json({ refundId: refund.id, status: refund.status });
} catch (err) {
console.error(err);
res.status(500).json({ error: "Impossibile creare il rimborso" });
}
});
Anche per rimborsi e contestazioni, usa i webhook (ad esempio eventi charge.refunded o simili) per mantenere allineato il tuo database.
Struttura consigliata per un progetto reale
- routes: definizione degli endpoint HTTP (checkout, payment intents, refunds).
- services: logica Stripe (creazioni, recuperi, validazioni, idempotency keys).
- webhooks: handler separati e testabili.
- db: ordini, utenti, stato pagamento, eventi webhook processati.
- config: lettura e validazione delle variabili d'ambiente.
Errori comuni e come evitarli
-
Uso errato del middleware JSON sul webhook:
se il body viene parsato in JSON, la verifica firma fallisce. Usa
express.rawsolo sulla route webhook. - Confermare l'ordine sul redirect: la pagina di successo non è una prova definitiva. Usa sempre i webhook.
- Prezzi dal client: un utente può manomettere la richiesta. Calcola importo/line items sul server.
- Doppie richieste: implementa idempotency e deduplica eventi webhook.
Conclusione
Per iniziare rapidamente e con un buon livello di sicurezza, Stripe Checkout è spesso la scelta migliore. Se invece hai bisogno di un'esperienza utente completamente personalizzata, Payment Intents ti offre il controllo necessario, a patto di gestire correttamente stati, conferme e webhook. In entrambi i casi, la regola d'oro è: lato server si calcolano importi e si confermano pagamenti tramite webhook.