Send Stripe failed payment and receipt emails

Stripe tells you when a payment succeeds or fails. Here are three ways to turn those events into emails, when each one fits, and how to set up the Loops version.

Three ways to send payment emails

Stripe tells you when a payment succeeds or fails. You can let Stripe send the email, send it from a Loops workflow, or send a transactional email from your own server. The right choice depends on whether every customer must get it.

Option 1: Stripe’s built-in emails

Stripe can send receipts and failed payment notices itself, from your Stripe Dashboard settings. It is the fastest start. It sends a notice after each failed payment and uses your Stripe branding, but you can’t change the copy or add your own follow-ups.

Option 2: The Loops Stripe app and workflows

Connect Stripe to Loops, then send branded emails from workflows triggered by Stripe events such as invoice.payment_failed and invoice.paid. No code is needed. Workflow emails respect unsubscribes, so a customer who unsubscribed from your marketing email will not get them.

Option 3: A Stripe webhook that calls the Loops API

Your server receives the Stripe event and sends a transactional email through Loops. Transactional email reaches unsubscribed contacts too, so use this route for receipts and payment notices that every customer must get. You write and host the webhook handler.

Set up failed payment emails with the Loops Stripe app

  • In Loops, open Settings, then Stripe, and click Connect to Stripe.

  • Follow Stripe’s install steps. You return to Loops when it is done.

  • Turn on invoice.payment_failed, plus invoice.paid if you want receipts, then click Save.

  • Loops matches each Stripe customer to a contact by email address and creates the contact if it is new.

  • Create a workflow from the Stripe - Payment Failed template.

  • Set the trigger to Event received, then Stripe, then invoice.payment_failed.

  • Write the email: which payment failed, where the customer updates their card, and when you will try again.

Test with the Stripe CLI

Send a test event with the Stripe CLI. Add an email address, because Loops ignores Stripe events without one.

stripe trigger invoice.payment_failed --add customer:[email protected]

Then open the Events page in Loops to confirm the event arrived and the workflow ran.

Turn one email into a dunning sequence

A single failed payment email is easy to miss. Send a short sequence over your retry period instead: one when the payment fails, a reminder a few days later, and a final notice before the subscription ends. Store the payment state in a contact property such as subscriptionStatus, set it to Failed through the API when a payment fails and back to Paying on invoice.paid, and trigger a workflow when it changes to Failed. Add an audience filter set to All following nodes so customers who pay leave the sequence. The lifecycle email recipe shows this setup.

Send receipts and must-deliver notices from your server

import Stripe from "stripe";
import { LoopsClient, APIError } from "loops";

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
const loops = new LoopsClient(process.env.LOOPS_API_KEY);

// Call this from the route that receives Stripe's raw request body.
export async function handleStripeEvent(rawBody, signature) {
  const event = stripe.webhooks.constructEvent(
    rawBody,
    signature,
    process.env.STRIPE_WEBHOOK_SECRET
  );
  if (event.type !== "invoice.payment_failed") return;

  const invoice = event.data.object;
  if (!invoice.customer_email) return;

  try {
    await loops.sendTransactionalEmail({
      transactionalId: process.env.LOOPS_PAYMENT_FAILED_ID,
      email: invoice.customer_email,
      dataVariables: {
        amount: (invoice.amount_due / 100).toFixed(2),
        currency: invoice.currency,
        payUrl: invoice.hosted_invoice_url,
      },
      headers: { "Idempotency-Key": event.id },
    });
  } catch (err) {
    if (!(err instanceof APIError && err.statusCode === 409)) throw err;
  }
}

Stripe can deliver the same event more than once, so the event ID is the idempotency key, and a 409 means the email already went out. amount_due is in the smallest currency unit, so dividing by 100 only works for two-decimal currencies. Handle invoice.paid the same way for receipts.

What each email should say

A failed payment email should give the amount, which payment method failed, a link to update it, and when you will try again. Keep the tone plain and helpful. A receipt should give the amount, the date, the plan or items, and a link to the invoice.

Common questions

Can Loops send Stripe receipts?

Do failed payment emails reach customers who unsubscribed?

Which Stripe events can trigger emails in Loops?