Zum Inhalt

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

  1. Gehen Sie zu https://dashboard.stripe.com
  2. Navigieren Sie zu EntwicklerWebhooks
  3. Klicken Sie auf "Endpunkt hinzufügen"

Webhook-URL konfigurieren

Produktions-Umgebung

https://ihre-domain.de/api/stripe/webhook

Beispiel:

https://hostadmin.example.com/api/stripe/webhook

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)

http://localhost:8000/api/stripe/webhook

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:

invoice.finalized
invoice.paid
invoice.payment_succeeded
invoice.payment_failed


Schritt 3: Webhook-Secret kopieren

Nach dem Erstellen des Endpunkts:

  1. Klicken Sie auf den neu erstellten Webhook-Endpunkt
  2. Im Bereich "Signing secret" finden Sie das Secret
  3. Klicken Sie auf "Reveal" oder auf das Kopiersymbol
  4. 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:

nano hostadmin/src/.env

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:

docker-compose down
docker-compose up -d


Schritt 5: Webhook testen

Test im Stripe Dashboard

  1. Gehen Sie zu EntwicklerWebhooks → Ihr Endpunkt
  2. Klicken Sie auf "Test-Webhook senden"
  3. Wählen Sie ein Event (z.B. invoice.paid)
  4. 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

# Laravel Logs
tail -f storage/logs/laravel.log

# Oder mit Pail (interaktiv)
php artisan pail

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:

[2024-01-15 10:30:45] local.ERROR: Invalid Stripe webhook signature {"error":"..."}

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:

brew install stripe/stripe-cli/stripe

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:

> Ready! Your webhook signing secret is whsec_xxxxxxxxxxxxxxxxxxxxx (^C to quit)

Kopieren Sie das angezeigte Secret und setzen Sie es in .env:

STRIPE_WEBHOOK_SECRET=whsec_xxxxxxxxxxxxxxxxxxxxx

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:

$event = Webhook::constructEvent(
    $payload,
    $sigHeader,
    $webhookSecret
);

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:

# Manuell ausführen
docker exec hostadmin-app php artisan stripe:sync-invoice-status

Siehe: Status-Abgleich Dokumentation


Troubleshooting

Webhook wird nicht empfangen

Problem: Events erscheinen nicht in den Logs

Lösungen:

  1. Endpunkt erreichbar prüfen:

    curl -X POST https://ihre-domain.de/api/stripe/webhook
    

  2. Firewall/Nginx-Konfiguration:

  3. Stelle sicher, dass /api/* nicht blockiert ist
  4. Prüfe nginx-Logs: docker-compose logs -f nginx

  5. Container läuft:

    docker-compose ps
    

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

# Secret prüfen
docker exec hostadmin-app grep STRIPE_WEBHOOK_SECRET .env

Erwarteter Output:

STRIPE_WEBHOOK_SECRET=whsec_1234567890abcdefghijklmnopqrstuvwxyz

❌ Falsch:

STRIPE_WEBHOOK_SECRET=whsec_your_webhook_secret_here
STRIPE_WEBHOOK_SECRET=

✅ 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:

docker exec hostadmin-app nano .env
# Ersetze mit echtem Secret
5. Cache leeren:
docker exec hostadmin-app php artisan config:clear

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:

docker exec hostadmin-app php artisan optimize:clear
docker-compose restart app

4. Container nicht neu gestartet

Problem: .env-Änderungen werden nicht übernommen

✅ Lösung:

docker exec hostadmin-app php artisan config:clear
docker-compose restart app

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:

docker exec hostadmin-app tail -f storage/logs/laravel.log | grep -A 5 "webhook"

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:

  1. Event-Typ unterstützt?
  2. Siehe Übersicht für unterstützte Events
  3. Unbekannte Events werden geloggt, aber nicht verarbeitet

  4. Daten fehlen:

  5. Kunde existiert in HostAdmin?
  6. Rechnung bereits importiert?

Monitoring

Webhook-Status in Stripe Dashboard

  1. Gehen Sie zu EntwicklerWebhooks
  2. Klicken Sie auf Ihren Endpunkt
  3. 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):

# Manuell ausführen
docker exec hostadmin-app php artisan stripe:sync-invoice-status

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:

  1. Webhook prüfen:

    # Logs checken
    docker exec hostadmin-app tail -f storage/logs/laravel.log | grep "subscription.updated"
    

  2. Event wird empfangen?

  3. Stripe Dashboard → Webhooks → Logs
  4. Sollte 200 OK zeigen

  5. Subscription gefunden?

    # Prüfe, ob Subscription mit stripe_subscription_id existiert
    docker exec hostadmin-app php artisan tinker
    >>> Subscription::where('stripe_subscription_id', 'sub_xxxxx')->first();
    

  6. CustomerServices verknüpft?

    >>> $sub = Subscription::find(42);
    >>> $sub->items()->count(); // Sollte > 0 sein
    

  7. Manuelle Synchronisation:

    docker exec hostadmin-app php artisan stripe:sync-invoice-status
    



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_SECRET in .env mit echtem Secret eingetragen
  • Secret beginnt mit whsec_ (nicht whsec_your_webhook_secret_here)
  • Test- und Live-Modus stimmen überein (alle Keys vom selben Modus)
  • Middleware-Ausnahmen in bootstrap/app.php vorhanden:
  • 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?

# Logs checken
docker exec hostadmin-app tail -f storage/logs/laravel.log | grep webhook

Noch immer Probleme? - Prüfe Signatur-Verifizierung fehlgeschlagen Sektion - Aktiviere Debug-Logging - Teste mit Stripe CLI