Why your Shopify payout doesn't match your sales, and how to reconcile it

Written by
Stavros Symeonidis, founding engineer
Published
Last updated
Read time
12 min read

Your Shopify payout is smaller than your sales figure, and it will never match it. A payout is the net of a ledger, after fees, refunds, chargebacks, reserves and currency conversion, paid on a settlement schedule unrelated to the day the orders were placed. Shopify says so on its own help page. Both numbers are correct, and the revenue standard requires them to differ. What you can do is tie one to the other, order by order, off the balance transactions ledger, and section 5 works one payout through to the penny.

Why doesn’t my Shopify payout match my sales?

Because the two numbers measure different things on different clocks, and Shopify publishes that fact itself. Its Payout reconciliation report help page says payouts “reflect balance activity after deductions, holds, currency conversion, and adjustments”, and that the report “isn’t a statement of revenue for accounting purposes”. A sales figure counts orders on the day they were placed. A payout counts money that moved, on the day it settled.

The accounting makes it permanent. A DTC brand controls its own goods, which makes it a principal under ASC 606-10-55-36 to 55-40. RevenueHub’s summary is that “a principal recognizes revenues at the gross amount received for the goods and services, while an agent recognizes revenue at the net amount”, and Scrubbed, a CPA firm, draws the consequence for card fees, which “are recorded separately as an operating expense, not as a direct reduction of revenue”. Your books carry revenue gross and the fee as a cost. The bank carries the net.

The usual answer, fees and timing, is true and useless. The job is joining the two numbers, not making them equal, so that every penny between them has a name.

What is a clearing account, and why does the balance never clear?

The accepted method treats Shopify as a bank account that exists only in your books. Sales go in, fees and refunds come out, and the transfer to your real bank should equal the payout exactly. It stops balancing for reasons the vendors document themselves.

Social Commerce Accountants, an accountancy firm that resells the tools, puts it plainly. “Treat Shopify as a virtual bank account. Record all sales into the Shopify Clearing Account. Record fees and refunds out of that same account.” You already have one of these, whatever your bookkeeper calls it.

The same people document where it fails. A2X’s own support pages say its software cannot split out gateway fees, so “there will likely be a small balance in the clearing account, and you will need to create a manual entry”. Ottit, a bookkeeping practice that works with A2X, says “a growing balance means A2X entries aren’t matching bank deposits”, and blames timing, missed payouts, or one A2X has not processed yet. The Seller CPA, another reseller, says Shop Pay Installments needs its own clearing account and a manual fee entry every month, or “the balance sheet account will grow”.

Here is the part nobody says out loud. We looked for a neutral source on this method and found none. No AICPA or ICAEW guidance, no textbook and no Big Four handbook documents it by name. That does not make it wrong. It means the only people describing its failure modes have a product for them, which is why every vendor claim in this piece is labelled as one.

What is actually inside a payout?

A payout is the net of a ledger, not a list of orders. The ledger is the balance transactions on your Shopify Payments account, and it holds far more kinds of movement than the charge, refund, chargeback model most people hold.

Shopify’s developer documentation calls balance transactions “the canonical ‘statement of account’, or ledger for a Shopify Payments account. Every single movement of money in or out of the account is recorded here.” Its type enum runs to roughly a hundred values, and the cleanest published list of what can hit a payout is the reconciliation report’s own activity groups.

Activity group What lands there Starts from an order?
Charges Captured payments, the processing fee on each row Yes, via its order transaction
Refunds One row per refund transaction Yes, to the refund, never to a line item
Disputes The chargeback, taken from the next available payout, plus the hold and the fee Yes, usually in a later payout
Fees Standalone fee lines, chargeback fees among them Sometimes
Adjustments Corrections Shopify posts against the account Sometimes
Reserves and Holds Funds held back, later released or withdrawn No
Installments Recovery The Shop Pay Installments recovery line No
Shopify Capital Repayments taken out of payouts No
Payouts The transfers themselves No
Managed Markets, Duties and Taxes, Shipping, Shop Cash, Shopify Collective, Promotions and Credits, Internal Transfers, Other Programme lines Row by row

Tax on orders placed in the Shop app or on the Shop website comes out of the payout as a line typed “Marketplace sales tax”, because Shopify files and pays that tax itself, as post 05 explains.

Then there is what the table cannot show. In the United States and France, Shopify uses multiple processing partners, and its Scheduling payouts page says you “might receive some payouts sooner than the scheduled payout date, and more than one payout per day”, and in France the bank statement may carry the partner’s name rather than Shopify’s. Gift cards go the other way. When store credit is used, Shopify’s Finance reports page says “nothing displays in the payments finance reports because you didn’t receive a payment”, so a redemption produces no payout entry, though the card’s original sale settles like any charge. And PayPal, Amazon Pay, Klarna and Afterpay settle outside Shopify Payments, so the report excludes “third-party payment methods such as Authorize.net or Afterpay”.

Why can’t I get a payout report by SKU?

Because the money moves at the transaction level and not the line item level, and Shopify confirms no built-in report closes that gap. When a merchant asked for exactly that, Shopify staff answered “Currently, it’s not possible to achieve this through Shopify’s built-in reports”.

Shopify’s developer documentation shows refund money movement recorded per refund transaction. A partial refund across three line items lands on the ledger as one refund row keyed to that transaction, not as three rows keyed to three SKUs. The payout knows which refund it paid. It does not know which product came back. Neither report helps, the reconciliation report does not link to orders and order level detail lives only in the Payouts page CSV.

So SKU economics is built, not exported. Join the order and refund line items to the payout at order grain, then spread each order’s fee across its lines. That spread is a choice, pro rata by line value is only the common one, and whoever picks the rule decides which product carries the cost of the card. A mis-allocated fee is a margin problem, our Unit Economics territory. The SKU question is also the one that sends people shopping for a connector, and the connectors reconcile at transaction grain too.

How do you tie a payout back to its orders?

This is the technical section. Everything an engineer needs is here and nowhere else. Hand it over intact, or skip to section 6.

In plain words first. Every payout has an id, every ledger row carries it and points at the order transaction that produced it, and that points at the order. Make the join and the payout becomes a list of orders with their fees attached, plus a few rows with no order behind them that need a person.

Matching a Shopify payout to its orders

On Shopify’s REST transactions resource each balance transaction carries payout_id, type, amount, fee, net, source_order_id and source_order_transaction_id, and the GraphQL object, ShopifyPaymentsBalanceTransaction, carries the same in camel case. One payout, worked through. The figures are invented and illustrative, the field names, transaction types and arithmetic real. Three orders were captured in the week, one earlier order took a partial refund, and Shopify posted one adjustment against the account.

Ledger row type source_order_transaction_id Order amount fee net
1 charge 7001 #1041 84.00 1.93 82.07
2 charge 7002 #1042 129.50 2.84 126.66
3 charge 7005 #1043 46.00 1.17 44.83
4 refund 7010 #1039 -30.00 0.00 -30.00
5 adjustment none none -4.50 0.00 -4.50
Payout 118234 225.00 5.94 219.06

All figures in GBP. The three orders total £259.50 and the deposit is £219.06. The £40.44 between them is £5.94 of fees, a £30.00 refund and a £4.50 adjustment, every penny of it on the ledger, and the columns tie the way the reconciliation report’s own summary ties, activity before fees £225.00, fees £5.94, net amount £219.06.

Row 4 is a refund on order #1039, paid out weeks earlier. It lands in the payout where it was processed, keyed to the refund transaction, and says nothing about which item came back. Row 5 has no order behind it, the kind of line the report groups under Adjustments, and the kind that leaves a clearing account a few pounds off zero when nobody reads the ledger.

The fee breakdown per order

The fee sits on the charge row, in the fee field, and net is amount less fee, so a single-capture order’s fee is a direct read. Order #1042 paid £129.50, cost £2.84 to take, and put £126.66 in the bank. The refund row’s fee is whatever your account does on refunds, here nothing. Shopify’s payout summary object splits the same figures by type, charges_gross_amount, charges_fee_amount, refunds_gross_amount, refunds_fee_amount, adjustments_gross_amount and adjustments_fee_amount, a second place the ledger has to agree with. Why an order’s fee is often more than the plan rate is in post 03.

When the join points at the wrong transaction

This is the paragraph to read twice. source_order_transaction_id sometimes returns the parent transaction rather than the leaf that moved the money. A developer on the shopify.dev forums described it as “SOMETIMES, it is the direct foreign key, but OTHER TIMES, it is the ID of the PARENT transaction”. No published fix exists. We looked.

It breaks silently. In the example, row 2 carries 7002, and 7002 is the authorisation on order #1042, not the capture. The capture is 7003, with 7002 as its parent. A pipeline that filters order transactions down to the kinds that move money drops row 2 without an error, and the payout reconciles £126.66 short with nothing flagged.

Our fix is in the join. Accept a match on the id or the parent id, restrict the kind to what the ledger row’s type implies, count the matches with an aggregate rather than a window so that every ledger line comes back exactly once, and route anything but exactly one match to a person. One capture under an authorisation matches once, through the parent. Two captures under it count as two, and the line comes back once, flagged, never doubled.

The query

The query lowers type, kind and status before it compares them, because Shopify’s GraphQL API writes them in capitals, CHARGE and SALE, where the REST API writes charge and sale.

with matched as (
  select
    bt.id, bt.type, bt.amount, bt.fee, bt.net,
    bt.source_order_id, bt.source_order_transaction_id,
    min(t.order_id) as order_id,
    count(t.id) as matches
  from balance_transactions bt
  left join order_transactions t
    on (t.id = bt.source_order_transaction_id
        or t.parent_id = bt.source_order_transaction_id)
   and lower(t.status) = 'success'
   and ((lower(bt.type) = 'charge' and lower(t.kind) in ('sale', 'capture'))
     or (lower(bt.type) = 'refund' and lower(t.kind) = 'refund'))
  where bt.payout_id = 118234
  group by bt.id, bt.type, bt.amount, bt.fee, bt.net,
           bt.source_order_id, bt.source_order_transaction_id
)
select
  m.id, m.type, o.name as order_name, m.amount, m.fee, m.net,
  case
    when m.source_order_transaction_id is null then 'no order'
    when lower(m.type) not in ('charge', 'refund') then 'not covered'
    when m.matches = 0 then 'unmatched'
    when m.matches > 1 then 'ambiguous'
    else 'tied'
  end as status
from matched m
left join orders o on o.id = coalesce(m.order_id, m.source_order_id)
order by m.id;

Against the example it returns rows 1 to 4 as tied and row 5 as no order. The join branches on charges and refunds only. Anything else with an order behind it, a dispute included, comes back as not covered and goes to a person, because Shopify’s pages say a chargeback is taken from the next available payout and say nothing about what its ledger row points at, and we do not write a branch we cannot check. Summing amount, fee and net for the payout gives 225.00, 5.94 and 219.06, and because every ledger line comes back exactly once, a total that misses the amount on the payout record means a row is missing from the ledger you pulled, not a row counted twice.

What the connectors actually land

Fivetran lands balance_transaction and adjustment_order_transaction tables, with an August 2026 beta backfilling the associated_payout_id column once Shopify finalises the payout, so check the beta’s status and treat that column as the payout key. Airbyte’s balance transactions stream needs the read_shopify_payments_payouts scope. Both land the tables. Neither does the join above, and no dbt package we could find does it either.

When does the clearing account stop being enough?

While you run one currency, one gateway and a low refund rate, the clearing account is enough and you should not spend money on anything else. It stops being enough at points the vendors document, and the symptom is always the same. A balance that no longer returns to zero.

Every threshold here is advisory or vendor, not audited, and the vendors disagree. Eightx, a fractional CFO firm, puts the point where brands leave QuickBooks Online “between $15M and $30M revenue, with early-warning evaluations starting at $10M”, as a planning range rather than survey data. Oracle’s NetSuite pages say “There’s no hard and fast rule based on revenue or employees”, and Protelo, a NetSuite reseller, agrees. The disagreement is more honest than either number. The breakpoints below are about shape, not size, and the moves are ours.

Where you are The clearing account The move
One currency, one gateway, low refunds Returns to zero after every payout Keep it. Spend nothing else.
A second gateway, PayPal, Amazon Pay, Klarna or Afterpay Carries fees the Shopify report never sees One clearing account per gateway, fed by its own settlement file
More than one currency Carries a residual that moves with the exchange rate Reconcile in payout currency, post the difference as FX
Wholesale mixed with DTC, or rising refunds Carries refunds landing weeks after their orders Reconcile at transaction grain, the section 5 join
A close past ten days Is what the close waits on An ERP payout module, holes and all, or the join built in-house

Above the app store tier, payout reconciliation exists at a price. Celigo’s Shopify to NetSuite integration sells it as a separate add-on that by its own documentation “reconciles individual transactions against existing orders and refunds, and generates Make Deposit records”, covers only Shopify Payments, and carries a documented duplicate deposit bug on backdated runs. Oracle’s NetSuite Connector posts payouts as deposits against the order, with the fees as lines. The capability exists, costs extra, and still has holes.

Below that tier the holes are the product. Intuit’s own QuickBooks Online connector on the Shopify app store sits at 2.1 stars, 69% of them one star, with one merchant reporting “about 1 out of every 15 payouts actually get sent to the app transactions part where you can match to deposits” and Intuit replying that it supports only Shopify Payments and Shop Pay.

Whichever tier you are on, the section 5 join is what ties a payout to its orders. Building it into your own stack, breaks routed to a person and the rest posted on their own, is our ERP Automation work.

FAQ

Why doesn’t my Shopify payout match my sales? It is the net of a ledger on its own clock, and Shopify’s help page says the report is not a statement of revenue.

What is a clearing account, and why does the balance never clear? A bank account that lives only in your books, where gateway fees and timing leave a balance.

What is actually inside a payout? Every movement on the Shopify Payments balance, from a type list of roughly a hundred values.

Why can’t I get a payout report by SKU? Money moves per transaction, not per line item, and Shopify staff confirm no built-in report closes the gap.

How do you tie a payout back to its orders? Payout id to ledger row, then to the order transaction and its order, accepting the parent id too.

When does the clearing account stop being enough? When a second gateway or currency, wholesale, or a rising refund rate leaves a balance that never returns to zero.


Written by Stavros Symeonidis, founding engineer at otum.

Found this useful?

Add otum.io as a preferred source, and Google is more likely to show you our posts.

Add on Google

Related

Engineering edge,
without hiring a team.

Numbers you can trust, operations that run themselves, all built into your stack and owned by you. Tell us where you want the edge, and we'll build it.

Have a project in mind?

  • Quick response

    You'll hear from an engineer the same working day, not a sales sequence.

  • Clear next steps

    A short call, then a plan, and we start with Proof Week or a full build.

  • Fixed fee first

    A fixed fee agreed before anything starts, for Proof Week or the full build.