Stripe Webhook Einrichtung¶
Diese Anleitung zeigt, wie Sie Stripe Webhooks für HostAdmin einrichten. Webhooks ermöglichen es Stripe, HostAdmin automatisch über Events wie Zahlungen, Subscription-Änderungen und Rechnungsstatus zu informieren.
Übersicht¶
Webhook-Endpunkt: /api/stripe/webhook
Unterstützte Events:
| Event | Beschreibung | Handler |
|---|---|---|
checkout.session.completed |
Checkout abgeschlossen | Aktiviert Subscriptions |
customer.subscription.created |
Subscription erstellt | Synchronisiert Status |
customer.subscription.updated |
Subscription aktualisiert | Synchronisiert Status + next_billing_date |
customer.subscription.deleted |
Subscription gelöscht | Setzt Status auf "cancelled" |
invoice.finalized |
Rechnung finalisiert | Erstellt/aktualisiert Rechnung |
invoice.paid |
Rechnung bezahlt | Setzt Status auf "paid" |
invoice.payment_succeeded |
Zahlung erfolgreich | Erstellt Payment-Record |
invoice.payment_failed |
Zahlung fehlgeschlagen | Setzt Status auf "overdue" |
Schritt 1: Webhook-Endpunkt in Stripe anlegen¶
Stripe Dashboard öffnen¶
- Gehen Sie zu https://dashboard.stripe.com
- Navigieren Sie zu Entwickler → Webhooks
- Klicken Sie auf "Endpunkt hinzufügen"
Webhook-URL konfigurieren¶
Produktions-Umgebung¶
Beispiel:
HTTPS erforderlich
Für Produktions-Umgebungen muss die URL mit HTTPS beginnen. Stripe sendet keine Webhooks an unsichere HTTP-Endpunkte im Live-Modus.
Test-Umgebung (Docker lokal)¶
Stripe CLI für lokale Tests
Für lokale Entwicklung empfehlen wir die Stripe CLI, um Webhooks an localhost zu forwarden.
Schritt 2: Events auswählen¶
Option 1: Alle Events (empfohlen)¶
Wählen Sie "Alle Events empfangen" aus. Dies stellt sicher, dass zukünftige Features automatisch funktionieren.
Option 2: Spezifische Events¶
Wenn Sie nur spezifische Events benötigen, wählen Sie folgende aus:
Checkout & Subscriptions:
checkout.session.completed
customer.subscription.created
customer.subscription.updated
customer.subscription.deleted
Rechnungen & Zahlungen:
Schritt 3: Webhook-Secret kopieren¶
Nach dem Erstellen des Endpunkts:
- Klicken Sie auf den neu erstellten Webhook-Endpunkt
- Im Bereich "Signing secret" finden Sie das Secret
- Klicken Sie auf "Reveal" oder auf das Kopiersymbol
- Das Secret beginnt mit
whsec_...
Secret geheim halten
Das Webhook-Secret ist wie ein Passwort. Teilen Sie es niemals öffentlich und committen Sie es nicht in Git.
Schritt 4: Webhook-Secret in .env konfigurieren¶
Lokale Installation¶
Öffnen Sie die .env-Datei im src/-Verzeichnis:
Fügen Sie die Stripe-Konfiguration hinzu:
# Stripe Configuration
STRIPE_KEY=pk_test_xxxxxxxxxxxxxxxxxxxxx
STRIPE_SECRET=sk_test_xxxxxxxxxxxxxxxxxxxxx
STRIPE_WEBHOOK_SECRET=whsec_xxxxxxxxxxxxxxxxxxxxx
Docker-Installation¶
Methode 1: .env-Datei bearbeiten¶
# Container-Terminal öffnen
docker exec -it hostadmin-app bash
# .env bearbeiten
nano /var/www/html/.env
# Nach dem Speichern Container neu starten
exit
docker-compose restart app
Methode 2: docker-compose.yml verwenden¶
Alternativ können Sie die Umgebungsvariablen in docker-compose.yml setzen:
services:
app:
environment:
- STRIPE_KEY=${STRIPE_KEY}
- STRIPE_SECRET=${STRIPE_SECRET}
- STRIPE_WEBHOOK_SECRET=${STRIPE_WEBHOOK_SECRET}
Dann in .env (Root-Verzeichnis) eintragen und Container neu starten:
Schritt 5: Webhook testen¶
Test im Stripe Dashboard¶
- Gehen Sie zu Entwickler → Webhooks → Ihr Endpunkt
- Klicken Sie auf "Test-Webhook senden"
- Wählen Sie ein Event (z.B.
invoice.paid) - Klicken Sie auf "Senden"
Logs prüfen¶
Docker-Installation¶
# App-Container Logs
docker-compose logs -f app
# Queue-Worker Logs (falls Events in Queue)
docker-compose logs -f queue
Lokale Installation¶
Erfolgreiche Webhook-Logs¶
Sie sollten folgende Einträge sehen:
[2024-01-15 10:30:45] local.INFO: Stripe webhook received {"type":"invoice.paid","id":"evt_xxxxx"}
[2024-01-15 10:30:45] local.INFO: Invoice marked as paid in Stripe {"stripe_invoice_id":"in_xxxxx","amount_paid":29.99}
[2024-01-15 10:30:45] local.INFO: Invoice status updated to paid {"invoice_id":123,"invoice_number":"RE-1001"}
Fehlerhafte Webhooks¶
Bei Fehlern sehen Sie:
Lösungen:
- ✅ Prüfen Sie, dass STRIPE_WEBHOOK_SECRET korrekt gesetzt ist
- ✅ Stellen Sie sicher, dass das Secret vom richtigen Modus (Test/Live) stammt
- ✅ Container neu starten nach .env-Änderungen
Test vs. Live Modus¶
Test-Modus (Entwicklung)¶
STRIPE_KEY=pk_test_xxxxxxxxxxxxxxxxxxxxx
STRIPE_SECRET=sk_test_xxxxxxxxxxxxxxxxxxxxx
STRIPE_WEBHOOK_SECRET=whsec_test_xxxxxxxxxxxxxxxxxxxxx
- ✅ HTTP-Endpunkte erlaubt
- ✅ Keine echten Zahlungen
- ✅ Test-Kreditkarten (z.B.
4242 4242 4242 4242)
Live-Modus (Produktion)¶
STRIPE_KEY=pk_live_xxxxxxxxxxxxxxxxxxxxx
STRIPE_SECRET=sk_live_xxxxxxxxxxxxxxxxxxxxx
STRIPE_WEBHOOK_SECRET=whsec_xxxxxxxxxxxxxxxxxxxxx
- ⚠️ Nur HTTPS-Endpunkte
- ⚠️ Echte Zahlungen
- ⚠️ Separater Webhook-Endpunkt erforderlich
Wichtig
Test- und Live-Modus haben separate Webhook-Secrets. Sie können nicht das Secret aus dem Test-Modus für Live-Webhooks verwenden!
Stripe CLI für lokale Entwicklung¶
Die Stripe CLI ermöglicht es, Webhooks an localhost zu forwarden, ohne einen öffentlichen Endpunkt zu benötigen.
Installation¶
Windows:
# Via Scoop
scoop bucket add stripe https://github.com/stripe/scoop-stripe-cli.git
scoop install stripe
# Oder Download von: https://github.com/stripe/stripe-cli/releases
macOS:
Linux:
# Via apt
curl -s https://packages.stripe.dev/api/security/keypair/stripe-cli-gpg/public | gpg --dearmor | sudo tee /usr/share/keyrings/stripe.gpg
echo "deb [signed-by=/usr/share/keyrings/stripe.gpg] https://packages.stripe.dev/stripe-cli-debian-local stable main" | sudo tee -a /etc/apt/sources.list.d/stripe.list
sudo apt update
sudo apt install stripe
Verwendung¶
# Anmelden
stripe login
# Webhooks an localhost forwarden
stripe listen --forward-to http://localhost:8000/api/stripe/webhook
Output:
Kopieren Sie das angezeigte Secret und setzen Sie es in .env:
Events triggern¶
In einem zweiten Terminal:
# Test-Event senden
stripe trigger invoice.paid
# Spezifisches Event mit Daten
stripe trigger checkout.session.completed
Webhook-Sicherheit¶
Signatur-Verifizierung¶
Der StripeWebhookController verifiziert automatisch alle eingehenden Webhooks:
Was wird geprüft:
- ✅ Webhook-Signatur (Stripe-Signature Header)
- ✅ Zeitstempel (max. 5 Minuten alt)
- ✅ Event-Integrität (Payload wurde nicht manipuliert)
Keine zusätzliche Authentifizierung erforderlich¶
Die Webhook-Route ist öffentlich (kein API-Token erforderlich), aber durch die Signatur-Verifizierung geschützt:
// routes/api.php
Route::post('stripe/webhook', [StripeWebhookController::class, 'handle'])
->name('stripe.webhook');
Nur Webhooks mit gültiger Stripe-Signatur werden akzeptiert.
Webhook-Retry-Logik¶
Stripe wiederholt fehlgeschlagene Webhooks automatisch:
| Versuch | Zeitpunkt |
|---|---|
| 1 | Sofort |
| 2 | Nach 1 Stunde |
| 3 | Nach 6 Stunden |
| 4 | Nach 24 Stunden |
| 5 | Nach 3 Tagen |
Nach 5 fehlgeschlagenen Versuchen gibt Stripe auf.
Best Practice: Auch wenn Webhooks fehlschlagen, läuft der tägliche Status-Sync (03:40 Uhr) als Fallback:
Siehe: Status-Abgleich Dokumentation
Troubleshooting¶
Webhook wird nicht empfangen¶
Problem: Events erscheinen nicht in den Logs
Lösungen:
-
Endpunkt erreichbar prüfen:
-
Firewall/Nginx-Konfiguration:
- Stelle sicher, dass
/api/*nicht blockiert ist -
Prüfe nginx-Logs:
docker-compose logs -f nginx -
Container läuft:
Signatur-Verifizierung fehlgeschlagen¶
Problem: {"error": "Invalid signature"} (HTTP 400)
Dies ist der häufigste Fehler bei der Webhook-Einrichtung. Die Signatur-Verifizierung schlägt aus folgenden Gründen fehl:
1. Webhook-Secret ist falsch oder fehlt¶
Symptom: Secret ist auf Platzhalter-Wert oder leer
Erwarteter Output:
❌ Falsch:
✅ Lösung:
1. Gehe zu Stripe Dashboard → Webhooks → Dein Endpunkt
2. Klicke auf "Signing secret" → "Reveal"
3. Kopiere den echten Secret (nicht den Platzhalter!)
4. Setze in .env:
2. Test- vs. Live-Modus Mismatch¶
Problem: Test-Secret wird für Live-Webhooks verwendet (oder umgekehrt)
✅ Lösung:
- Test-Keys (pk_test_*, sk_test_*) benötigen Test-Webhook-Secret
- Live-Keys (pk_live_*, sk_live_*) benötigen Live-Webhook-Secret
- Prüfe, dass alle drei Werte zum selben Modus gehören
3. Middleware modifiziert Request Body¶
Problem: Laravel-Middleware verändert den Request-Body vor der Signatur-Verifizierung
Stripe benötigt den RAW Request Body exakt wie gesendet. Wenn Middleware wie TrimStrings oder ConvertEmptyStringsToNull den Body ändern, schlägt die Signatur-Verifizierung fehl.
✅ Lösung: (Bereits in HostAdmin v2.0+ implementiert)
Die Webhook-Route ist in bootstrap/app.php von diesen Middleware ausgenommen:
->withMiddleware(function (Middleware $middleware): void {
// Exclude Stripe webhook from CSRF verification
$middleware->validateCsrfTokens(except: [
'api/stripe/webhook',
]);
// Exclude Stripe webhook from middleware that modifies request body
// (Stripe signature verification requires the RAW body)
$middleware->trimStrings(except: [
'api/stripe/webhook',
]);
$middleware->convertEmptyStringsToNull(except: [
fn ($request) => $request->is('api/stripe/webhook'),
]);
})
Wenn du eine ältere Version verwendest:
Aktualisiere deine bootstrap/app.php mit dem Code oben und führe aus:
4. Container nicht neu gestartet¶
Problem: .env-Änderungen werden nicht übernommen
✅ Lösung:
5. Reverse Proxy modifiziert Request¶
Problem: Cloudflare, nginx oder anderer Proxy ändert den Request-Body
✅ Lösung:
- Stelle sicher, dass der Reverse Proxy den Body unverändert weiterleitet
- Für nginx: proxy_pass_request_body on;
- Für Cloudflare: "Transform Rules" deaktivieren für /api/stripe/webhook
Debugging-Tipps für Signatur-Fehler¶
1. Aktiviere ausführliches Logging (temporär):
Öffne app/Http/Controllers/Api/StripeWebhookController.php und füge vor der Signatur-Verifizierung hinzu:
Log::info('Stripe webhook debug', [
'has_signature' => !empty($sigHeader),
'signature_start' => substr($sigHeader, 0, 20) . '...',
'secret_configured' => !empty($webhookSecret),
'secret_start' => $webhookSecret ? substr($webhookSecret, 0, 10) . '...' : 'MISSING',
'body_length' => strlen($payload),
]);
2. Prüfe Logs:
3. Test mit Stripe CLI:
# Lokales Forwarding (gibt dir einen Test-Secret)
stripe listen --forward-to http://localhost:8000/api/stripe/webhook
# Verwende den angezeigten Secret in .env
STRIPE_WEBHOOK_SECRET=whsec_xxxxxxxxxxxxx
4. Manueller Request-Test:
# Test, ob Endpunkt erreichbar ist
curl -X POST https://ihre-domain.de/api/stripe/webhook \
-H "Content-Type: application/json" \
-d '{"test": true}'
# Erwartete Antwort (ohne Signatur):
# {"error": "Invalid signature"}
Das ist korrekt - ohne gültige Stripe-Signatur wird abgelehnt!
Webhook wird empfangen, aber nicht verarbeitet¶
Problem: Logs zeigen Unhandled Stripe webhook event
Lösungen:
- Event-Typ unterstützt?
- Siehe Übersicht für unterstützte Events
-
Unbekannte Events werden geloggt, aber nicht verarbeitet
-
Daten fehlen:
- Kunde existiert in HostAdmin?
- Rechnung bereits importiert?
Monitoring¶
Webhook-Status in Stripe Dashboard¶
- Gehen Sie zu Entwickler → Webhooks
- Klicken Sie auf Ihren Endpunkt
- Tab "Logs" zeigt alle gesendeten Webhooks
Status-Codes:
- ✅ 200 OK - Erfolgreich verarbeitet
- ❌ 400 Bad Request - Ungültige Signatur
- ❌ 500 Server Error - Interner Fehler
Queue-Jobs überwachen¶
Falls Webhooks in die Queue gehen:
# Queue-Status
docker exec hostadmin-app php artisan queue:work --once
# Failed Jobs
docker exec hostadmin-app php artisan queue:failed
# Failed Job retry
docker exec hostadmin-app php artisan queue:retry <job-id>
Automatische Synchronisation: next_billing_date¶
Problem¶
Wenn Stripe eine Rechnung für ein Abonnement generiert, wird das next_billing_date automatisch in Stripe aktualisiert (z.B. von 06.12.2025 auf 06.01.2026). Ohne Webhook-Synchronisation bleibt das lokale next_billing_date in HostAdmin jedoch auf dem alten Wert stehen.
Beispiel:
- Lokal (HostAdmin): next_billing_date = 06.12.2025
- Stripe Subscription: current_period_end = 06.01.2026 ✅
Dies führt zu Inkonsistenzen zwischen HostAdmin und Stripe.
Lösung: Webhook-basierte Synchronisation¶
Der customer.subscription.updated Webhook synchronisiert automatisch das next_billing_date:
Stripe generiert Rechnung (06.12.2025)
↓
Zahlung erfolgreich
↓
customer.subscription.updated webhook
↓
HostAdmin aktualisiert:
- Subscription.next_billing_date → 06.01.2026
- CustomerService.next_billing_date → 06.01.2026 (für alle Services in Abo)
Was wird synchronisiert?¶
1. Subscription-Ebene:
Subscription::update([
'next_billing_date' => Carbon::createFromTimestamp($stripeSubscription->current_period_end),
'stripe_synced_at' => now()
]);
2. CustomerService-Ebene:
Alle CustomerServices, die Teil der Subscription sind, erhalten das gleiche next_billing_date:
foreach ($subscription->items as $item) {
$item->customerService->update([
'next_billing_date' => $nextBillingDate
]);
}
Wann wird synchronisiert?¶
Der Webhook customer.subscription.updated wird in folgenden Fällen getriggert:
| Trigger | Beschreibung |
|---|---|
| Rechnung generiert | Stripe erstellt periodische Rechnung → current_period_end verschiebt sich |
| Billing Cycle geändert | Manuell oder durch Admin-Aktion |
| Subscription aktualisiert | Items hinzugefügt/entfernt, Preise geändert |
| Trial endet | Übergang von Trial zu aktiver Subscription |
Logs überprüfen¶
Nach erfolgreicher Synchronisation erscheinen folgende Log-Einträge:
[2025-01-06 03:00:00] local.INFO: Subscription updated in Stripe
{
"stripe_subscription_id": "sub_xxxxx",
"status": "active",
"current_period_end": 1736121600
}
[2025-01-06 03:00:00] local.INFO: Updating subscription next_billing_date from Stripe
{
"subscription_id": 42,
"stripe_subscription_id": "sub_xxxxx",
"next_billing_date": "2026-01-06"
}
[2025-01-06 03:00:00] local.INFO: Updated CustomerService next_billing_date
{
"customer_service_id": 74,
"service_number": "SVC-00074",
"next_billing_date": "2026-01-06"
}
Fallback: Manuelle Synchronisation¶
Falls Webhooks fehlschlagen oder nicht eingerichtet sind, nutzt HostAdmin einen täglichen Fallback-Job (03:40 Uhr):
Dieser prüft alle Subscriptions in Stripe und synchronisiert: - Invoice Status - Subscription Status - next_billing_date ✅
Best Practice: Webhooks sind die bevorzugte Methode (Echtzeit), der Fallback-Job ist nur eine Sicherheitsmaßnahme.
Troubleshooting¶
Problem: next_billing_date wird nicht aktualisiert
Lösungen:
-
Webhook prüfen:
-
Event wird empfangen?
- Stripe Dashboard → Webhooks → Logs
-
Sollte
200 OKzeigen -
Subscription gefunden?
-
CustomerServices verknüpft?
-
Manuelle Synchronisation:
Weiterführende Links¶
Checkliste¶
Verwenden Sie diese Checkliste, um die Webhook-Einrichtung zu überprüfen:
Stripe Dashboard¶
- Webhook-Endpunkt in Stripe Dashboard erstellt
- Webhook-URL korrekt konfiguriert (HTTPS für Production)
- Events ausgewählt (alle oder spezifische)
- Webhook-Secret kopiert (nicht Platzhalter!)
HostAdmin Konfiguration¶
-
STRIPE_WEBHOOK_SECRETin.envmit echtem Secret eingetragen - Secret beginnt mit
whsec_(nichtwhsec_your_webhook_secret_here) - Test- und Live-Modus stimmen überein (alle Keys vom selben Modus)
- Middleware-Ausnahmen in
bootstrap/app.phpvorhanden: -
validateCsrfTokens(except: ['api/stripe/webhook']) -
trimStrings(except: ['api/stripe/webhook']) -
convertEmptyStringsToNull(except: [...])
Deployment¶
- Cache geleert:
php artisan config:clear - Container/Server neu gestartet
- Endpunkt erreichbar (curl-Test durchgeführt)
Testing & Monitoring¶
- Test-Webhook von Stripe gesendet
- HTTP 200 Response erhalten (nicht 400/500)
- Logs überprüft (keine "Invalid signature" Fehler)
- Event wurde verarbeitet (Log-Eintrag: "Stripe webhook received")
- Status-Sync Job läuft täglich (03:40 Uhr)
- Monitoring eingerichtet (Stripe Dashboard → Webhooks → Logs)
🎉 Webhooks erfolgreich eingerichtet!
Schnelle Fehlerbehebung¶
Webhook wird mit 400 abgelehnt?
# 1. Secret prüfen
docker exec hostadmin-app grep STRIPE_WEBHOOK_SECRET .env
# 2. Ist es der echte Secret? (nicht Platzhalter)
# Erwartung: whsec_[lange Zeichenkette]
# 3. Cache leeren
docker exec hostadmin-app php artisan config:clear
# 4. Neu starten
docker-compose restart app
# 5. Erneut testen
Webhook wird empfangen, aber nicht verarbeitet?
Noch immer Probleme? - Prüfe Signatur-Verifizierung fehlgeschlagen Sektion - Aktiviere Debug-Logging - Teste mit Stripe CLI