Stripe webhooks i PHP: en pålitlig hanterare du kan lita på
En Stripe webhook som fungerar på din bärbara dator är ännu inte en webhook du kan lita på med pengar. Den här guiden visar hur man verifierar signaturer, bekräftar snabba, deduplicerar händelser och hanterar återförsök i vanlig PHP och Laravel, med kod som du kan klistra in i ett projekt.
Kortfattat
- Behandla webhook, inte webbläsarens omdirigering, som sanningens källa för betalningsstatus.
- Läs den råa förfrågan och verifiera rubriken
Stripe-Signatureinnan du rör nyttolasten. - Skicka ett 2xx-svar snabbt och gör det verkliga arbetet i en kö eller efter att ha lagrat händelsen.
- Lagra händelse-ID med ett unikt index: Stripe kan leverera samma händelse mer än en gång och ur funktion.
- Testa med Stripe CLI (
stripe listenandstripe trigger) innan du går live.
Om du accepterar betalningar med Stripe är Stripe webhooks i PHP den del som avgör om beställningar markeras som betalda, prenumerationer aktiveras och återbetalningar återspeglas i din databas. Kassasidan är bara ytterdörren: kunder stänger flikar, banker bekräftar sent och vissa betalningsmetoder löser sig minuter eller dagar senare. Webhook är meddelandet från Stripe som säger vad som verkligen hände.
Vi bygger och underhåller betalningsintegrationer för kunder och för våra egna produkter (till exempel Bill, som tar Stripe och PayPal betalningar på en offentlig betalsida). Den här artikeln är checklistan vi följer när vi skriver en Stripe webhook endpoint i PHP.
Hur Stripe webhooks fungerar på en minut
Du registrerar en HTTPS URL i Stripe Dashboard (eller via API) och väljer vilka händelsetyper du vill ta emot. När något händer - en utcheckningssession slutförs, en faktura betalas, en avgift återbetalas - Stripe skickar en POST förfrågan med ett JSON Event- objekt till den URL:en. Din server måste svara med en 2xx-statuskod. Om den inte gör det, försöker Stripe igen med exponentiell backoff i upp till tre dagar i live-läge.
Tre fakta formar hela designen:
- Händelser kan komma mer än en gång. Omförsök och nätverksproblem gör dubbletter normala.
- Händelser kan komma ur funktion. Du kan se
invoice.paidinnancustomer.subscription.created. - Vem som helst kan POST till din URL. Utan signaturverifiering kan en angripare markera sin egen beställning som betald.
Steg 1. Installera SDK:n och skapa slutpunkten
Använd det officiella biblioteket, stripe/stripe-php, och håll slutpunkten hemlig (den börjar med whsec_) i en miljövariabel, aldrig i förvaret.
composer require stripe/stripe-php
# .env
STRIPE_SECRET_KEY=sk_live_...
STRIPE_WEBHOOK_SECRET=whsec_...
Varje webhook-slutpunkt i Dashboard har sin egen signeringshemlighet, och hemligheten från stripe listen under utveckling är annorlunda igen. En mycket vanlig "signaturverifiering misslyckades"-fel är helt enkelt att använda fel.
Steg 2. Verifiera signaturen på råkroppen
Stripe signerar varje begäran. Rubriken Stripe-Signature innehåller en tidsstämpel och en eller flera signaturer beräknade som en HMAC-SHA256 av timestamp.payload med din slutpunktshemlighet. Biblioteket kontrollerar signaturen och avvisar händelser äldre än fem minuter som standard.
Den viktiga detaljen: verifiering behöver de exakta råbyte Stripe skickade. Om ett ramverk analyserar JSON och du kodar om det, ändras blanksteg och signaturen matchar inte längre.
<?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;
}
I Laravel, läs $request->getContent() istället för $request->all(), och exkludera rutten från CSRF-skydd (webhooks bär inte en CSRF-token; signaturen är skyddet). Lägg rutten utanför web middleware-grupp om du kan.
Steg 3. Gör föraren idempotent
Idempotent betyder: att bearbeta samma händelse två gånger ger samma resultat som att bearbeta den en gång. Det enklaste tillförlitliga tillvägagångssättet är en tabell över bearbetade händelse-ID:n med ett unikt index. Sätt in först; om insättningen misslyckas på grund av att ID finns har du redan sett händelsen.
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;
}
Att lagra den råa nyttolasten ger dig ett spår: när en kund frågar "varför debiterades jag två gånger?", kan du spela upp exakt vad Stripe sa till dig. Det gör det också möjligt att ombearbeta misslyckade händelser efter att du åtgärdat en bugg.
Steg 4. Bekräfta snabbt, bearbeta separat
Stripe förväntar sig ett svar inom några sekunder. Om din hanterare skickar e-postmeddelanden, anropar andra API:er eller genererar PDF-filer inline, kommer det att ta slut, Stripe kommer att försöka igen och du kommer att göra samma arbete igen. Gör det minsta i begäran - verifiera, lagra, svara 200 - och kör den verkliga logiken i en köarbetare ( Laravel köer, Symfony Messenger, en cron-jobblässtatus status = 'received' eller en arbetsledare-hanterad arbetare).
Om händelsen inte kan lagras alls (databas nere), returnera en 5xx-status. Det är det enda fallet där du vill att Stripe ska försöka igen senare.
Steg 5. Hantera händelserna som betyder något
Du behöver inte alla typer av evenemang. Prenumerera endast på vad din affärslogik använder; färre abonnemang innebär mindre brus och mindre attackyta. En typisk uppsättning:
checkout.session.completed- kunden avslutade Checkout. Slå upp din beställning med hjälp avclient_reference_ideller metadata som du angav när du skapade sessionen.payment_intent.succeededochpayment_intent.payment_failed– för anpassade betalningsflöden.invoice.paidochinvoice.payment_failed- prenumerationsfakturering.customer.subscription.updatedochcustomer.subscription.deleted- planändringar och avbokningar.charge.refundedochcharge.dispute.created- pengar går tillbaka.
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;
}
Två vanor förhindrar de flesta buggar. Först, lita aldrig på belopp från webbläsaren: jämför det betalda beloppet och valutan i händelsen med beställningen i din databas. För det andra, eftersom händelser kan vara ur funktion, gör tillståndsändringar monotona: flytta inte en order från "betald" tillbaka till "väntande" eftersom en äldre händelse kom sent. Om du är osäker, hämta det aktuella objektet från API:et istället för att lita på händelsebilden.
Steg 6. Testa lokalt med Stripe CLI
Stripe CLI vidarebefordrar riktiga testlägeshändelser till din maskin, så att du inte behöver en offentlig tunnel:
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 skriver ut en tillfällig whsec_ secret. Lägg in det i din lokala .env. Testa också de olyckliga vägarna med flit: skicka en förfrågan med fel signatur (förvänta dig 400), skicka samma händelse två gånger (förvänta dig en databaseffekt) och stoppa din köarbetare i en minut för att se att händelser hopar sig istället för att försvinna.
Vanliga misstag vi ser vid revisioner
- Använder omdirigeringen som bevis på betalning. Sidan
success_urlkan öppnas av vem som helst; visa "bekräftar din betalning" tills webhook anländer. - Parsar JSON före verifiering. Omkodade kroppar bryter signaturen; framework body parsers är en frekvent bov.
- En hemlighet för test och live. Test- och liveläge har separata nycklar och separata webhook-slutpunkter.
- Ingen loggning. Utan lagrade händelser och en synlig felkanal hittas betalningsbuggar av kunder.
- Långsamma hanterare. Allt över några sekunder utlöser försök och dubbelarbete.
- Blocking Stripe vid brandväggen eller WAF. Om du använder Cloudflare eller annan WAF, tillåt webbhook-sökvägen och kontrollera att botreglerna inte returnerar 403 till Stripe servrar.
Övervakning: hur du vet att det fortsätter att fungera
En webhook kan gå sönder månader efter lanseringen: en utplacerad ändring byter namn på en kolumn, en hemlighet roteras, ett certifikat löper ut. Stripe Dashboard visar leveransförsök och misslyckanden per slutpunkt, och Stripe skickar e-post till dig när en slutpunkt fortsätter att misslyckas. På din sida, varna om tre saker: ett växande antal händelser i received eller failed, ett plötsligt fall i händelser per timme och eventuella 4xx- eller 5xx-hastigheter på webhook-rutten. Ett litet dagligt avstämningsjobb som jämför betalda beställningar i din databas med Stripe lista över lyckade betalningar fångar resten.
Hur vi kan hjälpa till
Vi designar och bygger produktionsbetalningar och webhook-integrationer med signaturkontroller, idempotens, omförsök och loggar som ditt team kan läsa. Om du redan har ett Stripe flöde och vill ha ett andra par ögon är en arkitekturgranskning eller konsultation det snabbaste sättet att hitta de svaga punkterna. För en bredare bild av att flytta kritiska händelser från Zapier eller Make, läs när ingen kod går sönder, och för en annan leverantör se vår guide till WayForPay integration i PHP.
Vanliga frågor
Varför misslyckas Stripe webhook signaturverifiering?
De vanliga orsakerna är att använda fel signeringshemlighet (test vs live, CLI vs Dashboard-slutpunkt), verifiering av en tolkad eller omkodad kropp istället för den råa begäranden, eller en proxy som modifierar nyttolasten. Verifiera mot exakta råbyte med slutpunktens egen whsec_ secret.
Hur förhindrar jag att en Stripe-händelse behandlas två gånger?
Lagra varje händelse-ID i en tabell med ett unikt index och infoga det innan du gör något arbete. Om insättningen misslyckas med en dubblettnyckel, returnera 200 och hoppa över bearbetningen. Gör också själva affärsåtgärden säker att upprepa.
Vilken svarskod ska en Stripe webhook returnera?
Returnera en 2xx-kod (200 eller 204) så snart händelsen har verifierats och lagrats. Returnera 400 för en ogiltig signatur och 5xx endast när du inte kunde lagra händelsen och vill att Stripe ska försöka igen.
Hur länge försöker Stripe igen misslyckade webhooks?
I live-läge försöker Stripe igen med exponentiell backoff i upp till tre dagar. I testläge är återförsöksfönstret mycket kortare, så lita inte på testlägesförsök för att bedöma produktionsbeteende.
Kan jag testa Stripe webhooks utan en offentlig URL?
Ja. Kör stripe listen --forward-to med Stripe CLI för att vidarebefordra testhändelser till localhost, och använd stripe trigger för att generera exempelhändelser.
Gratis upptäcktssamtal
Ta med en rörig stack eller en fastnad integration - Integrono kartlägger det smartaste första steget.
Ni får en senior bedömning av budgetfällor, beroenden och om ni ska köpa, limma eller bygga - innan betalt arbete startar. CEO stannar i loopen.