{
  "slug": "dunning-email-sequence",
  "title": "Dunning email sequence: template and code",
  "url": "https://www.courier.com/recipes/dunning-email-sequence",
  "markdown": "https://www.courier.com/recipes/dunning-email-sequence.md",
  "stage": "Monetization",
  "purpose": "Recover failed payments with three reminders that stop the moment the card goes through.",
  "summary": "A dunning email sequence recovers failed payments before they turn into involuntary churn. When a payment fails, it emails the customer right away, follows up on day 3 by email and SMS, and sends a final notice by email and Inbox on day 7. A successful payment stops whatever is left.",
  "channels": [
    "email",
    "sms",
    "inbox"
  ],
  "shape": "journey",
  "shows": [
    "Starting from your payment provider's webhook, once per invoice",
    "An SMS only for users with a phone number",
    "Cancel when the invoice is paid"
  ],
  "data": [
    {
      "name": "invoice_id",
      "type": "string",
      "example": "in_1Q2w3E",
      "note": "Builds the cancellation token and the idempotency key"
    },
    {
      "name": "profile.first_name",
      "type": "string",
      "example": "Dana",
      "note": "From the profile. \"there\" is the fallback when it's missing"
    },
    {
      "name": "plan_name",
      "type": "string",
      "example": "Pro"
    },
    {
      "name": "amount",
      "type": "integer",
      "example": "4900",
      "note": "In cents, as Stripe sends it. The templates show it as dollars"
    },
    {
      "name": "access_change_at",
      "type": "string",
      "example": "2026-10-25T17:00:00Z",
      "note": "An ISO timestamp. The date your own policy changes the account, which you pass in. It shows in the recipient's timezone"
    }
  ],
  "steps": [
    {
      "step": "1",
      "when": "Right after the payment fails",
      "channels": "Email",
      "templates": [
        "templates/payment-failed-email.json"
      ]
    },
    {
      "step": "2",
      "when": "3 days after the first email",
      "channels": "Email, plus SMS for anyone with a phone number",
      "templates": [
        "templates/payment-day3-email.json",
        "templates/payment-day3-sms.json"
      ]
    },
    {
      "step": "3",
      "when": "4 days after step 2, 7 days after the first email",
      "channels": "Email, Inbox",
      "templates": [
        "templates/payment-final-email.json",
        "templates/payment-final-inbox.json"
      ]
    }
  ],
  "files": {
    "journey": "journey.json",
    "templates": [
      "templates/payment-failed-email.json",
      "templates/payment-day3-email.json",
      "templates/payment-day3-sms.json",
      "templates/payment-final-email.json",
      "templates/payment-final-inbox.json"
    ],
    "setup": [
      "setup.ts"
    ],
    "start": [
      "start.ts",
      "start.py"
    ],
    "stop": [
      "stop.ts",
      "stop.py"
    ]
  },
  "docs": [
    "https://www.courier.com/docs/guides/notify-from-stripe",
    "https://www.courier.com/docs/journeys/nodes/delay",
    "https://www.courier.com/docs/journeys/nodes/send",
    "https://www.courier.com/docs/journeys/nodes/cancel",
    "https://www.courier.com/docs/journeys/invoke"
  ],
  "related_recipes": [
    "https://www.courier.com/recipes/expiring-card-reminder-email",
    "https://www.courier.com/recipes/subscription-renewal-reminder-email",
    "https://www.courier.com/recipes/payment-receipt-email"
  ],
  "agent_prompt": "Add a dunning sequence for failed payments to this app, sent through Courier: an email right away, an email and SMS on day 3, and a final notice on day 7, stopped as soon as the invoice is paid. Follow the steps at https://www.courier.com/recipes/dunning-email-sequence.md and use the Courier skill for SDK details.\n\n1. Create the journey and its templates in my Courier workspace by running setup.ts from that page with COURIER_API_KEY from our environment. Save the journey ID it prints as COURIER_DUNNING_JOURNEY_ID. Replace the placeholder product name (Acme) and billing link in the templates with ours.\n2. Find our payment provider's webhook handler and start the journey when a payment fails (invoice.payment_failed in Stripe). Start the journey with the user's ID. Pass the fields in the Data fields table from our own data. Use the invoice ID as the idempotency key, as start.ts does, because Stripe sends invoice.payment_failed for every failed attempt and can repeat an event.\n3. In the same handler, cancel the journey with the token invoice-<invoice_id> when the invoice is paid (invoice.paid in Stripe).\n4. Match this codebase's language, file layout, and error handling. Never hardcode the API key.\n5. When you're done, start one run for a test user and show me the run's steps. The first email sends right away and the run then waits 3 days, so cancel the test run once you've checked it.",
  "status": {
    "state": "passing",
    "last_tested": "2026-10-19"
  }
}
