Skip to content

Stripe Payout Reconciliation: Matching Deposits to Orders

A Stripe payout is a batch of balance transactions, not orders. What it takes to match deposits, fees, refunds, and sales tax to sales, and how Flint does it.
Reconciliation10 min readBy Flint Pay, Product & API Team

A Stripe deposit is a batch of balance transactions, and the orders it paid for live in your database, joined by a PaymentIntent ID or metadata. Someone runs that join every month.

Tip: The short answer

  • The problem: Stripe lists the balance transactions in a payout, not the orders, so every row needs a join to your own tables.
  • What Flint does: every balance transaction that belongs to an order carries its order_id, and one request lists a payout's rows with their orders inlined.
  • What you stop owning: the PaymentIntent-to-order mapping, the month-end join, and the tax reversal call in every refund handler.

At a glance#

Which orders are in this deposit?Stripe PaymentIntentsFollow source to a PaymentIntent, then join your tableFlintpayout_id filter with expand=order
A refund issued outside your codeStripe PaymentIntentsReaches your tables only through your webhook handlerFlintSettles against the order it refunds
A disputeStripe PaymentIntentsAn adjustment row; amount can differ from the chargeFlintA dispute row with the order's order_id
A failed payoutStripe PaymentIntentsTransactions retried under a retried_payout_idFlintRows keep payout_id; returned money posts as a new row
Tax on a partial refundStripe PaymentIntentsSpread across lines, or your code computes each reversalFlintAllocated per line by the refund itself
Tax report by jurisdictionStripe PaymentIntentsCustom integration: as complete as your reversal callsFlinttax_transactions_itemized_v1; fails with data_unavailable rather than leave gaps

Why a Stripe payout doesn't list orders#

An automatic Stripe payout "can include funds from multiple transactions," each a balance transaction with a type, fee, net, and a source object. Of the 52 types, these show up in an ordinary month:

Stripe balance transactionWhat it recordssource
charge or paymentA payment from a customer, with Stripe's feeThe charge (ch_) or payment
refundMoney returned to a customerThe refund (re_)
adjustmentDisputes, dispute reversals, and some correctionsA dispute, for dispute rows
refund_failureA card refund that failed, with the money back in your balanceThe refund, then its payment
stripe_feeFees for Stripe software and services, such as Radar and BillingNot an order
payoutThe payout itselfThe payout (po_)

None of these is an order. Stripe deprecated its original Orders API in October 2019, stripe-node v11 removed the Orders resource, 2022 version included, in November 2022, and Stripe's fulfillment guide now says: "Save a copy of the payment details and line items in your own database."

"When volume grows, reconciliation stops being 1:1," one member wrote on the Shopify Community forum in December 2025. "At some point Excel becomes very fragile and hard to trust." A reply named the worst part:

Most merchants I work with find matching refunds to specific payouts to be the biggest bottleneck.

haidernfactor, Shopify Community, December 2025
Where the sale lives
What was soldStripeLine items, discounts, tax, and refunds by line sit in your tablesFlintThe order is an API object with line items, discounts, tax per line, and charges
Link to the moneyStripeStripe holds amounts, linked by PaymentIntent ID or metadataFlintEvery payment, refund, and dispute settles against the order
Payout rowsStripeThe itemized payout report adds payment_intent_id and payment_metadata[key] columns; the hop to what was sold is your joinFlintBalance transactions carry order_id
MappingStripeWhen the records drift apart, a payout row goes unexplainedFlintStripe processes the cards underneath; you need no Stripe account or mapping table

The month-end job#

Stripe's guide starts with balance transactions filtered by payout, source expanded; the rest is your code. On Flint it's one filtered list and a tie-out check.

TypeScript
import Stripe from "stripe";

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);

export async function reconcilePayout(payoutId: string) {
  const lines = [];

  for await (const txn of stripe.balanceTransactions.list({
    payout: payoutId,
    expand: ["data.source"],
    limit: 100,
  })) {
    const source = txn.source as any;

    switch (txn.type) {
      case "charge":
      case "payment": {
        // Your join: PaymentIntent ID to your orders table.
        const order = await db.orders.findByPaymentIntent(source.payment_intent);
        lines.push({ order, gross: txn.amount, fee: txn.fee, net: txn.net });
        break;
      }
      case "payment_refund":
      case "refund": {
        // Which line items, and how much tax? Only your refunds table knows,
        // and only for refunds your code wrote down.
        const refund = await db.refunds.findByStripeId(source.id);
        const order = await db.orders.findByPaymentIntent(source.payment_intent);
        lines.push({ order, refund, net: txn.net });
        break;
      }
      case "refund_failure": {
        // The refund failed and the money came back. Your refunds table
        // still says refunded unless a webhook handler caught the failure.
        const refund = await db.refunds.findByStripeId(source.id);
        lines.push({ refund, net: txn.net });
        break;
      }
      case "adjustment": {
        // Disputes, dispute reversals, and corrections all arrive here.
        if (source?.object === "dispute") {
          const order = await db.orders.findByPaymentIntent(source.payment_intent);
          lines.push({ order, dispute: source.id, net: txn.net });
        } else {
          lines.push({ unmatched: txn.description, net: txn.net });
        }
        break;
      }
      default:
        // stripe_fee, tax_fee, and the rest belong to no order.
        lines.push({ unmatched: txn.type, net: txn.net });
    }
  }

  return lines;
}

On Flint, net_money = amount_money - fee_money at version 2026-09-07, and the rows' net_money adds up to the payout's amount_money: the deposit ties out to the cent or a page is missing.

What a payout's rows tell you
Payout contentsStripeListed with the payout filter and source expanded; each branch of the walk is your codeFlintPublish whole: every row exists, none belongs to another payout, and they sum to amount_money; until then, 503 with Retry-After, usually for minutes
RefundsStripeA Dashboard refund reaches your refunds table only through a webhook handler; without it, the job finds the order but not the line or taxFlintA refund row settles against the order it refunds
Keeping records currentStripeA refund_failure row returns the money while your table still says refundedFlintHandle balance_transaction.created and deduplicate by balance_transaction_id, since delivery is at least once and unordered (handling repeats)
Row typesStripeDisputes arrive as adjustment rows beside other corrections, and stripe_fee rows belong to no order; spreading them is your callFlintEach type means one thing: payment, refund, dispute, dispute_reversal, return, adjustment
Failed payoutsStripeThe transactions get a retried_payout_id, and your saved payout mapping needs updatingFlintpayout_id never changes; a failed or reversed payout posts a new payout_failure or payout_reversal row for a later payout to carry
Payouts you createStripeThe payout filter lists automatic payouts onlyFlintA payout you create with POST /v1/payouts doesn't list contents; match it to its own payout row

Fees and disputes on one order#

On Stripe, "processing fees from the original transaction aren't returned" on a refund, and a dispute "Debits the disputed amount, plus a dispute fee." A per-order margin report starts from your join.

What a per-order margin report needs
One order's moneyStripeYour margin report starts from your joinFlintGET /v1/balance-transactions?order_id=... returns its payment with fee, refunds, and disputes
Order totalsStripeYour tablesFlintsettlement_amounts: paid_money, refunded_money, written_off_money, and outstanding_money; refund_status is separate from payment_status, so a refunded order still reads as paid
Processing feeStripeEach charge row carries Stripe's feeFlintA $50.00 card payment at 3.79% + 35¢ reports processing_fee_money of 225 and nets $47.75
Fees on a refundStripeA refunded order keeps its processing feeFlintA refund doesn't return the processing fee or any add-on fee, such as the automatic tax fee, and Flint charges nothing to refund
Disputed amountStripeCan differ from the charge: a partial dispute, one covering several recurring charges, or a full dispute after a partial refundFlintA dispute row posts the principal with fee_money of 0
Dispute feeStripeOutside Mexico, the dispute received fee isn't returned if you win, unless your contract says otherwiseFlintA merchant billing event fee (GET /v1/merchant-billing-entries?dispute_id=...), the card network's cost with no Flint markup

Sales tax on refunds and in reports#

A filing needs tax collected and refunded per jurisdiction, with each refund reversing tax on the lines that came back. In the basic Stripe Tax integration for PaymentIntents, "Refund reversals use flat-amount distribution" across every line. In the custom one, "When you issue a refund, you must create a reversal tax transaction with a unique reference."

TypeScript
// After refunding part of one line, in your refund handler.
await stripe.tax.transactions.createReversal({
  mode: "partial",
  original_transaction: order.stripeTaxTransactionId,
  reference: `${order.id}-refund-${refund.id}`,
  line_items: [
    {
      original_line_item: line.stripeTaxLineItemId,
      reference: line.id,
      amount: -refundedAmount,
      amount_tax: -refundedTax, // your code computes this share
    },
  ],
});
Who computes the tax on a refund
Line amountsStripeYour handler sends the tax transaction and line IDs from your tables, and "If you refund parts of line items, you must calculate the amounts refunded," using the discount and tax arithmetic behind refundedTaxFlintThe refund splits each line's refund into subtotal, discount, and tax in line_item_allocations, with no second call
Tax reportingStripe"If you refund a tax amount such that the total tax is no longer proportional to the subtotal, your tax reporting can be unreliable."FlintWith external tax, the refund reverses each jurisdiction row separately, so refunded tax adds up to the cent
Repeat refundsStripeUp to 30 partial reversals per sale, and a full reversal "doesn't affect previous partial reversals"FlintFails with LIMIT_EXCEEDED instead of paying back more than what's left on the order
If a step failsStripeIf the handler fails after the money refund succeeds, the payout carries the refund and the tax report doesn'tFlintThe refund posts as the negative refund row in the payout, with related_resource naming it

Tax reports by jurisdiction#

For automatic tax, tax_transactions_itemized_v1 exports one CSV row per collection or refund reversal, with order_id, order_line_item_id, refund_id, destination address, state, county, city, tax_money, and refund_reversed_tax_money. With external tax, tax_summarized_v1 gives taxable sales, tax collected, tax refunded, and tax payable per jurisdiction and tax type.

cURL
curl -X POST https://api.withflintpay.com/v1/reports \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: tax-transactions-2026-09" \
  -d '{
    "report_type": "tax_transactions_itemized_v1",
    "interval_start_at": "2026-09-01T04:00:00Z",
    "interval_end_at": "2026-10-01T04:00:00Z",
    "timezone": "America/New_York",
    "currency": "USD"
  }'

If a refund in the interval lacks its tax lineage, generation fails with data_unavailable rather than produce a file with gaps. Flint applies, records, and reports tax, and calculates it with automatic tax, but isn't a merchant of record: you register where you owe tax and file the returns.

Run the month end on Flint#

Question at month endOn Flint
What's in this deposit?GET /v1/balance-transactions?payout_id=po_...&expand=order
What did each payment cost?payments_itemized_v1: processing_fee_money, each add-on fee, merchant_net_money, and order_id
What did disputes and ACH returns cost?GET /v1/merchant-billing-entries?type=event_fee
What's been paid and refunded on an order?settlement_amounts on GET /v1/orders/{order_id}
How much tax was collected and refunded?tax_transactions_itemized_v1 for automatic tax, tax_summarized_v1 for external tax
What does the accountant get?payouts_itemized_v1: each paid payout's balance transactions with order_id and arrival_date
ReconciliationBalance transactions, what each movement type means, and matching a deposit to the transactions that funded it.PayoutsSchedules, destinations, payout contents, and what happens when a payout fails or is reversed.ReportsOrder, payment, balance, payout, and tax reports as versioned CSV files.Sales taxAutomatic and external tax on orders, tax on refunds, and the tax reports.

Sign up for a sandbox. Pay a test order with card 4242 4242 4242 4242, refund one line item, and list the order's balance transactions to see the payment, its fee, and the refund on one order. The sandbox is free and needs no credit card.

FAQ#

How do I reconcile Stripe payouts with orders?#

List the payout's balance transactions with the payout filter (automatic payouts only), expand source, and follow each charge, refund, or dispute to its PaymentIntent. Then join that ID to your own orders table, because Stripe doesn't hold the order.

Why doesn't my Stripe payout match my sales?#

A payout is the net of charges minus Stripe's fees, refunds, disputes and their fees, and other adjustments, settled on Stripe's schedule rather than the day of the sale. Refunds don't return the original processing fee, and a dispute can be for a different amount than the charge.

Does Stripe still have an Orders API?#

No. Stripe deprecated the original in October 2019, and stripe-node v11 removed the Orders resource, including the 2022 Orders API, in November 2022.

How do I report sales tax refunded with Stripe Tax?#

The basic PaymentIntents integration spreads each refund's tax reversal across all lines. For line-level reversals, the custom integration has you create a reversal tax transaction per refund, sending each line's refunded amount and amount_tax yourself.

How does Flint connect payouts to orders?#

Every Flint balance transaction that belongs to an order carries its order_id, and GET /v1/balance-transactions?payout_id=...&expand=order returns a payout's rows with their orders inlined. The rows' net_money adds up to the payout's amount_money.

Does Flint file sales tax for me?#

No. Flint calculates US sales tax with automatic tax or records your own engine's rates, reverses tax on refunds, and exports tax reports by jurisdiction, but it isn't a merchant of record, so you register and file the returns.

Sources#

Everything attributed to Stripe above comes from its documentation and public SDK wikis, and the merchant quotes come from a public forum thread, checked on 2026-10-28.

  • Payout reconciliation: automatic payouts "can include funds from multiple transactions," and listing balance transactions by payout and expanding source.
  • Payout reconciliation report: the itemized report's payment_intent_id and payment_metadata[key] columns, and retried_payout_id in the failed payouts report.
  • List all balance transactions: the payout filter for automatic payouts only, and the 52 values of the type filter.
  • Balance transaction types: disputes and dispute reversals as adjustment rows whose source is a dispute, failed refunds as adjustments, and stripe_fee as fees for Stripe software and services.
  • Refund and cancel payments: processing fees from the original transaction aren't returned, and refunds must also be recorded when you use the Stripe Tax APIs.
  • How disputes work: the disputed amount plus a dispute fee debited from your balance, disputed amounts that differ from the charge, and the dispute received fee not returned outside Mexico.
  • Stripe Tax with PaymentIntents and basic Stripe Tax API: the two integrations, and refund reversals using flat-amount distribution.
  • Custom Stripe Tax API: creating tax transactions with a reference, reversal transactions for every refund, computing partial line amounts yourself, the 30 partial reversal limit, full reversals not affecting earlier partial ones, and unreliable reporting for non-proportional tax refunds.
  • Fulfill orders: "Save a copy of the payment details and line items in your own database."
  • stripe-node wiki: Migration guide for v9, the legacy Orders API deprecated in October 2019 because it doesn't work with Strong Customer Authentication, and Migration guide for v11, the Orders and SKU resources removed, and the v11.0.0 release of 2022-11-16.
  • Shopify Community: How do you reconcile Shopify orders with Stripe payouts without losing your mind?, the original post and a reply, both quoted above.

Flint behavior comes from the Reconciliation, Payouts, Reports, Processing fees, Sales tax, Refunds, and Handling disputes guides, and the Money Movement, Reports, and Orders API references.