How to Auto-Reconcile Stripe Payout Emails Against Your Ledger with a Mailhook

Forward Stripe payout notification emails to a Spojit Mailhook, extract the payout amount and date, match it against your stored charges, and post a clean reconciliation summary to Slack.

What This Integration Does

When Stripe sends a payout to your bank account, it emails a notification: an amount, a currency, an arrival date, and a payout reference. Finance teams usually copy those numbers into a spreadsheet by hand and compare them against the charges that should have funded the payout. This Spojit workflow removes that manual step. The moment a payout email lands, the workflow reads it, pulls the matching Stripe charges, totals them, compares that total against the payout figure, and tells your team in Slack whether the payout reconciles cleanly or needs a closer look.

The workflow is triggered by a Mailhook: Spojit gives you a unique inbound email address, and you point Stripe's notification (or a forwarding rule on your finance inbox) at it. Each email starts one run within seconds, with no mailbox connection needed. The run parses the email body, calls the Stripe connector to fetch recent charges, runs the math, writes a reconciliation record to MongoDB for your audit trail, and sends a Slack summary. Runs are independent and deduplicated per message, so the same payout email never reconciles twice, and re-running a single email simply produces a fresh record.

Prerequisites

  • A Stripe connection (API key) added under Connections. See the Stripe connector article for setup.
  • A MongoDB connection pointing at the database where you want to store reconciliation records. See the MongoDB connector article.
  • A Slack connection with access to the finance channel you want to post to. See the Slack connector article.
  • Knowledge of the currency and rough payout cadence you use in Stripe, plus the channel name or ID for Slack (for example #finance-ops).
  • Access to either Stripe's payout email notifications or a forwarding rule on the inbox that receives them, so you can redirect them to your Spojit Mailhook address.

Step 1: Add a Mailhook trigger and generate the address

Create a new workflow and open the Trigger node. Set Trigger Type to Mailhook. Optionally set an Address prefix (1-24 characters, for example stripe-payouts) so the generated address is recognizable, then click Generate email address. Spojit produces a unique address like stripe-payouts-a1b2c3d4e5f6g7h8@mailhook.spojit.com. Copy it and point your Stripe payout notifications at it, either by adding it as a notification recipient in Stripe or with a forwarding rule on your finance inbox.

To keep noise out, set the optional From allowlist to the sender Stripe uses (for example notifications@stripe.com) and a Subject regex such as payout so only payout emails start a run. The trigger output is available downstream as {{ input }}, including {{ input.subject }}, {{ input.text }}, {{ input.html }}, and {{ input.replyTo }}.

Step 2: Extract the payout amount and date with a Transform node

Add a Transform node to pull the structured figures out of the email. Stripe payout emails follow a consistent layout, so you can read the amount and arrival date from {{ input.text }}. Produce a clean object for the rest of the workflow to use, for example:

{
  "payoutAmount": 4820.55,
  "currency": "usd",
  "arrivalDate": "2026-06-21",
  "payoutReference": "po_1Nx8...",
  "receivedAt": "{{ input.receivedAt }}"
}

If the email layout varies, use a Connector node on the regex connector with the extract tool to capture the amount, or the match tool to find the payout reference, then assemble the values in this Transform node. Keep currency lowercase so it matches the Stripe charge data later.

Step 3: Fetch the funding charges from Stripe

Add a Connector node on the Stripe connector in Direct mode and pick the list-charges tool. Direct mode is the right choice here because you want a deterministic, no-AI-cost call. Set limit to 100 (the maximum) to pull the charges that should make up the payout. Because a single page may not cover the whole payout window, this node will likely need to page: use the returned cursor as starting_after on a follow-up call, or wrap the call in a Loop node (While) that keeps requesting pages until the returned list is shorter than the page size.

Filter the charges down to the ones that belong to this payout: keep only succeeded charges in the same currency whose timing lines up with the payout window. A short Connector node on the array connector using filter is a clean way to narrow the list before you total it.

Step 4: Total the charges and compare against the payout

Add a Connector node on the math connector and use the sum tool to add up the net amounts of the filtered charges into {{ chargeTotal }}. Then add a Condition node that compares the totals. Because Stripe deducts fees before paying out, expect a small expected gap rather than an exact match. A practical rule is to treat the payout as reconciled when the absolute difference between {{ chargeTotal }} and {{ payout.payoutAmount }} is within a tolerance you set (for example a few dollars plus your typical fee percentage).

Send the matched case down the true branch (clean reconciliation) and the mismatched case down the false branch (needs review). You can compute the difference with the math connector's currency tool so the value is formatted for display in the Slack summary later.

Step 5: Store the reconciliation record in MongoDB

On both branches, add a Connector node on the MongoDB connector in Direct mode with the insert-documents tool. Write one record per payout so you keep a permanent, queryable audit trail. Use a payload that captures the payout, the computed total, and the outcome:

{
  "collection": "payout_reconciliations",
  "documents": [
    {
      "payoutReference": "{{ payout.payoutReference }}",
      "payoutAmount": "{{ payout.payoutAmount }}",
      "currency": "{{ payout.currency }}",
      "arrivalDate": "{{ payout.arrivalDate }}",
      "chargeTotal": "{{ chargeTotal }}",
      "difference": "{{ difference }}",
      "status": "RECONCILED",
      "reconciledAt": "{{ payout.receivedAt }}"
    }
  ]
}

On the false branch, set status to NEEDS_REVIEW instead. Storing the payoutReference lets a later workflow use the MongoDB find-documents tool to check whether a payout was already reconciled before acting on it again.

Step 6: Post a reconciliation summary to Slack

Add a Connector node on the Slack connector in Direct mode with the send-message tool. Set the channel (for example #finance-ops) and build a templated message from the values you computed. A clean-match message might read:

Stripe payout reconciled: {{ payout.payoutReference }}
Payout: {{ payout.payoutAmount }} {{ payout.currency }} (arrives {{ payout.arrivalDate }})
Charges total: {{ chargeTotal }} | Difference: {{ difference }}
Status: RECONCILED

On the false branch, post a message flagged for attention with the difference highlighted and Status: NEEDS_REVIEW. If you prefer to handle mismatches by email instead of (or in addition to) Slack, add a Send Email node and reply to the original sender with {{ input.replyTo }} as the recipient, since a Mailhook never replies to the sender on its own.

Step 7: Gate large discrepancies with a Human approval node (optional)

For payouts where the difference exceeds a meaningful threshold, add a Human node before the final write so a finance owner signs off. Set a clear Message that includes {{ payout.payoutReference }} and {{ difference }}, choose Urgency, add an Approval slot, and optionally enable Email approvers. Approvers act in the Approvals inbox. On approval the run continues to record the payout as reviewed; a rejection halts the run, leaving the discrepancy visibly unresolved for follow-up. For deeper coverage see the Human approval nodes article.

Tips

  • Stripe amounts in charge data are in the smallest currency unit (cents for USD). Convert to and from major units consistently before comparing, or you will see differences that are really just a factor of 100.
  • Use the Address prefix on your Mailhook so the inbound address is self-documenting, and keep separate workflows (and prefixes) per currency or per Stripe account.
  • Let Miraxa, the intelligent layer across your automation, scaffold the branching for you: try a prompt like "Add a Condition node that checks if the absolute value of {{ difference }} is over 5 and connect the true branch to a Human node," then fine-tune in the properties panel.
  • If your payout window spans more than 100 charges, the list-charges pagination loop in Step 3 is essential. Stop paging when a returned page is shorter than your page size.

Common Pitfalls

  • Skipping the From allowlist and Subject regex filters lets unrelated mail start runs. Scope the Mailhook to Stripe's sender and a payout subject pattern.
  • Expecting an exact match: Stripe deducts processing fees before paying out, so the charge total is almost always slightly higher than the payout. Reconcile within a tolerance, not on equality.
  • Timezone drift between the email's arrivalDate and your charge timestamps can push charges into the wrong payout window. Normalize dates with the date connector before filtering.
  • Regenerating the Mailhook address retires the old one instantly. If you rotate it, update Stripe's notification recipient or your forwarding rule in the same sitting, or payout emails will silently stop arriving.

Testing

Before pointing live Stripe notifications at the address, send one real payout email (or forward a past one) to your Mailhook address and watch the run in execution history. Confirm the Transform node extracted the right payoutAmount and arrivalDate, that list-charges returned the expected charges, and that the Condition node took the branch you expected. Check that the MongoDB record was written with the correct status and that the Slack message read cleanly. Once one email reconciles end to end, enable the workflow and let real payout notifications flow in.

Learn More

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