Stripe webhooks in PHP: een betrouwbare handler waarop u kunt vertrouwen
Een Stripe webhook die op uw laptop werkt, is nog geen webhook waarop u met geld kunt vertrouwen. Deze handleiding laat zien hoe u handtekeningen kunt verifiëren, snel kunt erkennen, gebeurtenissen kunt ontdubbelen en nieuwe pogingen kunt afhandelen in gewone PHP en Laravel, met code die u in een project kunt plakken.
In het kort
- Behandel de webhook, en niet de browseromleiding, als de bron van waarheid voor de betalingsstatus.
- Lees de onbewerkte hoofdtekst van het verzoek en verifieer de
Stripe-Signatureheader voordat u de payload aanraakt. - Retourneer snel een 2xx-antwoord en doe het echte werk in een wachtrij of na het opslaan van de gebeurtenis.
- Sla de gebeurtenis-ID op met een unieke index: Stripe kan dezelfde gebeurtenis meer dan één keer en buiten de juiste volgorde leveren.
- Test met de Stripe CLI (
stripe listenenstripe trigger) voordat u live gaat.
Als u betalingen met Stripe accepteert, zijn Stripe webhooks in PHP het onderdeel dat beslist of bestellingen als betaald worden gemarkeerd, abonnementen worden geactiveerd en terugbetalingen in uw database worden weergegeven. De afrekenpagina is slechts de voordeur: klanten sluiten hun rekeningen, banken bevestigen te laat en sommige betaalmethoden worden minuten of dagen later afgehandeld. De webhook is het bericht van Stripe dat zegt wat er werkelijk is gebeurd.
We bouwen en onderhouden betalingsintegraties voor klanten en voor onze eigen producten (bijvoorbeeld Bill, dat Stripe en PayPal betalingen op een openbare betaalpagina verwerkt). Dit artikel is de checklist die we volgen als we een Stripe webhook-eindpunt in PHP schrijven.
Hoe Stripe-webhooks in één minuut werken
U registreert een HTTPS-URL in het Stripe Dashboard (of via de API) en kiest welke gebeurtenistypen u wilt ontvangen. Wanneer er iets gebeurt - een Checkout-sessie is voltooid, een factuur wordt betaald, een bedrag wordt terugbetaald - stuurt Stripe een POST verzoek met een JSON Event- object naar die URL. Uw server moet antwoorden met een 2xx-statuscode. Als dit niet het geval is, probeert Stripe het opnieuw met exponentieel uitstel gedurende maximaal drie dagen in de livemodus.
Drie feiten bepalen het hele ontwerp:
- Gebeurtenissen kunnen meerdere keren voorkomen. Nieuwe pogingen en netwerkproblemen maken duplicaten normaal.
- Evenementen kunnen in de verkeerde volgorde arriveren. Het kan zijn dat u
invoice.paidziet vóórcustomer.subscription.created. - Iedereen kan POSTen naar jouw URL. Zonder handtekeningverificatie kan een aanvaller zijn eigen bestelling als betaald markeren.
Stap 1. Installeer de SDK en maak het eindpunt
Gebruik de officiële bibliotheek stripe/stripe-php en houd het eindpunt geheim (het begint met whsec_) in een omgevingsvariabele, nooit in de repository.
composer require stripe/stripe-php
# .env
STRIPE_SECRET_KEY=sk_live_...
STRIPE_WEBHOOK_SECRET=whsec_...
Elk webhook-eindpunt in het Dashboard heeft zijn eigen ondertekeningsgeheim, en het geheim van stripe listen tijdens de ontwikkeling is weer anders. Een veel voorkomende fout "handtekeningverificatie mislukt" is simpelweg het gebruik van de verkeerde.
Stap 2. Controleer de handtekening op het onbewerkte lichaam
Stripe tekent elk verzoek. De Stripe-Signature header bevat een tijdstempel en een of meer handtekeningen berekend als een HMAC-SHA256 van timestamp.payload met uw eindpuntgeheim. De bibliotheek controleert de handtekening en weigert standaard gebeurtenissen ouder dan vijf minuten.
Het belangrijke detail: voor verificatie zijn de exacte onbewerkte bytes nodig Stripe heeft verzonden. Als een raamwerk de JSON parseert en u deze opnieuw codeert, verandert de witruimte en komt de handtekening niet meer overeen.
<?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;
}
Lees in Laravel $request->getContent() in plaats van $request->all() en sluit de route uit van CSRF-bescherming (webhooks dragen geen CSRF-token; de handtekening is de bescherming). Plaats de route indien mogelijk buiten de middlewaregroep van de web.
Stap 3. Maak de handler idempotent
Idempotent betekent: twee keer dezelfde gebeurtenis verwerken geeft hetzelfde resultaat als één keer verwerken. De eenvoudigste betrouwbare aanpak is een tabel met verwerkte gebeurtenis-ID's met een unieke index. Eerst invoegen; als het invoegen mislukt omdat de ID bestaat, hebt u de gebeurtenis al gezien.
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;
}
Door de ruwe payload op te slaan, krijgt u een audittrail: wanneer een klant vraagt: "Waarom zijn er twee keer kosten in rekening gebracht?", kunt u precies herhalen wat Stripe u heeft verteld. Het maakt het ook mogelijk om mislukte gebeurtenissen opnieuw te verwerken nadat u een bug hebt opgelost.
Stap 4. Snel erkennen, afzonderlijk verwerken
Stripe verwacht binnen enkele seconden een reactie. Als uw handler e-mails verzendt, andere API's aanroept of inline PDF's genereert, treedt er een time-out op, probeert Stripe het opnieuw en doet u hetzelfde werk opnieuw. Doe het minimum in het verzoek - verifiëren, opslaan, reageren 200 - en voer de echte logica uit in een wachtrijwerker ( Laravel wachtrijen, Symfony Messenger, een cron-job-leesstatus status = 'received' of een door een supervisor beheerde medewerker).
Als de gebeurtenis helemaal niet kan worden opgeslagen (database uitgeschakeld), retourneert u een 5xx-status. Dat is het enige geval waarin je wilt dat Stripe het later opnieuw probeert.
Stap 5. Behandel de gebeurtenissen die er toe doen
Je hebt niet elk evenementtype nodig. Abonneer u alleen op wat uw bedrijfslogica gebruikt; minder abonnementen betekent minder ruis en een kleiner aanvalsoppervlak. Een typisch setje:
checkout.session.completed- de klant heeft het afrekenen voltooid. Zoek uw bestelling op met behulp van declient_reference_idof metagegevens die u heeft ingesteld bij het maken van de sessie.payment_intent.succeededenpayment_intent.payment_failed- voor aangepaste betalingsstromen.invoice.paideninvoice.payment_failed- facturering van abonnementen.customer.subscription.updatedencustomer.subscription.deleted- planwijzigingen en annuleringen.charge.refundedencharge.dispute.created- geld gaat terug.
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;
}
Twee gewoonten voorkomen de meeste bugs. Vertrouw ten eerste nooit bedragen uit de browser: vergelijk het betaalde bedrag en de valuta in het evenement met de bestelling in uw database. Ten tweede, omdat gebeurtenissen niet in de juiste volgorde kunnen plaatsvinden, kunnen statuswijzigingen monotoon zijn: verplaats een bestelling niet van 'betaald' naar 'in behandeling' omdat een oudere gebeurtenis te laat is aangekomen. Haal bij twijfel het huidige object op uit de API in plaats van te vertrouwen op de momentopname van de gebeurtenis.
Stap 6. Test lokaal met de Stripe CLI
De Stripe CLI stuurt echte testmodusgebeurtenissen door naar uw machine, zodat u geen openbare tunnel nodig heeft:
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 drukt een tijdelijk whsec_ af. Zet dat in uw lokale .env. Test ook expres de ongelukkige paden: stuur een verzoek met een verkeerde handtekening (verwacht 400), stuur dezelfde gebeurtenis tweemaal (verwacht één database-effect) en stop uw wachtrijwerker een minuutje om te zien dat gebeurtenissen zich opstapelen in plaats van verdwijnen.
Veel voorkomende fouten die we tegenkomen bij audits
- Gebruik de omleiding als betalingsbewijs. De
success_urlpagina kan door iedereen worden geopend; toon "uw betaling bevestigen" totdat de webhook arriveert. - JSON parseren vóór verificatie. Opnieuw gecodeerde lichamen breken de handtekening; Framework-lichaamsparsers zijn een veelvoorkomende boosdoener.
- Eén geheim voor testen en live. De test- en livemodus hebben afzonderlijke sleutels en afzonderlijke webhookeindpunten.
- Geen logboekregistratie. Zonder opgeslagen gebeurtenissen en een zichtbaar storingskanaal worden betalingsbugs door klanten gevonden.
- Langzame behandelaars. Alles langer dan een paar seconden leidt tot nieuwe pogingen en dubbel werk.
- Stripe blokkeren bij de firewall of WAF. Als u Cloudflare of een andere WAF gebruikt, sta dan het webhookpad toe en controleer of botregels geen 403 retourneren naar de servers van Stripe.
Monitoring: hoe je weet dat het blijft werken
Een webhook kan maanden na de lancering kapot gaan: een geïmplementeerde wijziging hernoemt een kolom, een geheim wordt gerouleerd, een certificaat verloopt. Het Stripe Dashboard toont bezorgpogingen en -fouten per eindpunt, en Stripe e-mailt u wanneer een eindpunt blijft mislukken. Aan uw kant moet u op drie dingen letten: een groeiend aantal received of failed gebeurtenissen, een plotselinge daling van het aantal gebeurtenissen per uur en een eventueel 4xx- of 5xx-tarief op de webhook-route. Een kleine dagelijkse afstemmingstaak die betaalde bestellingen in uw database vergelijkt met de lijst met succesvolle betalingen van Stripe, vangt de rest op.
Hoe wij kunnen helpen
We ontwerpen en bouwen productiebetalingen en webhook-integraties met handtekeningcontroles, idempotentie, nieuwe pogingen en logboeken die uw team kan lezen. Als u al een Stripe-flow heeft en een tweede paar ogen wilt, is een architectuurbeoordeling of -advies de snelste manier om de zwakke plekken te vinden. Voor het bredere beeld van het verplaatsen van kritieke gebeurtenissen uit Zapier of Make, lees wanneer no-code breaks, en voor een andere provider zie onze gids voor WayForPay integratie in PHP.
Veelgestelde vragen
Waarom mislukt de verificatie van Stripe webhookhandtekening?
De gebruikelijke oorzaken zijn het gebruik van het verkeerde ondertekeningsgeheim (test versus live, CLI versus Dashboard-eindpunt), het verifiëren van een geparseerde of opnieuw gecodeerde hoofdtekst in plaats van de onbewerkte aanvraagtekst, of een proxy die de payload wijzigt. Verifieer aan de hand van de exacte onbewerkte bytes met het eigen whsec_secret van het eindpunt.
Hoe voorkom ik dat een Stripe gebeurtenis tweemaal wordt verwerkt?
Bewaar elke gebeurtenis-ID in een tabel met een unieke index en voeg deze in voordat u enig werk gaat doen. Als het invoegen mislukt met een dubbele sleutel, retourneert u 200 en slaat u de verwerking over. Zorg er ook voor dat de zakelijke actie zelf veilig kan worden herhaald.
Welke responscode moet een Stripe webhook retourneren?
Retourneer een 2xx-code (200 of 204) zodra de gebeurtenis is geverifieerd en opgeslagen. Retourneer 400 voor een ongeldige handtekening en 5xx alleen als u de gebeurtenis niet kunt opslaan en wilt dat Stripe het opnieuw probeert.
Hoe lang probeert Stripe mislukte webhooks opnieuw?
In de live-modus probeert Stripe het opnieuw met exponentieel uitstel gedurende maximaal drie dagen. In de testmodus is de periode voor nieuwe pogingen veel korter, dus vertrouw niet op nieuwe pogingen in de testmodus om het productiegedrag te beoordelen.
Kan ik Stripe webhooks testen zonder een openbare URL?
Ja. Voer stripe listen --forward-to uit met de Stripe CLI om testgebeurtenissen door te sturen naar localhost, en gebruik stripe trigger om voorbeeldgebeurtenissen te genereren.
Gratis ontdekkingsgesprek
Breng een rommelige stack of vastgelopen integratie - Integrono schetst de slimste eerste zet.
U krijgt een senior blik op budgetvallen, afhankelijkheden en of u moet kopen, lijmen of bouwen - voordat betaald werk start. De CEO blijft in the loop.