How to Email a Monthly Stripe Failed-Payment and Dunning Summary

Build a Spojit workflow that runs once a month, pulls your Stripe charges, separates failed payments from recovered ones, tallies lost versus recovered revenue with the math connector, and emails finance a CSV-backed dunning summary so they can chase outstanding accounts.

What This Integration Does

When payments fail, the money does not disappear on its own: a card declines, Stripe retries, some charges eventually succeed, and the rest become revenue you have to chase. Most finance teams only find out by digging through the Stripe dashboard. This workflow turns that recurring chore into an automatic monthly report. Spojit lists your recent Stripe charges, loops over them to bucket the failed and recovered ones, and emails a single summary that names how much revenue was recovered through dunning retries and how much is still outstanding, with a CSV of the individual failures attached so finance can follow up account by account.

The workflow is driven by a Schedule trigger, so it runs on a fixed monthly cron with no manual step. Each run pulls a fresh page of charges from Stripe, computes its totals from scratch, and sends one email; it stores no state between runs, so re-running the same month simply produces the same report again. Because the report is read-only against Stripe (it only lists charges, never modifies them), it is safe to run as often as you like while you tune it.

Prerequisites

  • A Stripe connection in Connections with a restricted API key that can read charges. See the Stripe connector article for setup.
  • The built-in math and csv connectors, which need no connection or credentials.
  • The finance recipient address added to your org allowlist under Settings -> General -> Email recipients so the Send Email node can reach it.
  • Comfort with the designer basics: see adding and connecting nodes and working with variables and templates.

Step 1: Schedule the workflow to run monthly

Add a Trigger node and set its type to Schedule. A Schedule trigger uses a 5-field Unix cron expression plus an IANA timezone. To run at 7:00 AM on the first day of every month, set the cron to 0 7 1 * * and the timezone to your finance team's zone, for example Australia/Sydney. The trigger output is { scheduledAt }, which you can reference later as {{ trigger.scheduledAt }} to stamp the report. For more on cron syntax, see setting up a Schedule trigger.

Step 2: List recent Stripe charges

Add a Connector node in Direct mode on the stripe connector and choose the list-charges tool. Direct mode is deterministic and costs no AI credits, which is what you want for a predictable data pull. Set limit to the maximum of 100 to fetch a full page of charges. Bind the result to a variable such as charges. Each charge in the returned list carries a status (for example succeeded or failed), an amount in the smallest currency unit (cents), a currency, a created timestamp, and the customer reference, which is everything you need to classify and tally below.

Tool: list-charges
limit: 100
result variable: charges

If you process more than 100 charges a month, Stripe returns a pagination cursor; pass the last charge id back into the starting_after field on a second list-charges call and merge the lists. See the Tips section for handling this cleanly.

Step 3: Loop over the charges to isolate failures and recoveries

Add a Loop node in ForEach mode and point it at {{ charges.result.data }}, the array of charge records. Inside the loop body you classify each charge. A charge whose status is failed represents lost or still-outstanding revenue; a charge whose status is succeeded but that carries Stripe's retry/dunning metadata represents revenue you recovered after an earlier decline.

Use a Transform node in the loop body to emit a small, flat record per charge that you can collect for both the math totals and the CSV. Reference the current item as {{ item }}. A useful shape per charge is:

{
  "chargeId": "{{ item.id }}",
  "customer": "{{ item.customer }}",
  "amount": "{{ item.amount }}",
  "currency": "{{ item.currency }}",
  "status": "{{ item.status }}",
  "createdAt": "{{ item.created }}"
}

Use a Condition node to branch the loop body: on the branch where {{ item.status }} equals failed, collect the record into your failed list; on the branch where the charge succeeded after a prior attempt, collect it into your recovered list. See using Loop nodes and using Condition nodes for the branching mechanics.

Step 4: Tally recovered versus lost revenue with the math connector

After the loop, add two Connector nodes in Direct mode on the math connector, each using the sum tool. The sum tool takes a numbers array and returns { result }. Feed it the amounts (in cents) from each bucket. Build the arrays with a Transform node that plucks the amount from your failed and recovered lists.

Tool: sum   (lost / outstanding)
numbers: [ amounts of all failed charges, in cents ]
result variable: lostTotal

Tool: sum   (recovered)
numbers: [ amounts of all recovered charges, in cents ]
result variable: recoveredTotal

To present the totals as readable money rather than raw cents, add a Connector node on the math connector using the currency tool. Pass number as the dollar value (divide the cent total by 100 first in a Transform node) and set currency to your reporting code, for example USD. The currency tool returns a formatted result string such as $4,210.00. Do this for both lostTotal and recoveredTotal. The math connector runs in-process and costs no AI credits.

Step 5: Build the CSV of individual failures

Add a Connector node in Direct mode on the csv connector and choose the from-json tool, which converts a JSON array into a CSV string. Pass your collected failed-charge records (the flat objects from Step 3) as the input array. Each object key becomes a CSV column, so finance gets a clean table of chargeId, customer, amount, currency, status, and createdAt.

Tool: from-json
input: [ failed-charge records ]
result variable: failedCsv

Bind the CSV string to a variable such as failedCsv. You will attach it to the email in the next step. For more on shaping tabular data, see the CSV Tools connector article.

Step 6: Email the dunning summary to finance

Add a Send Email node. It sends from Spojit's built-in mail service, so no connection is required. Set Recipients to your finance address (already on the allowlist from the Prerequisites), write a templated Subject, and compose the Body from the variables you computed. Only upstream variables resolve, so reference the formatted totals and counts directly.

Subject: Stripe dunning summary - {{ trigger.scheduledAt }}

Body:
Recovered revenue this period: {{ recoveredFormatted.result }}
Outstanding / lost revenue: {{ lostFormatted.result }}
Failed payments needing follow-up: {{ failedCount }}

The attached CSV lists each failed charge by customer so the
team can chase outstanding accounts.

Attach the CSV by feeding your failedCsv string into the node's attachment input. Set If sending fails to Fail the workflow so a delivery problem surfaces in the execution history rather than passing silently. For options and limits, see using Send Email nodes. If you would rather send from your own domain, swap this node for a Connector node on the resend or smtp connector using its send-email tool.

Tips

  • If your charge volume exceeds 100 per month, page through Stripe: capture the last charge id from list-charges and call the tool again with that id in starting_after, then merge the lists before the Loop. A Loop in While mode can keep paging until Stripe returns fewer than 100 records.
  • Stripe amounts are in the smallest currency unit (cents), so divide by 100 in a Transform node before passing values to the currency tool, and keep your sum inputs in a single currency.
  • Ask Miraxa, the intelligent layer across your automation, to scaffold the skeleton for you with a prompt like "Add a Schedule trigger that runs monthly, a Connector node on the stripe connector using list-charges, and a Loop node over the charges," then fine-tune each node in the properties panel.
  • Use the average or percentage math tools to add a recovery rate (recovered divided by recovered-plus-lost) to the email for a quick health signal.

Common Pitfalls

  • Timezone drift on month boundaries. A cron of 0 7 1 * * fires relative to the timezone you set on the trigger. If you leave it on a different zone, the report can land on the wrong calendar day for finance. Always set the IANA timezone explicitly.
  • Counting all charges as failures. list-charges returns succeeded charges too. Always branch on {{ item.status }} in the Loop so your totals separate failed from recovered rather than summing everything.
  • Mixed currencies in one sum. The sum tool adds raw numbers and does not know about currency. If you transact in more than one currency, group charges by currency first and total each group separately.
  • Recipient not allowlisted. The Send Email node only delivers to addresses on your org allowlist. Add the finance address under Settings -> General -> Email recipients before the first real run.

Testing

Before scheduling it live, validate on a small scope. Temporarily lower the list-charges limit to 10 and run the workflow once with the Run button instead of waiting for the cron. Open the run in the execution history and confirm the Loop classified each charge correctly, the two sum totals match what you see in Stripe, and the from-json step produced a CSV with one row per failed charge. Check the email arrived with the attachment, then restore limit to 100 and enable the Schedule trigger.

Learn More

Did this answer your question? Thanks for the feedback There was a problem submitting your feedback. Please try again later.