Match Stripe payments to Xero invoices automatically

By General Input

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

Integrations

  • Stripe
  • Xero
  • Google Sheets
  • Slack Bot

Type

Deterministic Code

Categories

  • Finance
  • Operations

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.

Related prompts

Explore more prompts
Call overdue Xero customers with an AI collections agentLocal listing health board for every location you manageLet support send one-off Loops emails without an engineerA brand asset library your marketing team actually searchesTurn Mailjet email clicks into ranked HubSpot follow-upsClean out the Looker dashboards and Looks nobody opensStop cold emails to anyone with a live deal in PipedriveLiveKit live operations console for room moderationWake up dormant Keap leads with a researched reasonCustomer lifecycle inspector for Loops, Stripe and PostHog