Webhook Stripe in PHP: un gestore affidabile di cui ti puoi fidare
Un webhook Stripe che funzioni sul tuo laptop non è ancora un webhook di cui ti puoi fidare con i tuoi soldi. Questa guida mostra come verificare le firme, riconoscere rapidamente, deduplicare eventi e gestire i tentativi in PHP e Laravel, con codice che puoi incollare in un progetto.
In breve
- Considera il webhook, non il reindirizzamento del browser, come fonte di verità per lo stato del pagamento.
- Leggi il corpo della richiesta non elaborata e verifica l'intestazione
Stripe-Signatureprima di toccare il payload. - Restituisci rapidamente una risposta 2xx ed esegui il vero lavoro in coda o dopo aver archiviato l'evento.
- Memorizza l'ID dell'evento con un indice univoco: Stripe potrebbe fornire lo stesso evento più di una volta e fuori servizio.
- Prova con la Stripe CLI (
stripe listenestripe trigger) prima di andare in diretta.
Se accetti pagamenti con Stripe, i webhook Stripe in PHP sono la parte che decide se gli ordini vengono contrassegnati come pagati, gli abbonamenti vengono attivati e i rimborsi si riflettono nel tuo database. La pagina di pagamento è solo la porta d'ingresso: i clienti chiudono le schede, le banche confermano in ritardo e alcuni metodi di pagamento saldano minuti o giorni dopo. Il webhook è il messaggio di Stripe che dice cosa è realmente accaduto.
Costruiamo e manteniamo integrazioni di pagamento per i clienti e per i nostri prodotti (ad esempio Bill, che accetta i pagamenti Stripe e PayPal su una pagina di pagamento pubblica). Questo articolo è la lista di controllo che seguiamo quando scriviamo un endpoint webhook Stripe in PHP.
Come funzionano i webhook Stripe in un minuto
Registra un URL HTTPS nella Dashboard Stripe (o tramite l'API) e scegli quali tipi di eventi ricevere. Quando succede qualcosa (una sessione di pagamento viene completata, una fattura viene pagata, un addebito viene rimborsato) Stripe invia una richiesta POST con un oggetto Evento JSON a quell'URL. Il tuo server deve rispondere con un codice di stato 2xx. In caso contrario, Stripe riprova con backoff esponenziale per un massimo di tre giorni in modalità live.
Tre fatti modellano l’intero design:
- Gli eventi possono arrivare più di una volta. I nuovi tentativi e i problemi di rete rendono normali i duplicati.
- Gli eventi possono arrivare fuori ordine. Potresti vedere
invoice.paidprima dicustomer.subscription.created. - Chiunque può POSTARE sul tuo URL. Senza la verifica della firma, un utente malintenzionato può contrassegnare il proprio ordine come pagato.
Passaggio 1. Installa l'SDK e crea l'endpoint
Utilizza la libreria ufficiale, stripe/stripe-php e mantieni segreto l'endpoint (inizia con whsec_) in una variabile di ambiente, mai nel repository.
composer require stripe/stripe-php
# .env
STRIPE_SECRET_KEY=sk_live_...
STRIPE_WEBHOOK_SECRET=whsec_...
Ogni endpoint webhook nella Dashboard ha il proprio segreto di firma e il segreto stripe listen in fase di sviluppo è ancora diverso. Un bug molto comune di "verifica della firma non riuscita" è semplicemente l'utilizzo di quello sbagliato.
Passaggio 2. Verificare la firma sul corpo grezzo
Stripe firma ogni richiesta. L'intestazione Stripe-Signature contiene un timestamp e una o più firme calcolate come HMAC-SHA256 di timestamp.payload con il segreto dell'endpoint. Per impostazione predefinita, la libreria controlla la firma e rifiuta gli eventi più vecchi di cinque minuti.
Il dettaglio importante: la verifica richiede gli esatti byte grezzi inviati Stripe. Se un framework analizza il JSON e lo ricodifichi, gli spazi bianchi cambiano e la firma non corrisponde più.
<?php
// public/webhooks/stripe.php (plain PHP)
require __DIR__ . '/../../vendor/autoload.php';
$payload = file_get_contents('php://input');
$sigHeader = $_SERVER['HTTP_STRIPE_SIGNATURE'] ?? '';
$secret = getenv('STRIPE_WEBHOOK_SECRET');
try {
$event = \Stripe\Webhook::constructEvent($payload, $sigHeader, $secret);
} catch (\UnexpectedValueException $e) {
http_response_code(400); // invalid JSON
exit;
} catch (\Stripe\Exception\SignatureVerificationException $e) {
http_response_code(400); // bad or missing signature
exit;
}
In Laravel, leggi $request->getContent() invece di $request->all() ed escludi il percorso dalla protezione CSRF (i webhook non trasportano un token CSRF; la firma è la protezione). Se possibile, inserisci il percorso all'esterno del gruppo middleware della sessione web.
Passaggio 3. Rendere il gestore idempotente
Idempotente significa: elaborare lo stesso evento due volte dà lo stesso risultato di elaborarlo una volta. L'approccio affidabile più semplice è una tabella di ID eventi elaborati con un indice univoco. Inserisci prima; se l'inserimento fallisce perché l'ID esiste, hai già visto l'evento.
CREATE TABLE stripe_events (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
event_id VARCHAR(64) NOT NULL,
type VARCHAR(100) NOT NULL,
payload JSON NOT NULL,
status ENUM('received','processed','failed') NOT NULL DEFAULT 'received',
received_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
processed_at TIMESTAMP NULL,
UNIQUE KEY uniq_event (event_id)
);
try {
$pdo->prepare('INSERT INTO stripe_events (event_id, type, payload) VALUES (?, ?, ?)')
->execute([$event->id, $event->type, $payload]);
} catch (PDOException $e) {
if ($e->getCode() === '23000') { // duplicate key: already received
http_response_code(200);
exit;
}
throw $e;
}
Memorizzare il carico utile grezzo ti fornisce una traccia di controllo: quando un cliente chiede "perché mi è stato addebitato due volte?", puoi riprodurre esattamente ciò che ti ha detto Stripe. Consente inoltre di rielaborare gli eventi non riusciti dopo aver corretto un bug.
Passaggio 4. Riconoscere rapidamente, elaborare separatamente
Stripe prevede una risposta entro pochi secondi. Se il tuo gestore invia e-mail, chiama altre API o genera PDF in linea, scadrà il timeout, Stripe riproverà e tu ripeterai lo stesso lavoro. Esegui il minimo nella richiesta - verifica, archivia, rispondi 200 - ed esegui la logica reale in un lavoratore in coda (code Laravel, Symfony Messenger, un lavoro cron che legge status = 'received' o un lavoratore gestito dal supervisore).
Se l'evento non può essere archiviato affatto (database inattivo), restituisce uno stato 5xx. Questo è l'unico caso in cui vuoi che Stripe riprovi più tardi.
Passaggio 5. Gestisci gli eventi che contano
Non hai bisogno di ogni tipo di evento. Iscriviti solo a ciò che utilizza la tua logica aziendale; meno abbonamenti significano meno rumore e una superficie di attacco più piccola. Un insieme tipico:
checkout.session.completed: il cliente ha terminato il checkout. Cerca il tuo ordine utilizzando ilclient_reference_ido i metadati impostati durante la creazione della sessione.payment_intent.succeededepayment_intent.payment_failed- per flussi di pagamento personalizzati.invoice.paideinvoice.payment_failed: fatturazione dell'abbonamento.customer.subscription.updatedecustomer.subscription.deleted: modifiche e cancellazioni del piano.charge.refundedecharge.dispute.created: denaro che viene restituito.
switch ($event->type) {
case 'checkout.session.completed':
$session = $event->data->object;
$orderId = $session->client_reference_id;
if ($session->payment_status === 'paid') {
markOrderPaid($orderId, $session->payment_intent);
}
break;
case 'invoice.payment_failed':
notifyBillingProblem($event->data->object->customer);
break;
case 'charge.refunded':
markOrderRefunded($event->data->object->payment_intent);
break;
default:
// unknown or unsubscribed type: acknowledge and ignore
break;
}
Due abitudini prevengono la maggior parte dei bug. Innanzitutto, non fidarti mai degli importi visualizzati dal browser: confronta l'importo pagato e la valuta dell'evento con l'ordine nel tuo database. In secondo luogo, poiché gli eventi possono essere fuori ordine, rendono monotonici i cambiamenti di stato: non spostare un ordine da "pagato" a "in sospeso" perché un evento più vecchio è arrivato in ritardo. In caso di dubbi, recupera l'oggetto corrente dall'API invece di fare affidamento sullo snapshot dell'evento.
Passaggio 6. Testare localmente con la Stripe CLI
La Stripe CLI inoltra eventi reali in modalità test al tuo computer, quindi non hai bisogno di un tunnel pubblico:
stripe login
stripe listen --forward-to http://localhost:8000/webhooks/stripe
# in another terminal:
stripe trigger checkout.session.completed
stripe trigger invoice.payment_failed
stripe listen stampa un segreto whsec_ temporaneo. Inseriscilo nel tuo .env locale. Testa di proposito anche i percorsi infelici: invia una richiesta con una firma sbagliata (aspettati 400), invia lo stesso evento due volte (aspettati un effetto database) e ferma il tuo addetto alla coda per un minuto per vedere che gli eventi si accumulano invece di scomparire.
Errori comuni che vediamo negli audit
- Utilizzando il reindirizzamento come prova di pagamento. La pagina
success_urlpuò essere aperta da chiunque; mostra "conferma del pagamento" fino all'arrivo del webhook. - Analisi JSON prima della verifica. I corpi ricodificati rompono la firma; i parser del corpo della struttura sono un colpevole frequente.
- Un segreto per testare e vivere. La modalità test e live hanno chiavi separate ed endpoint webhook separati.
- Nessuna registrazione. Senza eventi memorizzati e un canale di errore visibile, i clienti riscontrano bug nei pagamenti.
- Gestori lenti. Qualsiasi operazione superiore a pochi secondi attiva nuovi tentativi e duplica il lavoro.
- Blocco di Stripe sul firewall o WAF. Se utilizzi Cloudflare o un altro WAF, consenti il percorso del webhook e controlla che le regole del bot non restituiscano 403 ai server di Stripe.
Monitoraggio: come fai a sapere che continua a funzionare
Un webhook può rompersi mesi dopo il lancio: una modifica distribuita rinomina una colonna, un segreto viene ruotato, un certificato scade. La dashboard Stripe mostra i tentativi di consegna e gli errori per endpoint e Stripe ti invia un'e-mail quando un endpoint continua a non funzionare. Da parte tua, avvisa su tre cose: un numero crescente di eventi received o failed, un calo improvviso degli eventi all'ora e qualsiasi frequenza 4xx o 5xx sul percorso del webhook. Un piccolo lavoro di riconciliazione quotidiana che confronta gli ordini pagati nel tuo database con l'elenco dei pagamenti andati a buon fine di Stripe rileva il resto.
Come possiamo aiutare
Progettiamo e realizziamo pagamenti di produzione e integrazioni webhook con controlli di firma, idempotenza, tentativi e registri che il tuo team può leggere. Se disponi già di un flusso Stripe e desideri un secondo paio di occhi, una revisione o una consulenza sull'architettura è il modo più rapido per individuare i punti deboli. Per un quadro più ampio sullo spostamento di eventi critici da Zapier o Make, leggi quando si interrompe il no-code e per un altro provider consulta la nostra guida all'integrazione WayForPay in PHP.
Domande frequenti
Perché la verifica della firma del webhook Stripe non riesce?
Le cause più comuni sono l'utilizzo del segreto di firma errato (test o live, CLI o endpoint Dashboard), la verifica di un corpo analizzato o ricodificato anziché il corpo della richiesta non elaborata o un proxy che modifica il payload. Verificare rispetto agli esatti byte grezzi con il segreto whsec_ dell'endpoint.
Come posso impedire l'elaborazione di un evento Stripe due volte?
Memorizza ciascun ID evento in una tabella con un indice univoco e inseriscilo prima di eseguire qualsiasi operazione. Se l'inserimento non riesce con una chiave duplicata, restituisce 200 e salta l'elaborazione. Inoltre, rendi sicura la ripetizione dell'azione commerciale stessa.
Quale codice di risposta dovrebbe restituire un webhook Stripe ?
Restituisci un codice 2xx (200 o 204) non appena l'evento viene verificato e memorizzato. Restituisci 400 per una firma non valida e 5xx solo quando non puoi memorizzare l'evento e desideri che Stripe riprovi.
Per quanto tempo Stripe ritenta i webhook non riusciti?
In modalità live Stripe riprova con backoff esponenziale per un massimo di tre giorni. In modalità test la finestra dei tentativi è molto più breve, quindi non fare affidamento sui tentativi in modalità test per giudicare il comportamento della produzione.
Posso testare i webhook Stripe senza un URL pubblico?
SÌ. Esegui stripe listen --forward-to con la Stripe CLI per inoltrare eventi di test a localhost e utilizza il trigger stripe per generare eventi di esempio.
Chiamata conoscitiva gratuita
Portate uno stack disordinato o un'integrazione bloccata - Integrono mapperà la prima mossa più intelligente.
Ottenete una lettura senior su trappole di budget, dipendenze e se conviene comprare, assemblare o costruire - prima che parta il lavoro a pagamento. Il CEO resta nel loop.