Stripe gives you payment_intent, charge_id, payout IDs, fees, and net movement. FreshBooks gives you invoice numbers, client names, paid dates, invoice totals, and payment status. Those fields can describe the same client payment and still share no reliable column for a direct match.
That is where the reconciliation breaks for consultants reviewing a client's month. The client says FreshBooks is paid. Stripe says the payment exists. The bank says a lower net deposit arrived two days later. None of those statements proves the invoice was matched correctly.
The file-level answer is to build the bridge yourself: identify which Stripe record is the payment, identify which FreshBooks row is the invoice or payment, decide whether you are matching gross invoice value or net payout cash, then produce an exceptions list that explains every unmatched row.
Know Which Stripe File You Have
Stripe exports can look similar on screen while answering different questions. A payments export, balance transactions export, payout report, and fees export should not be matched to FreshBooks in the same way.
| Stripe file or view | What it shows | FreshBooks file it can support |
|---|---|---|
| Payments or charges export | Individual customer payments before payout settlement | Invoice payments or paid invoice rows |
| Balance transactions export | Gross amount, fee, net, refunds, and adjustments | Invoice payments plus fee or adjustment review |
| Payout reconciliation report | The batch that reached the bank | Bank or cash reconciliation, not invoice matching by itself |
| Fees export | Processor fee rows | Expense posting or gross-to-net explanation |
| Refunds export | Negative payment activity | Credit notes, refunds, or invoice corrections |
The wrong export creates the wrong answer. If you compare a Stripe payout total to FreshBooks invoice totals, the file will look short because fees, refunds, and settlement timing sit between invoice value and cash. If you compare a Stripe payment export to the bank statement, the file will look high because the bank receives net payout cash, not gross customer payment rows.
For the FreshBooks invoice match, start with Stripe payment records. Bring in payout and fee data only after the invoice-level match is clear.
That distinction matters when the client infrastructure is mixed. One client may have a working Stripe connection. Another may export FreshBooks manually because the integration stopped syncing payment references. The consultant still needs a verification layer that works from the raw files.
Read the FreshBooks Export as Invoice Data, Not Cash Data
FreshBooks invoice exports are built around billing records. Depending on the report, the CSV may show invoice issue date, due date, paid date, client name, invoice number, status, total, balance, and payment details. Those are useful fields, but they do not mean the file is a cash ledger.
Start by separating invoice rows from payment rows.
| FreshBooks field | How to use it in the match |
|---|---|
| Invoice number | Strongest FreshBooks-side reference when Stripe metadata contains it |
| Client name | Secondary check when no shared invoice number exists |
| Invoice date | Billing context only; not the cash match date |
| Paid date | Useful date for matching Stripe payment timing |
| Invoice total | Gross amount expected from the client |
| Paid amount | Amount to compare against Stripe payment value |
| Balance due | Tells you whether a difference is a partial payment |
| Status | Keeps draft, unpaid, and void invoices out of the cash match |
| Currency | Required before comparing any amount |
Do not compare every invoice in the export to Stripe. Filter to invoices that are paid, partly paid, or relevant to the period under review. Unpaid invoices belong in receivables. They do not belong in the Stripe payment match unless the question is whether FreshBooks missed a payment.
If the FreshBooks export is being compared to bank activity as well, the adjacent workflow is matching FreshBooks invoice CSVs to monthly bank statements. For Stripe-to-FreshBooks, stay one layer earlier: invoice payment to Stripe payment.
Build a Match Key From the Strongest Shared Evidence
The best match key is an invoice number stored in Stripe metadata. That happens when the payment was created from the invoice flow or when the client passed the invoice number into Stripe as a description, statement descriptor, or metadata field.
When it exists, use it first.
| Stripe field | FreshBooks field | Match strength |
|---|---|---|
metadata.invoice_number | Invoice number | Strong |
description containing invoice number | Invoice number | Strong if the text is consistent |
| Customer email | Client email or contact | Useful, not enough alone |
| Customer name | Client name | Useful, weak with repeated clients |
| Amount | Paid amount | Useful, weak when amounts repeat |
| Created date | Paid date | Useful inside a date window |
| Payment ID or charge ID | Payment reference | Strong only if FreshBooks stores it |
If the invoice number is missing from Stripe, build a composite key. A composite key combines fields that are not reliable alone but become reliable together.
Use this order:
- Invoice number or payment reference, if present.
- Client email plus amount plus date window.
- Client name plus amount plus date window.
- Amount plus date window plus description fragment.
- Amount-only matching only as a temporary candidate list.
Amount-only matching is not a finished reconciliation. Two clients can pay 1,500.00 in the same week. One client can pay two invoices for the same amount. A recurring advisory invoice can repeat every month. Amount narrows the field; it does not prove the match.
The date window also needs discipline. Stripe's created timestamp is when the payment event happened. FreshBooks paid date may be when the invoice was marked paid. The bank payout date is later. Comparing all three as if they were the same date creates false exceptions.
For invoice payment matching, allow a small window between Stripe payment creation and FreshBooks paid date. For payout-to-bank work, use the payout date and bank posting date instead. Those are different tests.
Normalize the Fields Before You Trust Any Lookup
The first pass should make both files comparable without changing the source records.
Create a working copy or helper columns for:
| Helper field | What it fixes |
|---|---|
| Normalized invoice number | Removes spaces, casing differences, prefixes, and punctuation |
| Normalized client name | Reduces spelling and casing differences |
| Client email | Gives a more stable identifier than client name |
| Signed amount | Aligns Stripe and FreshBooks debit or credit signs |
| Amount basis | Labels gross payment, fee, refund, partial payment, or net payout |
| Date window | Prevents false mismatches caused by one- or two-day timing gaps |
| Currency | Blocks accidental matches across currencies |
For Normalized invoice number, strip the prefix so "INV-1048" in FreshBooks and "Invoice 1048" in a Stripe description reduce to the same token:
=TRIM(SUBSTITUTE(UPPER(A2),"INV-",""))Column A is the invoice number or Stripe description field. For Signed amount, convert text and remove sign direction:
=ABS(VALUE(B2))Column B is the amount field. VALUE converts text amounts; ABS normalizes sign when Stripe shows positive charges and FreshBooks may show negative credits.
This is where a normal spreadsheet lookup often fails. It assumes the shared key already exists. In this workflow, it usually does not. The work is not "find the matching column." The work is "build enough evidence that two rows represent the same payment."
For example:
| Stripe field | Value |
|---|---|
| charge_id | ch_3R81K2 |
| created | 2026-05-08 22:41:19 UTC |
| customer_email | ops@exampleclient.com |
| description | Payment for Invoice 1048 |
| amount | 2500.00 |
| fee | -76.50 |
| net | 2423.50 |
FreshBooks might show:
| FreshBooks field | Value |
|---|---|
| invoice number | INV-1048 |
| client | Example Client LLC |
| paid date | 2026-05-09 |
| invoice total | 2500.00 |
| paid amount | 2500.00 |
| status | Paid |
Those rows match by invoice number and gross amount. They do not match by exact date, and they do not match by net cash. If you compare Stripe net to FreshBooks paid amount, you create a fake difference of 76.50. That difference is the Stripe fee, not a missing invoice payment.
Separate Payment Matching From Payout Explanation
Stripe adds a second layer after the invoice is paid: settlement. The payment can match the FreshBooks invoice perfectly while the cash that reaches the bank is lower or later.
Keep the layers separate:
| Layer | Stripe source | FreshBooks source | Expected result |
|---|---|---|---|
| Invoice payment | Payments or charges export | Paid invoices or payment records | Gross amount should match |
| Fee explanation | Balance transactions or fees export | Expense posting review | Fee should explain gross-to-net gap |
| Refund or credit | Refund export or balance transaction | Credit note, refund, or adjusted invoice | Negative activity should be classified |
| Payout cash | Payout report | Bank statement or cash account | Net payout should match bank |
If a client paid a 2,500.00 invoice by card, FreshBooks may show a paid invoice for 2,500.00. Stripe may show:
| Stripe category | Amount |
|---|---|
| Gross payment | 2,500.00 |
| Stripe fee | -76.50 |
| Net to payout | 2,423.50 |
The FreshBooks invoice is not short. The cash deposit is net of fees. That is a payout or bank reconciliation question. The invoice match is already resolved.
This is the same separation used when matching a Stripe payout CSV to your bank statement: first prove the payment or payout being compared, then explain the transactions inside the cash movement.
Work the Exceptions in the Right Order
Once direct matches are removed, the remaining rows need labels. Do not leave them as "unmatched." That tells the client nothing and gives you no audit trail for the next month.
Use a practical exception sequence:
Invoice number exists in FreshBooks but not in Stripe.
Check whether the invoice was paid outside Stripe, marked paid manually, paid by bank transfer, or carried from another processor. If the payment route was not Stripe, remove it from the Stripe match and classify it separately.
Stripe payment exists but no FreshBooks invoice matches it.
Check whether the invoice number was missing from metadata, the client name changed, the payment was for a deposit, or the invoice was created after the payment. If no FreshBooks record exists, it is a real accounting exception.
Same client and amount, date outside the window.
Check UTC timing, manual paid-date edits, and month-end cutoffs. A one-day shift is usually timing. A two-week gap needs explanation.
Same invoice, different amount.
Look for partial payment, overpayment, discount, tax treatment, currency conversion, or a fee being netted incorrectly. Do not force the full invoice total to equal the paid amount when FreshBooks shows a balance still due.
Refund appears in Stripe but FreshBooks still shows the invoice as paid.
Check whether a credit note or refund was recorded. If not, the invoice match is incomplete because the payment was later reversed or reduced.
Duplicate candidates remain after matching.
Use invoice number first, then email, then description, then date sequence. If two rows still cannot be distinguished, leave them open as duplicate candidates rather than choosing one by feel.
The output should tell the next reviewer what happened:
| Status | Meaning | Action |
|---|---|---|
| Matched | Stripe payment and FreshBooks invoice agree on reference and gross amount | No action |
| Timing difference | Same payment, date differs within expected window | Note timing |
| Fee explanation | Invoice gross agrees, Stripe net is lower because of fee | Post or verify fee handling |
| Partial payment | Stripe payment covers only part of the invoice | Leave balance open |
| FreshBooks-only | Paid invoice has no Stripe payment | Check other payment route or manual entry |
| Stripe-only | Stripe payment has no invoice | Find missing invoice or classify receipt |
| Refund missing | Stripe refund has no FreshBooks credit or refund record | Record or investigate refund |
A consultant does not need a beautiful workbook at this stage. They need a defensible answer: which invoices matched, which Stripe payments have no invoice, which FreshBooks paid invoices were not paid through Stripe, and which differences are timing, fee, refund, or partial-payment issues.
A Worked Example
Suppose the Stripe payments export shows:
| Stripe payment | Description | Created | Gross | Fee | Net | |
|---|---|---|---|---|---|---|
ch_1001 | ops@northco.test | Invoice 1048 | 2026-05-08 | 2,500.00 | -76.50 | 2,423.50 |
ch_1002 | ap@northco.test | Invoice 1051 | 2026-05-10 | 1,200.00 | -37.20 | 1,162.80 |
ch_1003 | finance@eastco.test | Deposit | 2026-05-12 | 800.00 | -25.10 | 774.90 |
re_2001 | ops@northco.test | Refund INV-1048 | 2026-05-20 | -500.00 | 0.00 | -500.00 |
FreshBooks shows:
| Invoice | Client | Paid date | Paid amount | Balance | Status |
|---|---|---|---|---|---|
INV-1048 | North Co | 2026-05-09 | 2,500.00 | 0.00 | Paid |
INV-1051 | North Co | 2026-05-10 | 1,200.00 | 0.00 | Paid |
INV-1059 | East Co | 2026-05-12 | 1,600.00 | 800.00 | Partly paid |
The matches are:
| FreshBooks invoice | Stripe record | Result | Reason |
|---|---|---|---|
INV-1048 | ch_1001 | Matched | Invoice number, client, and gross amount agree |
INV-1051 | ch_1002 | Matched | Invoice number and gross amount agree |
INV-1059 | ch_1003 | Partial payment | Stripe payment covers 800.00; FreshBooks balance remains 800.00 |
INV-1048 refund | re_2001 | Review | Stripe shows a later refund; FreshBooks needs a matching credit or refund record |
The net amounts do not equal the invoice amounts because Stripe fees are present. That does not damage the invoice match. It creates a second task: verify that fees were posted and that the payout later agrees to the bank.
The finished note should read like this:
| Category | Count | Amount |
|---|---|---|
| Matched paid invoices | 2 | 3,700.00 |
| Partial payments matched | 1 | 800.00 |
| Stripe fees to verify | 3 | -138.80 |
| Refunds needing FreshBooks review | 1 | -500.00 |
| Unexplained difference | 0 | 0.00 |
That is a usable reconciliation result. It tells the client that the payment records match the invoices, one invoice remains partly paid, the fees are a separate posting check, and one refund needs FreshBooks treatment.
Use the Same Verification Layer Next Month
The durable process is not tied to one client's export layout. It is a sequence:
- Identify the Stripe file type.
- Identify whether the FreshBooks file shows invoices, payments, or both.
- Filter FreshBooks to paid and partly paid records.
- Normalize invoice numbers, client names, emails, dates, currency, and signs.
- Match by invoice number or payment reference first.
- Match by email, amount, and date window second.
- Separate gross invoice matching from Stripe fees and payout cash.
- Label every exception by cause.
- Produce a summary that another reviewer can understand without reopening both exports.
This is how independent consultants match Stripe payments FreshBooks invoices without depending on a client integration being configured correctly. The client can change their export columns. FreshBooks can omit the Stripe ID. Stripe can settle the payment two days later. The workflow still holds because it tests the business event, not a fragile single-column lookup.
