HandleEvent icon
Exit icon
Labels icon
Switch icon
Parallel icon
Get icon
ListIntents icon
ChatCompletion icon
GoogleGemini icon
MailSend icon
If icon
SlackIncomingWebhook icon
Log icon
Set icon
Webhook icon

Recover Failed Stripe Payments and Triage Disputes with AI

Verify Stripe webhooks, email AI-written recovery notes for failed invoices, and send dispute deadlines to Slack, with duplicate-safe processing.

Categories
AIBusiness

Failed card payments are the biggest source of involuntary churn for subscription businesses, and Stripe's generic dunning emails do not tell a customer what actually went wrong. Disputes are worse: each one has a hard evidence deadline, and missing it means losing the money and paying the fee.

This blueprint turns Stripe's webhooks into a small billing desk. Every request is verified against the endpoint's signing secret before anything runs, duplicate deliveries are ignored, and two event types get their own handling.

How it works

  1. stripe_webhook (io.kestra.plugin.core.trigger.Webhook) receives the event. fetchType: STORE keeps the body byte-for-byte, which signature verification requires.
  2. verify_event (io.kestra.plugin.stripe.webhook.HandleEvent) checks the Stripe-Signature header and fails the execution on a forged or replayed request.
  3. skip_duplicate_delivery (io.kestra.plugin.core.execution.Exit) ends the run when the same event id was already handled, using the KV store.
  4. label_execution tags the execution with the event type and id, so runs can be filtered in the UI.
  5. route_event (io.kestra.plugin.core.flow.Switch) branches on the event type:
    • invoice.payment_failed: fetches the customer and their latest PaymentIntents in parallel (io.kestra.plugin.stripe.customer.Get, io.kestra.plugin.stripe.payment.ListIntents), asks Gemini for a recovery email that explains the actual decline reason in the customer's preferred language, and sends it with a button to Stripe's hosted invoice page. When Stripe has stopped retrying, or the customer has no email address, escalate_if_unrecoverable (io.kestra.plugin.core.flow.If) posts a churn-risk alert to Slack.
    • charge.dispute.created: posts the amount, reason, response deadline and a reason-specific evidence checklist to Slack, with a link to the dispute in the Stripe Dashboard.
    • any other type is logged and ignored.
  6. mark_event_processed (io.kestra.plugin.core.kv.Set) records the event id for 7 days. It runs last, so an event whose run failed halfway is processed again when it is resent.
  7. alert_on_failure (errors) posts the first error to Slack, rejected signatures included.

Prerequisites

  • A Stripe account. Test mode is free and is enough to run everything.
  • The Stripe CLI to forward events to a local Kestra and to fire test events.
  • A Google Gemini API key from Google AI Studio.
  • A Slack incoming webhook.
  • An SMTP server. Mailpit works locally; any provider (SendGrid, Postmark, SES) works in production.

Secrets

  • STRIPE_API_KEY: a restricted or secret key (sk_test_...) with read access to customers and PaymentIntents.
  • STRIPE_WEBHOOK_SECRET: the endpoint signing secret (whsec_...), printed by stripe listen or shown on the endpoint in the Dashboard.
  • STRIPE_WEBHOOK_KEY: a long random string that becomes the last segment of the webhook URL.
  • GEMINI_API_KEY: Google AI Studio key used to draft the recovery email.
  • SLACK_WEBHOOK_URL: incoming webhook for the finance or billing channel.
  • SMTP_HOST, SMTP_PORT, SMTP_USERNAME, SMTP_PASSWORD: the mail server used to email customers.

Variables

  • company_name, billing_from_email, support_email: used in the email and its sender address.
  • gemini_model (default gemini-3.5-flash-lite): any Gemini model that supports JSON output.

Quick start

  1. Add the secrets above, then save the flow.
  2. Forward Stripe test events to Kestra: stripe listen --events invoice.payment_failed,charge.dispute.created --forward-to http://localhost:8080/api/v1/main/executions/webhook/company.billing/stripe-failed-payment-and-dispute-desk/<STRIPE_WEBHOOK_KEY> and use the whsec_... value it prints as STRIPE_WEBHOOK_SECRET.
  3. Fire a failed payment: stripe trigger invoice.payment_failed --override customer:email=you@example.com --override customer:name="Jenny Rosen". The recovery email lands in your SMTP inbox.
  4. Fire a dispute: stripe trigger charge.dispute.created. The alert lands in Slack.
  5. In production, create a webhook endpoint in the Stripe Dashboard pointing at the same path on your public Kestra URL, subscribed to the same two events.

Outputs

  • outputs.verify_event.id and outputs.verify_event.type: the verified Stripe event.
  • outputs.draft_recovery_email.jsonOutput.subject and .body: the email that was sent.
  • Execution labels stripe_event_type and stripe_event_id.
  • KV entry stripe-event-<event id> in the flow's namespace, kept for 7 days.

Pitfalls

  • The webhook trigger answers Stripe as soon as the execution is created, so Stripe does not retry when a task fails later. Fix the cause, then resend the event with stripe events resend <event id>.
  • The customer name and invoice line text are written by your customers and go into the prompt. The email cannot carry a link from the model, since the only link is the payment button added by the flow.
  • Subscribe the endpoint to these two event types only. HandleEvent 1.3.10 cannot convert some Stripe objects, such as the customer in customer.* events, so those end in the failure alert instead of the ignore branch.
  • Fields are read from Stripe's raw event JSON, so they follow your account's API version. Pin the webhook endpoint's version when you upgrade.

Links

Created by Keshav Agarwal.

See How

New to Kestra?

Use blueprints to kickstart your first workflows.