Skip to content
312+ businesses automated avg. 14h/week savedManual workflows cost the average team €560/week fix it in 10 daysDeployed in 5–10 business days · 30-day money-back guaranteeDental · Real Estate · Agencies · E-commerce · Covered99.97% uptime SLA · Monitored 24/7 by our ops teamA full-time ops hire costs €50K+/yr PURIST delivers more in daysn8n · Make · Claude AI · 500+ workflow templatesFree automation audit limited to 5 spots this week312+ businesses automated avg. 14h/week savedManual workflows cost the average team €560/week fix it in 10 daysDeployed in 5–10 business days · 30-day money-back guaranteeDental · Real Estate · Agencies · E-commerce · Covered99.97% uptime SLA · Monitored 24/7 by our ops teamA full-time ops hire costs €50K+/yr PURIST delivers more in daysn8n · Make · Claude AI · 500+ workflow templatesFree automation audit limited to 5 spots this week312+ businesses automated avg. 14h/week savedManual workflows cost the average team €560/week fix it in 10 daysDeployed in 5–10 business days · 30-day money-back guaranteeDental · Real Estate · Agencies · E-commerce · Covered99.97% uptime SLA · Monitored 24/7 by our ops teamA full-time ops hire costs €50K+/yr PURIST delivers more in daysn8n · Make · Claude AI · 500+ workflow templatesFree automation audit limited to 5 spots this week
PURIST
312+
Clients automated
14 h/wk
Avg time saved
99.97%
Uptime SLA
< 7 days
Deploy time
PURIST AI
Claude Opus 4.7 · n8n v1.71 · <80ms
What type of business are you running? I'll show you exactly which processes we'd automate first and your estimated ROI.
Powered by n8n + Claude Opus 4.7 Get my free automation plan →

Finance & Accounting

Advanced · 24 nodes

Stripe → Xero Reconciliation Pro

A 24-node production workflow that reconciles every Stripe charge, refund and payout against Xero, with signature verification, fee separation and mismatch alerting.

StripeXeroSlackAirtable
We deploy it for you

Free download, just your email, no spam, unsubscribe anytime.

This is the workflow most finance teams eventually build by hand, badly. It takes the three Stripe events that actually matter for your books (charge.succeeded, charge.refunded, payout.paid), routes each one down its own path, finds or creates the matching Xero record, separates the Stripe processing fee into its own expense line so your net deposit ties out exactly, and posts a Slack alert only when the numbers genuinely disagree. Every event is logged to an audit table before and after processing, so nothing is ever lost silently.

Workflow diagram

flowchart TD
  A[Stripe Webhook] --> B[Log Raw Event]
  B --> C[Validate Signature]
  C --> D{Signature Valid?}
  D -->|No| E[Reject]
  D -->|Yes| F{Route Event Type}
  F -->|charge.succeeded| G[Find Xero Invoice]
  F -->|charge.refunded| H[Find Invoice For Refund]
  F -->|payout.paid| I[Reconcile Bank Feed]
  G --> J{Invoice Exists?}
  J -->|Yes| K[Update Invoice]
  J -->|No| L[Create Invoice]
  K --> M[Calculate Net Amount]
  L --> M
  M --> N[Post Stripe Fee Expense]
  H --> O[Create Credit Note]
  N --> P[Merge All Cases]
  O --> P
  I --> P
  P --> Q{Amounts Match?}
  Q -->|No| R[Slack Mismatch Alert]
  Q -->|Yes| S[No Action]
  R --> T[Log Reconciliation Entry]
  S --> T

Every branch shown here (IF/Switch outcomes) exists as a real conditional in the downloadable JSON, not a simplification for this diagram.

Why reconciliation breaks without automation

The arithmetic problem at the heart of Stripe reconciliation is simple to state and tedious to solve: Stripe charges a customer a gross amount, deducts a processing fee, and deposits the net. Your accounting system records the gross invoice. Your bank feed shows the net deposit. Those two numbers will never match on their own, and closing the gap manually means opening a Stripe payout report, exporting a CSV, and matching line by line against Xero invoices.

For a business processing a few dozen transactions a month, that is an annoying afternoon. At a few hundred transactions, it is a recurring multi-day task that someone dreads. And the failure mode is not that it takes long, it is that under time pressure people start matching in bulk and stop checking edge cases. Partial refunds, disputed charges, multi-currency payouts and subscription proration are exactly the transactions most likely to be wrong, and exactly the ones a rushed manual reconciliation glosses over.

The deeper issue is that manual reconciliation produces no audit trail of its own. If a number is wrong three months later, there is no record of what was matched against what, or why. This workflow treats the audit log as a first-class output, not an afterthought: every raw event is written to a log table before processing begins, and every reconciliation outcome is written again after it completes.

Architecture: why the branches are structured this way

The workflow has a deliberate shape. A single webhook entry point receives all Stripe events, but the very first thing it does is log the raw payload, before any validation or business logic. This matters: if a later node throws, the raw event is already on disk and the execution can be replayed from that point rather than lost. This is the single most common structural mistake in hand-built n8n reconciliation flows, validation and processing happen before any durable record exists, so a failure means the event is simply gone.

Signature verification comes second, not first, for the same reason. An invalid signature is still worth logging (it may indicate a misconfigured endpoint or an attack), but it must never reach Xero. The IF node after verification dead-ends invalid requests into a no-op rather than throwing, so a burst of bad requests does not fill your execution error log.

The Switch node then fans out into three genuinely different code paths. A successful charge needs an invoice lookup, a possible create, a fee calculation and a fee expense line. A refund needs a credit note against an existing invoice. A payout needs bank-feed line reconciliation. Trying to handle all three in one linear path with nested IFs is how these workflows become unmaintainable, the Switch keeps each concern visually and logically separate, and the Merge node at the end rejoins them into one stream for the shared mismatch-check and audit-log logic.

The fee separation logic that makes the books tie out

The Calculate Net Amount node is short but it is the entire point of the workflow. Stripe reports a gross amount and a fee. Xero needs three things to reconcile cleanly: an invoice for the gross amount, a bank transaction for the net deposit, and an expense entry for the fee. Most naive integrations record only the gross, which leaves a permanent unexplained gap between the invoice total and the bank feed.

By posting the Stripe fee to a dedicated expense account (Post Stripe Fee Expense), the bank feed reconciliation in Xero becomes a one-click confirm rather than a manual investigation. Your accountant sees processing fees as a proper, categorised business expense rather than a mysterious shortfall, which also means the number is available for actual analysis: fee as a percentage of revenue, fee trend over time, effect of a pricing or payment-method change.

Failure handling and what happens when a step breaks

The Mismatch Check IF node is the workflow's safety net. After all three branches rejoin, it compares the calculated net amount against what Xero actually recorded. If they agree, execution proceeds straight to the audit log with no noise. If they disagree, a Slack alert fires with the order context attached, and execution still continues to the audit log so the mismatch itself is recorded.

This is an important design choice: the alert path does not terminate the workflow. Both branches merge back together before the final log node, so the audit table contains a complete record of every event, reconciled or not. A workflow that alerts and stops leaves gaps in its own history exactly where the interesting data is.

In n8n, set the workflow's error workflow to a shared handler if you run several of these. Node-level retry (Settings → Retry on Fail) is worth enabling on the Xero HTTP nodes specifically, since Xero's API rate-limits aggressively and a brief 429 should not escalate to a human.

Node-by-node reference

Node Type Role
Stripe Webhook Webhook Entry point for charge.succeeded, charge.refunded, payout.paid
Log Raw Event HTTP Request Writes the untouched payload to an audit table before any processing
Validate Webhook Signature Code HMAC-SHA256 verification against the Stripe signing secret
Signature Valid? IF Gates everything downstream, invalid requests dead-end
Route Event Type Switch Fans out into three independent processing paths
Find Xero Invoice HTTP Request Looks up an existing invoice by reference
Invoice Exists? IF Decides between update and create
Update / Create Xero Invoice HTTP Request ×2 The two halves of the upsert
Calculate Net Amount Set gross − Stripe fee = actual bank deposit
Post Stripe Fee Expense HTTP Request Writes the fee as its own categorised expense line
Create Xero Credit Note HTTP Request Refund path, offsets the original invoice
Reconcile Bank Feed Line HTTP Request Payout path, matches the deposit in Xero
Merge All Cases Merge Rejoins all three branches into one stream
Mismatch Check IF Compares calculated net against recorded Xero amount
Post Slack Mismatch Alert HTTP Request Fires only on genuine discrepancies
Log Reconciliation Entry HTTP Request Final audit row for every processed event

24 total nodes in the downloadable file, including sticky-note documentation embedded directly on the canvas.

Key logic, in code

Stripe signature verification (Validate Webhook Signature node)

const crypto = require('crypto');

const sigHeader = $input.first().json.headers['stripe-signature'];
const rawBody   = JSON.stringify($input.first().json.body);
const secret    = $env.STRIPE_WEBHOOK_SECRET;

const parts = Object.fromEntries(
  sigHeader.split(',').map(p => p.split('='))
);

const signedPayload = `${parts.t}.${rawBody}`;
const expected = crypto
  .createHmac('sha256', secret)
  .update(signedPayload)
  .digest('hex');

// Constant-time comparison avoids leaking timing information
const signatureValid = crypto.timingSafeEqual(
  Buffer.from(expected),
  Buffer.from(parts.v1)
);

return [{ json: { ...$input.first().json, signatureValid } }];

Net amount calculation (Calculate Net Amount node)

// Stripe reports amounts in the smallest currency unit (cents)
const gross = $json.data.object.amount / 100;
const fee   = $json.data.object.application_fee_amount
  ? $json.data.object.application_fee_amount / 100
  : (gross * 0.029 + 0.30);   // fallback to standard Stripe pricing

return [{
  json: {
    ...$json,
    grossAmount: gross,
    stripeFee:   Number(fee.toFixed(2)),
    netAmount:   Number((gross - fee).toFixed(2)),
    currency:    $json.data.object.currency.toUpperCase(),
  }
}];

Before / after

Metric Before After this workflow
Monthly reconciliation time 4–8 hours Under 15 minutes of review
Time to close the books End of month scramble Same-day, continuously
Unexplained bank feed gaps Common, fees not separated Zero, fees posted as expense
Audit trail None beyond the CSV export Every event logged twice, raw and resolved

Prerequisites

  • A self-hosted or cloud n8n instance (v1.40+ recommended for the Switch v3 node)
  • Stripe account with webhook endpoint access and the signing secret
  • Xero account with OAuth2 app credentials and the accounting.transactions scope
  • A Slack workspace with a bot token carrying the chat:write scope
  • An Airtable base (or substitute any database node) for the two audit tables

Common pitfalls

Do not skip signature verification in production

A webhook endpoint without signature verification accepts any POST from anyone who discovers the URL. For a workflow that writes to your accounting system, that is a genuinely serious exposure, not a theoretical one.

Xero rate-limits harder than most APIs

Xero enforces both a per-minute and a daily call limit. Enable node-level retry with backoff on all Xero HTTP nodes, and if you process high volume, add a Split In Batches node ahead of the Xero calls.

Multi-currency needs explicit handling

The template assumes a single currency. If you take payments in several, the Calculate Net Amount node must convert using the rate at settlement date, not the rate at charge date, or your books will drift.

Test with Stripe CLI before going live

Run stripe trigger charge.succeeded against your development webhook URL first. Reconciliation bugs discovered in production are expensive to unwind because they have already written to your ledger.

Want this deployed, configured and monitored?

The template is free. Wiring in your real credentials, tuning the logic to your business, and keeping it running when an upstream API changes is what we do.

Get my free automation plan →

Frequently asked questions

Does this work with QuickBooks instead of Xero?

The structure is identical, only the four Xero HTTP Request nodes change to QuickBooks endpoints. The signature verification, routing, fee calculation and alerting logic are unchanged.

How does it handle partial refunds?

The charge.refunded branch creates a Xero credit note for the refunded amount specifically, not the full invoice, so partial refunds offset correctly rather than zeroing out the original sale.

What happens if Xero is down when an event arrives?

The raw event is already logged by node 2, so nothing is lost. Enable retry on the Xero nodes, and failed executions can be replayed from the log table once Xero recovers.

Can I run this on n8n Cloud?

Yes. The only node requiring care is Validate Webhook Signature, which uses the crypto module, available in n8n Cloud's Code node by default.