# What You’ll Build#
This guide shows how to implement n8n Stripe reconciliation from Stripe payouts into Xero or QuickBooks, with a controlled exception process.
You will build a workflow that pulls Stripe payout data, computes accounting-ready totals for sales, fees, refunds, and adjustments, posts a journal or deposit to Xero or QuickBooks, and routes anything that does not match expected rules into an exceptions queue.
You will also implement schedules, idempotency, logging, and controls, using patterns that scale past a few payouts per month.
# Why Payout Reconciliation Matters in Stripe#
Stripe is not your bank ledger. Your bank sees payouts, while Stripe’s operational view includes charges, refunds, disputes, and fees that may happen on different days.
If you reconcile using charges only, you will constantly fight timing differences. If you reconcile using payouts, you align to what actually hits your bank statement, which makes month-end close faster and more reliable.
Common failure modes we see in real teams:
- Posting gross sales to the accounting system but not recording Stripe fees, leaving an unexplained gap.
- Handling refunds incorrectly, especially when they settle in a later payout.
- Multi-currency payouts where FX fees and conversions are missed.
- Manual CSV exports and VLOOKUP processes that don’t scale past 100 transactions.
# Reconciliation Concepts You Must Get Right#
Payouts versus Charges#
A charge is customer-level revenue activity. A payout is the net amount Stripe sends to your bank, typically daily or weekly.
One payout usually includes many charges, plus:
- Stripe processing fees
- Refunds and chargebacks
- Dispute fees
- Manual adjustments
- Application fees if you run a platform
In Stripe, the payout is your reconciliation anchor because it corresponds to a bank deposit or transfer.
Fees, Refunds, and Timing Differences#
Stripe fees are deducted before funds reach your bank. Refunds can be deducted from later payouts. That creates timing differences where:
- Sale happens in payout A
- Refund happens in payout B
If you post charge-level entries without mapping them to payouts, you create unmatched deposits and messy clearing accounts.
The Correct Model for Accounting Systems#
Most teams use one of these models:
- 1Payout-level journal entry posted to a clearing account, then matched to the bank deposit.
- 2Bank deposit object posted with line items, if your accounting system supports it cleanly.
In both cases, you typically use a Stripe Clearing account:
- Debits and credits flow through the clearing account.
- The payout posts net movement to the clearing account.
- The bank deposit matches the payout amount from Stripe to your bank feed.
🎯 Key Takeaway: Reconcile at the payout level to match bank deposits, and store charge-level detail for audit instead of pushing every charge into Xero or QuickBooks.
# Data Model Blueprint#
You need two layers: what Stripe provides, and what your accounting system expects.
Stripe Objects You’ll Use#
For payout reconciliation, the two most important Stripe objects are:
payoutbalance_transaction
Every payout is composed of many balance transactions, and each balance transaction has a type such as charge, refund, adjustment, stripe_fee, and potentially dispute-related types depending on your account.
Accounting Output Model#
Most payout reconciliation workflows can be reduced to these fields:
- Payout ID, payout date, currency, payout amount
- Totals by category for the payout:
- Gross sales
- Refunds
- Fees
- Disputes and dispute fees
- Other adjustments
- Posting result and identifiers in Xero or QuickBooks
- Exception status and reason if not posted
Suggested Tables for Control and Audit#
If you want reliable operations, store state. Even a small Postgres instance is enough.
| Table | Key columns | Purpose |
|---|---|---|
stripe_payout_runs | payout_id, run_id, status, started_at, finished_at | One run per payout per attempt, for traceability |
stripe_payout_lines | payout_id, type, amount, currency, source_id | Optional detail lines for audit |
recon_exceptions | entity_id, entity_type, reason_code, payload, status | Structured queue for human review |
idempotency_keys | key, created_at | Prevent duplicates in Xero or QuickBooks |
If you already run reliable workflows, implement the outbox and queue pattern described here: n8n Postgres queue and outbox pattern for reliable integrations.
# Workflow Architecture Overview#
You will build three n8n workflows:
- 1Payout Fetch and Compute: scheduled, loads payouts and computes totals.
- 2Post to Accounting: posts journal entries or deposits into Xero or QuickBooks with idempotency.
- 3Exceptions and Backfill: routes anomalies, supports re-runs, and handles late-arriving transactions.
This split is deliberate. It prevents API timeouts, makes retries safer, and isolates credentials and posting rules.
ℹ️ Note: If you expect more than a few thousand balance transactions per day, you must build for pagination, deduplication, and incremental sync. This is covered in n8n data sync patterns: CDC, pagination, deduplication.
# Step-by-Step n8n Workflow Blueprint#
Step 1: Define Accounts Mapping for Xero and QuickBooks#
Create a configuration record in your database or as n8n environment variables. Keep mappings explicit and versioned.
Minimum mapping you need:
- Stripe clearing account
- Sales revenue account
- Stripe fees expense account
- Refunds contra-revenue account
- Disputes and dispute fees accounts if applicable
| Category | Xero typical account | QuickBooks typical account | Notes |
|---|---|---|---|
| Stripe clearing | Current asset | Other current asset | Used for matching deposits |
| Sales | Revenue | Income | Optionally split by product |
| Refunds | Contra revenue | Contra income | Keeps net sales clean |
| Stripe fees | Expense | Expense | Track separately for margin |
| Dispute losses | Expense | Expense | Optional but recommended |
| Dispute fees | Expense | Expense | Often separate line |
⚠️ Warning: Do not post payout entries directly to your bank account ledger in Xero or QuickBooks unless you are confident your bank feed matching is configured correctly. Most teams get better control by posting to Stripe clearing and matching the bank deposit separately.
Step 2: Schedule the Workflow#
Use a Cron node to run:
- Daily at 06:00 in your accounting timezone for yesterday’s payouts.
- Optional hourly run to catch delayed payouts if you operate 24/7.
A practical schedule:
- Daily: reconcile payouts with
arrival_dateequal to yesterday. - Weekly: backfill last 14 days to catch late refunds and adjustments.
Step 3: Fetch Payouts from Stripe#
Use an HTTP Request node with Stripe API. Filtering by arrival date is helpful, but you should also support status-based logic.
# Stripe API endpoint used by the workflow
GET https://api.stripe.com/v1/payouts?limit=100&status=paidCapture fields:
idamountcurrencyarrival_datestatusdestinationif you have multiple bank accounts
If you have multiple Stripe accounts, separate workflows per account or pass account identifier through and store it in your tables.
Step 4: For Each Payout, Pull Balance Transactions#
The payout details are in balance_transactions filtered by payout.
GET https://api.stripe.com/v1/balance_transactions?limit=100&payout=po_123Paginate using starting_after until all pages are fetched. The balance transactions include:
amountandfeenettypesourcewhich links to charge or refund objectscreatedtimestamp
You can store detailed lines for audits, but you only need category totals to post to Xero or QuickBooks.
Step 5: Compute Totals by Category#
In an n8n Code node, sum amounts into buckets. Use integer minor units to avoid float rounding errors, then convert to decimals when posting.
// Input: items are Stripe balance transactions
const totals = {
grossSales: 0,
refunds: 0,
stripeFees: 0,
disputes: 0,
adjustments: 0,
currency: $input.first().json.currency,
};
for (const item of $input.all()) {
const t = item.json.type;
const amount = item.json.amount; // minor units
const fee = item.json.fee || 0;
if (t === 'charge') totals.grossSales += amount;
else if (t === 'refund') totals.refunds += amount; // negative in Stripe often
else if (t === 'stripe_fee') totals.stripeFees += amount;
else if (t.includes('dispute')) totals.disputes += amount;
else totals.adjustments += amount;
// Optional: fee is sometimes separate, depending on type
// If you rely on fee, you can add fee handling here.
}
return [{ json: totals }];Your exact mapping depends on your Stripe account settings and how balance transaction types appear. In production, you should log the distribution of unseen type values and treat unknown ones as exceptions.
💡 Tip: Add a rule that flags any payout where
adjustmentsis non-zero and above a threshold, for example more than5000minor units. Adjustments are where most reconciliation surprises live.
Step 6: Validate the Payout Equation#
Your computed totals must reconcile to the payout net movement.
A simplified check is:
expectedNet = grossSales + refunds + disputes + adjustments + stripeFeesexpectedNetshould equalpayoutAmountin minor units
Note that Stripe signs can be counterintuitive. Many teams normalize by treating inflows as positive and outflows as negative, then enforce:
net = inflows + outflows
Make the check explicit and fail fast when it doesn’t match.
| Control | What you compare | Why it matters |
|---|---|---|
| Net equals payout | sum(net) versus payout.amount | Prevents posting wrong totals |
| Currency consistency | all lines currency equals payout currency | Avoid multi-currency misposts |
| Missing types | unexpected balance_transaction.type | Catches new Stripe behavior |
| Duplicate protection | payout ID already posted | Prevents double entries |
Step 7: Persist Run State and Idempotency Keys#
Before posting, insert a run record:
status = computed- store the computed totals payload
- store an idempotency key, for example
stripe_payout_po_123_v1
If a later run restarts, it can detect that payout already posted and skip or compare.
This is the same operational principle used in lead-to-cash automation, where state makes retries safe. See: Lead-to-cash automation with n8n workflow.
Step 8: Post to Xero or QuickBooks#
Choose one posting strategy and stick to it. For most finance teams, a journal entry per payout is easiest to audit.
Posting Pattern A: Journal Entry to Stripe Clearing
Journal entry lines, expressed conceptually:
- Debit Stripe Clearing for payout net amount
- Credit Sales revenue for gross sales
- Debit Refunds contra account for refunds
- Debit Stripe fees expense for fees
- Debit dispute expense for disputes
- Debit or credit adjustments depending on sign
The journal must balance.
If you use QuickBooks, you can create a JournalEntry. If you use Xero, you can create a ManualJournal. The exact API fields differ, but the accounting idea is the same.
Posting Pattern B: Deposit with Fee Lines
This can work well in QuickBooks if your bank feed matching is clean, but it varies by setup. Many teams still prefer the clearing account plus manual match.
Step 9: Handle Exceptions with a First-Class Queue#
Exceptions should not be a vague Slack message. Model them as structured events with:
- reason code
- payload snapshot
- recommended action
- assigned owner and status
Suggested exception reason codes:
NET_MISMATCHUNKNOWN_TXN_TYPECURRENCY_MISMATCHMISSING_ACCOUNT_MAPPINGACCOUNTING_API_ERRORDUPLICATE_PAYOUT
Workflow behavior:
- 1Do not post to accounting if validation fails.
- 2Write exception to
recon_exceptions. - 3Notify finance ops with a link to the record and a recommended fix.
- 4Allow re-run after resolution.
A practical SLA:
- High severity exceptions, for example net mismatch, should be reviewed within 24 hours.
- Low severity exceptions, for example unknown type with zero amount, can be reviewed weekly.
⚠️ Warning: Do not auto-retry posting on every failure. If the failure is a validation issue or mapping issue, retries just spam your accounting system and increase the chance of duplicates.
Step 10: Logging, Monitoring, and Controls#
At minimum, implement:
- Structured logs for each payout run with
run_id - A summary metric: payouts processed, posted, failed
- A daily reconciliation report sent to finance
Suggested control report fields:
- Payout date and ID
- Net payout amount and currency
- Total sales, refunds, fees, adjustments
- Posted reference in Xero or QuickBooks
- Exception count and links
You can implement reporting by writing to a table, then using an n8n workflow that sends an email or creates a ticket.
# Exceptions Handling Patterns That Work in Production#
Unknown Balance Transaction Types#
Stripe can introduce new types or your business model can activate new flows. When a new type appears:
- Capture it
- Store it
- Fail the payout posting if the amount is material
- Provide a safe default mapping only after review
Rule of thumb:
- If unknown type absolute amount is greater than
1000minor units, fail the run. - Otherwise, classify as adjustment and open a low severity exception.
Refund Timing Differences Across Payouts#
Refunds often show up days after the original sale, in a different payout.
To keep accounting consistent:
- Continue reconciling payouts as they happen.
- Use a refunds contra account to represent the period where refund hits.
- Do not attempt to rewrite the original sales posting unless your accounting policy requires revenue restatement.
If finance needs customer-level audit, store charge IDs and refund IDs in stripe_payout_lines.
Partial Refunds and Rounding#
Partial refunds create fractional rounding differences when converted to reporting currency. Treat this as an adjustment line when the variance is small.
A practical policy:
- If absolute rounding difference is less than
10minor units, post it to an adjustments account. - If greater than or equal to
10minor units, open an exception.
Disputes and Chargebacks#
Disputes can have multiple events: dispute created, funds withdrawn, dispute won, fee charged. These often span multiple payouts.
Treat dispute movements as their own category. Keep them separate from refunds for better reporting, because dispute rates are a product and risk metric, not a customer success metric.
# End-to-End n8n Implementation Notes#
Pagination and Deduplication#
Balance transactions are paginated. You must:
- paginate until
has_moreis false - store
starting_aftercursor - deduplicate by
balance_transaction.id
A robust approach is incremental sync into Postgres, then compute payout totals from your local store. This scales better and makes backfills simpler. If you need that architecture, start with: n8n data sync patterns: CDC, pagination, deduplication.
Reliability and Exactly-Once Posting#
Accounting APIs can time out after they have already created the record. If you retry without idempotency, you may create duplicates.
Implement one of these:
- Idempotency key stored in your database and checked before posting
- External ID field in the accounting object set to Stripe payout ID, then query-before-create
- Queue and outbox pattern so posting is a separate transactional step
For the most reliable pattern with n8n, use Postgres as the source of truth and implement an outbox-based worker workflow. Reference: n8n Postgres queue and outbox pattern for reliable integrations.
Security and Access Controls#
Keep finance workflows locked down:
- Separate n8n credentials per environment
- Least privilege for Xero or QuickBooks apps
- Audit log access to workflows and credentials
Also log who approved exception overrides, if you support manual fixes.
# Example Posting Payload Shape#
Even if you use different APIs, keep your internal payload stable. This makes testing and re-runs easier.
| Field | Example | Notes |
|---|---|---|
payoutId | po_123 | Primary key for idempotency |
arrivalDate | 2026-09-23 | Use accounting timezone |
currency | usd | Enforce single currency per payout |
grossSalesMinor | 250000 | Minor units |
refundsMinor | -12000 | Keep Stripe sign convention or normalize consistently |
feesMinor | -7750 | Often negative |
adjustmentsMinor | 0 | Non-zero triggers review |
netMinor | 230250 | Must equal payout amount |
postingTarget | xero | Or quickbooks |
idempotencyKey | stripe_payout_po_123_v1 | Stored and reused |
# Testing Checklist Before You Go Live#
Validate using at least 20 historical payouts, including edge cases:
- 1Normal payout with only charges and fees.
- 2Payout containing refunds.
- 3Payout containing disputes.
- 4Payout with manual adjustments.
- 5Multi-currency scenario if applicable.
Operational tests:
- 1Re-run the same payout and confirm no duplicate posting.
- 2Simulate accounting API timeout and ensure safe retry.
- 3Force a net mismatch and confirm exception is created and posting is blocked.
- 4Backfill last 14 days and confirm consistent results.
# Key Takeaways#
- Reconcile Stripe at the payout level to match bank deposits, and keep charge-level detail in a database for audit.
- Enforce controls that validate
expectedNetequals the payout amount before posting to Xero or QuickBooks. - Model exceptions as structured records with reason codes, not ad-hoc notifications, and block posting on material anomalies.
- Use idempotency keys and a persistent run log to prevent duplicate accounting entries during retries.
- Build pagination and deduplication into Stripe balance transaction syncing, especially as volume grows.
# Conclusion#
Automating n8n Stripe reconciliation is less about connecting APIs and more about building a finance-grade system: payout-level truth, consistent sign conventions, validation controls, and a real exception queue.
If you want Samioda to implement this end-to-end for your Stripe and Xero or QuickBooks setup, including Postgres state, outbox-based reliability, and finance-friendly reporting, contact us via Samioda automation services and we’ll scope it against your payout volume and close process.
FAQ
Founder & Senior Developer at Samioda. 8+ years building React, Next.js, Flutter and n8n automation solutions for clients across Europe.
More in Business Automation
All →n8n Customer Support Automation: Ticket Triage, SLA Alerts, and Escalations (Zendesk or Intercom plus Slack)
A practical 2026 guide to n8n customer support automation: automatic tagging, routing, priority scoring, SLA breach alerts, escalations, and human-in-the-loop approvals with auditability and safe retries.
n8n Operations Runbook: Monitoring, Alerting, SLOs, and On-Call Playbooks for Reliable Automations
Operate n8n like a production service: define SLOs, build dashboards, set actionable alerting, classify incidents, and use ready-to-copy runbooks and post-incident templates tailored to automation workflows.
Securing n8n in Production: Credential Rotation, Least Privilege, and Service Account Patterns
A practical security playbook for n8n credential rotation best practices: managing secrets across environments, rotating safely without downtime, designing least-privilege service accounts, and staying audit-ready with Vault and cloud KMS examples.
Need help with your project?
We build custom solutions using the technologies discussed in this article. Senior team, fixed prices.
Related Articles
Automated Reporting with n8n: Build Weekly KPI Digests from GA4, Stripe, and Postgres
A practical guide to automated reporting with n8n: pull weekly KPIs from GA4, Stripe, and Postgres, validate data quality, generate a concise narrative summary, and send it to Slack and email with retries and maintainable structure.
How to Automate Your Invoicing Process: Step-by-Step n8n Guide (2026)
A practical, step-by-step guide to automate your invoicing process with n8n: invoice generation, email sending, payment reminders, and reconciliation with accounting-ready logs.
n8n Customer Support Automation: Ticket Triage, SLA Alerts, and Escalations (Zendesk or Intercom plus Slack)
A practical 2026 guide to n8n customer support automation: automatic tagging, routing, priority scoring, SLA breach alerts, escalations, and human-in-the-loop approvals with auditability and safe retries.