Match Stripe payments to Xero invoices automatically

Every successful Stripe charge finds its matching Xero invoice, records the payment, books the Stripe fee, and logs the reconciliation for finance.

Deterministic Code
StripeXeroGoogle SheetsSlack BotFinanceOperationsData SyncNotifications & Alerts

Build a deterministic Stripe-to-Xero payment reconciliation workflow. Use a code workflow (not an agent) because every branch is structured: match by metadata, match by email and amount, or escalate.

Trigger: a Stripe webhook on charge.succeeded. Optionally also accept payment_intent.succeeded for setups where the invoice ID lives on the PaymentIntent instead of the Charge.

On each event, extract the Stripe charge details: amount, currency, balance_transaction fee and net, customer email, and any metadata.xero_invoice_id or metadata.invoice_number we wrote at checkout. If the webhook payload does not already include the balance transaction breakdown for fee and net, call Stripe Retrieve Charge with the charge ID (expanding balance_transaction) to get it.

Then find the matching Xero invoice using Xero List Invoices, in this order: (1) if metadata.xero_invoice_id is present, fetch that invoice by ID and confirm Status is AUTHORISED with a non-zero AmountDue; (2) else if metadata.invoice_number is present, look up the AUTHORISED invoice with that InvoiceNumber; (3) else look up the Xero contact by the Stripe customer's email via Xero List Contacts, then list AUTHORISED invoices for that ContactID and pick the one whose AmountDue equals the Stripe charge amount (converted to Xero's decimal money format) within a configurable tolerance (default: exact match, configurable to a few cents).

When a unique match is found, record the payment with Xero Create Payment for the gross charge amount against the matched invoice, dated the Stripe charge date, paid into the configured Stripe clearing bank account. Then book the Stripe processing fee separately: call Xero Create Bank Transactions to post a SPEND-money bank transaction against the same Stripe clearing account, with the line allocated to the configured Stripe fees expense account, so the Xero bank account reconciles to the net Stripe payout instead of being off by the fee.

After a successful reconciliation, append a row to the configured Google Sheet via Google Sheets Append Values with: timestamp, Stripe charge ID, Xero invoice number, contact name, currency, gross, fee, net, match method (metadata-id / metadata-number / email+amount), and the resulting Xero payment ID. Then post a one-line confirmation in the configured finance Slack channel via Slack Bot Send a Message.

If no AUTHORISED invoice matches, or multiple invoices tie on email+amount, or the currency on the invoice does not match the Stripe charge currency, do NOT guess. Skip the Xero writes, append an exception row to the same Google Sheet flagged as needing review, and post a Slack alert in the configured exceptions channel tagging the finance user group with the Stripe charge link, the candidate invoice numbers (if any), and the specific reason we could not choose (no match / multiple matches / currency mismatch).

Idempotency: before calling Xero Create Payment, check the reconciliation log (or Xero List Payments filtered by Reference containing the Stripe charge ID) to confirm we have not already recorded this charge. If we have, exit silently so Stripe webhook retries are safe.

Configurable inputs: Xero tenant ID, Stripe clearing bank account code, Stripe fees expense account code, amount-match tolerance in cents, Google Sheet ID and tab name, Slack confirmation channel, Slack exceptions channel, finance user group handle.

Additional information

What does this prompt do?
  • Listens for successful Stripe charges and finds the right Xero invoice using your invoice ID, invoice number, or the customer's email and amount.
  • Records the payment against the invoice in Xero and books the Stripe processing fee separately to your fees account, so your Xero bank balance matches the Stripe payout.
  • Appends a row to a Google Sheets reconciliation log with the charge, invoice, gross, fee, net, and how the match was made.
  • Posts a Slack confirmation on a clean match, or pings the finance team in a separate channel if no invoice fits or several invoices tie.
What do I need to use this?
  • A Stripe account with permission to read charges and configure outgoing webhooks
  • A Xero organisation you can use to read invoices and contacts and create payments and bank transactions
  • A Google Sheet to use as the reconciliation log
  • A Slack workspace, plus the channels where finance gets confirmations and exceptions
How can I customize it?
  • Set how strict the amount match has to be — exact only, or within a small tolerance in cents.
  • Choose which Xero account the Stripe processing fee gets posted to, and which clearing or bank account the payment lands on.
  • Pick the Slack channel for clean reconciliations and the separate channel for exceptions, and decide which user group to tag when something needs a human.

Frequently asked questions

What happens if a Stripe charge does not match any Xero invoice?
We do not guess. The workflow skips the Xero writes, logs an exception row, and posts an alert in your finance Slack channel tagging the team with the charge details and any candidate invoices so someone can decide what to do.
Does this handle the Stripe processing fee correctly?
Yes. We record the gross charge as a payment against the invoice, then post the Stripe fee separately to the fees account you configure. Your Xero bank account stays in sync with the actual Stripe payout instead of being off by the fee.
What if the invoice and the Stripe charge are in different currencies?
Currency mismatches are treated as exceptions rather than guessed at. You can extend the workflow to apply a conversion rate, but the safe default is to escalate to finance.
Will this duplicate payments if Stripe retries the webhook?
No. Every Stripe charge ID is checked against the reconciliation log before we write to Xero. If a charge is already recorded the workflow exits silently, so webhook retries are safe.
Do I have to put the invoice ID in Stripe metadata, or can it match on email?
Both. If your checkout writes the Xero invoice ID or number into Stripe metadata we use that first. Otherwise we look up the contact by the Stripe customer's email and match the open invoice whose amount equals the charge.

Stop hand-matching Stripe charges to Xero invoices.

Connect Stripe, Xero, Google Sheets, and Slack once, and Geni reconciles every payment the moment it lands.