The fee total is probably not wrong. Your Stripe fees CSV and your internal sales report are measuring different layers of the same payment flow, so a raw comparison turns ordinary processor costs into fake mismatches.

A sales report tracks orders and internal revenue logic. Stripe's fees export tracks fee events tied to charges, refunds, disputes, or other balance activity. Until the period, reference key, and amount basis match, you cannot tell whether fees are missing or whether the files were never comparable.

Diagnose the mismatch before you total anything

Most bad fee comparisons can be classified before you write a formula. The two files usually disagree because the same commercial event is stored in different shapes.

What you see on screenHow the internal sales report stores itHow the Stripe fees CSV stores itWhat happens when matched raw
Order #10428 for 120.00One row keyed by order number, dated in local timeOne fee row keyed by ch_... or another Stripe-side referenceNo direct match even though the fee belongs to that sale
Sale dated 2026-05-31Local sale date inside the month-end reportFee incurred just after midnight UTC on 2026-06-01Same sale drops outside the filter and looks like a missing fee
Sales report shows 97.00 net product revenueTax and shipping excluded from the internal numberStripe fee assessed on the full customer chargeEffective fee rate looks too high because the denominator changed
Refunded order no longer appears in current-period salesOriginal sale removed or netted down in the reportOriginal processing fee still sits against the charge historyFee looks orphaned or overstated against current sales
Additional fee line appears with no obvious saleInternal report has no bucket for cross-border, FX, or dispute-related costsStripe exports the fee as its own eventTotal Stripe fees look inflated if you total every fee line together
Amounts look identical but still fail to matchAmount or key stored as textAmount or key stored as a number or cleaned textExact lookups fail before reconciliation even starts

Each row needs a different fix. Classify the mismatch first, then correct the comparison.

Define what each file is actually measuring

FileTypical columnsWhat the file is really telling you
Internal sales reportOrder ID, order date, gross sales, discounts, tax, shipping, net sales, maybe booked processor feeWhat the business sold, billed, or recognized internally
Stripe fees CSVincurred_by, incurred_by_type, fee_description, amount, currency, fee date, balance transaction referenceWhat Stripe charged as a fee, when it charged it, and what object caused it

One sale can create more than one Stripe-side event. The original charge creates a processing fee. A later refund can change the economics of that order without restoring the original fee. A dispute can create a separate cost later.

That is why this article is not the same workflow as matching a Stripe payout CSV to your bank statement. A payout-to-bank match proves cash. A fees-to-sales comparison proves whether processor costs were captured correctly for the sales you are reviewing.

The practical question is which in-scope Stripe fees are already reflected internally and which are missing or understated. If the files cannot answer that, the problem is structure, not math.

Set the comparison boundary before you touch formulas

Do not total anything until both files use the same window and amount basis.

Boundary checkWhy it mattersWhat you need to standardize
Period basisSales reports often use local order date. Stripe fee exports can use UTC or fee-incurred datePick one reporting basis for both sides before filtering
TimezoneA late-night charge can move into the next UTC dayConvert Stripe-side dates to the same reporting timezone as your sales report
CurrencyOne side may be local currency while Stripe fee rows stay in settlement currencyCompare only rows in the same currency or split the report first
Amount basisInternal report may exclude tax, shipping, or discounts while Stripe fee is assessed on the full chargeDecide whether the denominator is customer charge amount or an internal net-sales figure
Status scopeVoids, unpaid invoices, or uncaptured orders can sit in a sales report without a finalized Stripe feeLimit the comparison to completed charge activity

If every difference is off by one day, test the date logic first. That usually means a cutoff or timezone problem, not a missing fee. Why Stripe UTC timestamps mismatch bank statement CSV dates covers the same failure from the payout side, and the rule is the same here: compare local reporting dates to local reporting dates, not UTC raw values to local summaries.

Check the imported types before you trust the cells. Use TYPE() because it tells you directly whether Excel sees text or a number.

  • =TYPE(A2) on a date or amount should return 1 for a numeric Excel value.
  • =TYPE(A2) returning 2 means the value is text and cannot be compared safely yet.

If an amount came in as text with commas, force it back to a number inline:

Formula
=VALUE(SUBSTITUTE(A2,",",""))

If a reference is stored as a number on one side and text on the other, standardize it before the lookup:

Formula
=TEXT(A2,"0")

If a date arrived as text and the format is ambiguous, extract the parts explicitly instead of hoping Excel guesses correctly. For MM/DD/YYYY text:

Formula
=DATE(RIGHT(B2,4),LEFT(B2,2),MID(B2,4,2))

For DD/MM/YYYY text, swap the month and day positions:

Formula
=DATE(RIGHT(B2,4),MID(B2,4,2),LEFT(B2,2))

Those checks do not solve the whole comparison. They only stop import damage from masquerading as a fee issue.

Build a bridge key, or admit you do not have one yet

Fee comparisons fail on reference before arithmetic. The internal sales report and the Stripe fees CSV often use different identifiers for the same transaction.

Use the strongest bridge available:

Match optionReliabilityWhen to use it
Stripe charge ID or payment reference in both filesHighestBest case. Fee row can be tied directly to the sale
Order ID carried into Stripe metadata and surfaced in an exportHighGood if your platform pushes the same order reference into Stripe
Exact amount + normalized local date + currencyMediumFallback only when no durable reference exists
Customer email, customer name, or rounded totalsLowManual review only, never primary matching logic

If your internal sales report has only order numbers and your Stripe fees export has only Stripe charge IDs, those two files alone do not support a reliable fee-by-fee comparison. Pull a bridge export that maps the order to the Stripe charge. Without that bridge, the review is defensible only at the grouped-total level.

When the key exists but the types differ, fix the mismatch inside the lookup before creating helper columns. If the internal reference in A2 is text and the Stripe-side key column is numeric:

Formula
=XLOOKUP(VALUE(A2),Stripe!$F:$F,Stripe!$G:$G,"No match")

If the internal reference is numeric and the Stripe-side key is text:

Formula
=XLOOKUP(TEXT(A2,"0"),Stripe!$F:$F,Stripe!$G:$G,"No match")

Do not compare on gross amount alone unless the file is tiny and you are proving each row manually.

Separate fee types before you decide fees are missing

A Stripe fees CSV is not one clean line called "processing fee" repeated all month. If you total every fee row together and compare it to one processor-fee column in the sales report, you will almost always overstate the difference.

Group the fee rows first.

Stripe fee lineCompare directly to current sales in scope?Why
Processing fee on a captured chargeYesThis is the core fee most teams expect to see against current sales
Fee impact tied to a later refundTrack separately from current sales totalThe sale may have dropped out of the current report while the original fee economics still matter
Cross-border or currency-conversion costSeparate bucketIt changes effective rate and is often invisible in internal sales summaries
Dispute or chargeback-related costSeparate exception bucketIt is real, but it is not the same thing as standard processing cost
Non-payment product or account feesExclude from sales-fee comparisonThese belong to a different cost analysis, not a per-sale fee test

Split the file by what incurred the fee before you calculate anything.

Internal reports often book one processor-fee column based on an expected rate, while the Stripe export shows extra fee components that never made it into that estimate. The reverse can happen too when the files use different period bases. Both are scope problems before they are accounting problems.

Run the comparison that actually finds missing or understated fees

Once the period, timezone, amount basis, and bridge key are stable, the comparison becomes mechanical.

  1. Filter the Stripe fees CSV to the reporting window using the normalized local date, not the raw imported text.
  2. Remove fee lines that do not belong in the current sales-fee comparison.
  3. Map each remaining fee row to the strongest available bridge key.
  4. Aggregate the Stripe fee rows to the same grain as the internal sales report.
  5. Compare the internal booked fee to the Stripe total at that same grain.
  6. Classify every difference before you correct anything.

A useful output looks like this:

Match keyGross sale in reportFee in internal reportFee in Stripe CSVDifferenceWhat it usually means
ORD-10428120.003.783.780.00Correctly booked
ORD-10429240.005.206.100.90Internal report understated the actual fee
ORD-1043088.002.550.002.55Wrong period, wrong key, or missing Stripe-side row in scope
ORD-098710.00 in current report0.003.403.40Fee tied to a sale that later dropped out or was refunded
No bridge key150.004.354.35 across two Stripe rows0.00 total, but unproven by rowPeriod total may be right even though transaction proof is weak

This is the point where you can distinguish real failures from expected differences:

  • A missing fee is a fee present in Stripe that should be in scope and has no corresponding amount in the internal report or books.
  • An understated fee is a fee present in both places, but the internal amount is lower because it ignored part of the actual Stripe cost.
  • A timing difference is a fee that belongs to the commercial activity you care about but falls into a different reporting window under the chosen basis.
  • An unproven fee is a fee that looks right in total but cannot be tied back to a sale because the bridge key does not exist.

If your internal sales report does not carry a fee column, use Stripe's actual fee data to identify what should have been booked, then measure the gap against whatever fee expense or accrual exists elsewhere in your records.

Do not use percentage alone as proof. Use the blended rate only as a reasonableness check after the row-level or grouped comparison is classified.

What a finished review should prove

By the end of the review, you should be able to answer more than "the totals are close."

QuestionWhat the finished comparison should tell you
Which sales are in scope?Exact period, timezone, and currency boundary used
Which Stripe fee rows belong to those sales?Processing-fee set after non-comparable fee lines were separated
Which fees already exist in the internal report?Matched amount and matched row count or grouped total
Which fees are missing or understated?Clear exception list with amount and likely reason
Which rows still cannot be proven?Transactions that need a bridge export or a scope correction

If the files do not share a usable key, more filtering will not solve it. Pull the bridge export or accept that the comparison is only defensible at the grouped-total level.

When the file-by-file routine becomes the real cost

This becomes expensive when every close requires the same routine: normalize dates, repair types, separate fee classes, rebuild the match key, aggregate the Stripe rows, and explain the same exceptions again.