How to Send Overdue Invoice Reminders to Clients from Stripe with Escalation Approval

Build a Spojit workflow that runs every morning, pulls your open Stripe invoices, emails each client a polite reminder through Resend, and routes any large overdue balance to a human approval before a firmer collections message goes out.

What This Integration Does

Chasing unpaid invoices by hand is slow and easy to forget, and the firmest reminders are exactly the ones you want a person to sign off on before they reach a client. This tutorial wires those two needs together. A daily schedule reads your outstanding Stripe invoices, loops over each one, and sends a friendly reminder by email. When a balance crosses a threshold you set (for example, anything over 1,000 in your currency), the workflow pauses and asks a teammate to approve the escalation before any stronger collections wording is sent. Nothing firmer leaves Spojit without a human saying yes.

The workflow runs on a Schedule trigger, so it fires on its own each weekday morning with no manual step. On each run it lists Stripe invoices whose status is open (issued but not yet paid), iterates through them with a Loop node, and uses a Condition node to split low-value reminders (sent straight away) from high-value ones (held for approval). A Human approval node gates the escalation path. Because the reminder is read-only against Stripe and idempotent per invoice, re-running the workflow is safe: a client who already paid drops out of the open list on the next run and stops receiving reminders automatically.

Prerequisites

  • A Stripe connection in Spojit using an API key with read access to invoices and customers. See Setting Up an API Key Connection if you have not added it yet.
  • A Resend connection with a verified sending domain, so reminders come from your own billing address (for example billing@yourfirm.com).
  • A Slack connection if you want the optional internal alert step, with access to the channel that handles collections.
  • At least one Spojit user, role, or team you can assign as an approver for the escalation step.
  • A clear escalation threshold (an amount, in the smallest currency unit Stripe uses, such as cents) and your reminder email copy ready.

Step 1: Add the Schedule trigger

Open the Workflow Designer, create a new workflow, and set the trigger node Type to Schedule. Schedules use a 5-field Unix cron expression plus an IANA timezone. To run at 9:00 AM on weekdays, enter the cron expression 0 9 * * 1-5 and a timezone such as Australia/Sydney or America/New_York. The trigger output is { scheduledAt }, which you can reference later as {{ trigger.scheduledAt }} in your email copy. A single Schedule trigger can hold more than one schedule if you later want a second daily pass.

Step 2: List open invoices from Stripe

Add a Connector node in Direct mode, choose your Stripe connection, and select the list-invoices tool. Direct mode is the right choice here because you want a predictable, single tool call with no AI cost. Set status to open so you only pull issued invoices that have not been paid, and set limit to a sensible page size (max 100, default 10). Leave customer empty to cover all clients. Map the result to an output variable such as invoices so later steps can read {{ invoices }}.

If you bill many clients, Stripe returns results one page at a time. Use the starting_after field with the ID of the last invoice from the previous page to fetch the next batch, or keep limit at 100 and accept that very large books may need a follow-up run.

Step 3: Loop over each invoice

Add a Loop node set to ForEach mode and point it at the list of invoices, for example {{ invoices.data }} (the array Stripe returns). Inside the loop each item is available as the current element, for example {{ invoice }}, exposing fields like {{ invoice.id }}, {{ invoice.customer }}, {{ invoice.amount_due }}, {{ invoice.currency }}, and {{ invoice.customer_email }}. Every node you place inside the loop body runs once per invoice. If you need the client's full name and you only have a customer ID, add a Connector node in Direct mode on Stripe with the get-customer tool, passing {{ invoice.customer }}, and read the name from its result.

Step 4: Branch on the balance with a Condition node

Inside the loop, add a Condition node to separate routine reminders from escalations. Compare the invoice balance against your threshold, for example check whether {{ invoice.amount_due }} is greater than 100000 (1,000.00 if your currency uses cents). The Condition node produces a true output and a false output:

  • False branch (balance at or below the threshold): send the standard friendly reminder right away, with no approval.
  • True branch (balance over the threshold): route to the Human approval node before any firmer collections message goes out.

If you want to compare more than one field (for example, balance over the threshold AND the invoice more than 14 days old), compute the age first in a Connector node on the date connector using the diff tool, then test that value in the Condition node.

Step 5: Send the standard reminder via Resend

On the Condition node's false branch, add a Connector node in Direct mode on your Resend connection and select the send-email tool. Sending through Resend (rather than the built-in Send Email node) lets the reminder come from your own verified domain. Map the fields directly from the current invoice:

from:    "billing@yourfirm.com"
to:      "{{ invoice.customer_email }}"
subject: "Friendly reminder: invoice {{ invoice.number }} is due"
html:    "<p>Hello,</p>
          <p>Our records show invoice {{ invoice.number }} for
          {{ invoice.amount_due }} {{ invoice.currency }} is still open.
          You can pay it here: {{ invoice.hosted_invoice_url }}</p>
          <p>Thank you,<br>Accounts at Your Firm</p>"

Use {{ invoice.hosted_invoice_url }} so clients can pay in one click. Keep this message warm and low pressure: it is the version that goes out without review.

Step 6: Hold escalations for human approval

On the Condition node's true branch, add a Human approval node. This pauses the workflow until a person approves or rejects, so a large overdue balance never triggers firmer wording automatically. Configure it:

  • Label and Message: describe the decision, for example "Send firm collections notice to {{ invoice.customer_email }} for {{ invoice.amount_due }} {{ invoice.currency }}?"
  • Approval slots (the only required field): add a slot containing a User, Role, or Team that owns collections. Any atom satisfies its slot, and approval completes only when every slot is satisfied.
  • Notification title/body: support variables, so you can include {{ invoice.number }} for context.
  • Timeout (minutes): optional; leave blank for no deadline, or set a value knowing a timeout is treated as a reject.
  • Urgency: set High for large balances; optionally turn on Email approvers so the first ten are emailed.

Approvers act in the Approvals inbox at /approvals. On Approved, the node outputs { approved: true, approvalId, outcome: "APPROVED" } and the workflow continues to the firmer email. On Rejected or Timed out, the run halts on that branch, so no firm message is sent. Note that "on reject, do X" branching is not supported: a rejection simply stops the path. After approval, add a second Resend send-email step with your firmer collections wording and the same invoice variables.

Step 7: Post a daily summary to Slack (optional)

After the loop, add a Connector node in Direct mode on your Slack connection using the send-message tool. Post a short run summary to your collections channel, for example "Today's reminder run processed {{ invoices.data.length }} open invoices; escalations were sent for review." Set channel to your channel ID and text to the summary. This gives your team visibility without anyone opening the Designer. If you would rather alert only when something needs attention, move this Slack step inside the true branch so it fires per escalation.

Tips

  • Keep the list-invoices call in Direct mode: it is deterministic and costs no AI credits, which matters when the workflow runs every day.
  • Stripe amounts are in the smallest currency unit (cents for many currencies). Set your Condition threshold in the same unit, so 100000 means 1,000.00.
  • If you want Miraxa, the intelligent layer across your automation, to scaffold this for you, try a prompt like "Add a Condition node that checks if {{ invoice.amount_due }} is over 100000 and connect the true branch to a Human approval node," then fine-tune the fields in the properties panel.
  • For very large invoice books, raise limit to 100 and page with starting_after, or split the run across morning and afternoon schedules on the same trigger.

Common Pitfalls

  • Filtering on the wrong status. Use open for unpaid issued invoices; draft invoices are not finalized and paid, void, or uncollectible should not receive reminders.
  • Forgetting that a rejected or timed-out approval halts the branch. A timeout is treated as a reject, so set the timeout deliberately and brief approvers on the deadline.
  • Sending from an unverified Resend domain. Verify your domain first, or reminders may fail to deliver or land in spam.
  • Cron timezone surprises. The Schedule trigger uses the IANA timezone you set, not the viewer's local time, so confirm it matches your billing hours.

Testing

Before turning the schedule on for everyone, scope the test small. Temporarily set the customer field on list-invoices to a single test customer ID, or lower limit to 1, and use the Run button to fire the workflow manually instead of waiting for the cron. Watch the execution in the run history to confirm the loop iterates, the Condition routes low and high balances correctly, and a high-value invoice pauses at the Human node. Approve and reject once each from the Approvals inbox to verify both the firmer email and the halt-on-reject behaviour. Once the small scope behaves, remove the test filters and let the Schedule trigger run on its own.

Learn More

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