Reconciler Refunds, payouts, and reconciliation
Processing fee not recorded on the order
A customer pays fifty dollars, and the order says fifty dollars. But Shopify Payments keeps a slice of that as a processing fee before the payout ever reaches your bank, and the order never mentions it. Every revenue report built from order totals is quietly counting money you never kept. Here is why the fee goes missing and a small script that pulls it from the transaction and writes it back onto the order.
The order total is what the customer paid, not what you kept. The processing fee lives on the order's own transactions, as a fees array on each OrderTransaction, and Shopify never copies it onto the order for you. Run a small Python or Node.js script that lists recent orders with their transactions, sums the fees on the successful sale and capture transactions in cents, and writes that total onto the order with an orderUpdate metafield, skipping any order that already has the fee recorded. Full code, tests, and a dry run guard are below.
The problem in plain words
When someone checks out with a card, Shopify Payments charges the customer the full order total and then takes its own cut before the rest lands in your bank account. That cut is the processing fee: a percentage plus a small flat amount, depending on your plan and the card used.
The order itself does not know any of this happened. Its total, its subtotal, and its totalReceivedSet all describe the customer's side of the transaction: what they were charged. The fee is recorded separately, attached to the payment transaction as a TransactionFee, and it only ever surfaces in the Shopify Payments payout report. If your reporting, your accounting sync, or your margin calculation reads the order total and stops there, it is reading gross revenue and calling it net. The gap is small per order and enormous across a year of orders.
Why it happens
Shopify's data model keeps the money the customer moved and the money the platform takes separate on purpose, since one is the merchant's revenue and the other is a cost of accepting the payment. A few things push this from a modeling detail into a real reporting problem:
- The order's
totalPriceSetandtotalReceivedSetdescribe what the customer was charged, never what the processor kept. - The processing fee lives on the
OrderTransaction.feesfield, and it is only populated for Shopify Payments transactions, so apps that only read order totals never see it. - The Shopify Payments payout report does have the fee, but it is grouped by payout, not by order, so tying a specific fee back to a specific order takes another lookup.
- Finance tools, accounting syncs, and margin dashboards are usually wired to the order object because that is the obvious source, and nobody notices the gap until the bank deposit is smaller than the reports say it should be.
This is a common source of confusion for anyone reconciling payouts by hand. The order says fifty dollars, the bank shows a little less, and the difference looks like an error until you remember the processor took its cut first. See the citations at the end for the exact docs.
We are not changing what the order charged the customer. We are recording, next to the order, what the processor actually took, so anyone reading the order later can compute net revenue without a second trip to the payout report. That means a read-only pass over transactions, one small write per order, in cents so nothing rounds wrong, and never touching a price or a total.
The fix, as a flow
We add a job that lists recent orders along with their transactions, sums the fee on every successful sale or capture transaction, and writes that number onto the order as a metafield in minor units. An order that already carries the metafield is left alone, so the job is safe to run again and again without double counting or overwriting a value someone corrected by hand.
Build it step by step
Get an Admin API access token
Create a custom app in your Shopify admin under Settings, Apps and sales channels, Develop apps. Give it the read_orders and write_orders scopes and install it to get an Admin API access token that starts with shpat_. Keep the token and the shop domain in environment variables, never in the file.
pip install requests
export SHOPIFY_SHOP="yourstore.myshopify.com"
export SHOPIFY_ACCESS_TOKEN="shpat_..."
export LOOKBACK_DAYS="14"
export FEE_METAFIELD_NAMESPACE="recon"
export FEE_METAFIELD_KEY="processing_fee_cents"
export DRY_RUN="true" # start safe, change to false to write
// Node 18+ has fetch built in, no dependencies needed
export SHOPIFY_SHOP="yourstore.myshopify.com"
export SHOPIFY_ACCESS_TOKEN="shpat_..."
export LOOKBACK_DAYS="14"
export FEE_METAFIELD_NAMESPACE="recon"
export FEE_METAFIELD_KEY="processing_fee_cents"
export DRY_RUN="true" // start safe, change to false to write
Talk to the Admin GraphQL API
Every call goes to one GraphQL endpoint with your token in the X-Shopify-Access-Token header. A small helper sends a query and returns the data, and raises if Shopify reports an error. We use this same helper to read orders and to run the mutation that writes the fee back.
import os, requests
SHOP = os.environ["SHOPIFY_SHOP"]
TOKEN = os.environ["SHOPIFY_ACCESS_TOKEN"]
API_VERSION = os.environ.get("SHOPIFY_API_VERSION", "2025-01")
ENDPOINT = f"https://{SHOP}/admin/api/{API_VERSION}/graphql.json"
def gql(query, variables=None):
r = requests.post(
ENDPOINT,
json={"query": query, "variables": variables or {}},
headers={"X-Shopify-Access-Token": TOKEN, "Content-Type": "application/json"},
timeout=30,
)
r.raise_for_status()
body = r.json()
if body.get("errors"):
raise RuntimeError(body["errors"])
return body["data"]
const SHOP = process.env.SHOPIFY_SHOP;
const TOKEN = process.env.SHOPIFY_ACCESS_TOKEN;
const API_VERSION = process.env.SHOPIFY_API_VERSION || "2025-01";
const ENDPOINT = `https://${SHOP}/admin/api/${API_VERSION}/graphql.json`;
async function gql(query, variables = {}) {
const res = await fetch(ENDPOINT, {
method: "POST",
headers: { "X-Shopify-Access-Token": TOKEN, "Content-Type": "application/json" },
body: JSON.stringify({ query, variables }),
});
if (!res.ok) throw new Error(`Shopify ${res.status}`);
const body = await res.json();
if (body.errors) throw new Error(JSON.stringify(body.errors));
return body.data;
}
List recent orders with their transactions and fees
Ask for orders created within a lookback window, and read back what the decision needs: the order id and name, the existing fee metafield if one is set, and each transaction's kind, status, and fees array with its own amount. We page through with a cursor so the job handles a large window without missing orders.
ORDERS_QUERY = """
query($cursor: String, $q: String!, $ns: String!, $key: String!) {
orders(first: 25, after: $cursor, query: $q) {
pageInfo { hasNextPage endCursor }
nodes {
id
name
feeMetafield: metafield(namespace: $ns, key: $key) { value }
transactions(first: 10) {
kind
status
fees { amount { amount currencyCode } }
}
}
}
}"""
def recent_orders(namespace, key):
q = f"created_at:>-{LOOKBACK_DAYS}d"
cursor = None
while True:
data = gql(ORDERS_QUERY, {"cursor": cursor, "q": q, "ns": namespace, "key": key})["orders"]
for node in data["nodes"]:
yield node
if not data["pageInfo"]["hasNextPage"]:
return
cursor = data["pageInfo"]["endCursor"]
const ORDERS_QUERY = `
query($cursor: String, $q: String!, $ns: String!, $key: String!) {
orders(first: 25, after: $cursor, query: $q) {
pageInfo { hasNextPage endCursor }
nodes {
id
name
feeMetafield: metafield(namespace: $ns, key: $key) { value }
transactions(first: 10) {
kind
status
fees { amount { amount currencyCode } }
}
}
}
}`;
async function* recentOrders(namespace, key) {
const q = `created_at:>-${LOOKBACK_DAYS}d`;
let cursor = null;
while (true) {
const data = (await gql(ORDERS_QUERY, { cursor, q, ns: namespace, key })).orders;
for (const node of data.nodes) yield node;
if (!data.pageInfo.hasNextPage) return;
cursor = data.pageInfo.endCursor;
}
}
Decide, with one pure function
Keep the decision in its own function that takes an order and returns the fee in cents to write, or null if there is nothing to do. A pure function like this is easy to read and easy to test, which we do later. It sums the fees on successful sale and capture transactions only, skips an order that already carries the metafield, and skips an order with no fee at all, since not every transaction is a Shopify Payments transaction.
CHARGE_KINDS = {"SALE", "CAPTURE"}
def to_cents(amount):
return round(float(amount) * 100)
def fee_cents_for_order(order):
"""Sum the processing fee on successful sale and capture transactions.
Returns the fee in cents, or None if there is nothing new to record.
"""
if (order.get("feeMetafield") or {}).get("value") is not None:
return None # already recorded, do not overwrite
total = 0
for t in order.get("transactions") or []:
if t.get("status") != "SUCCESS" or t.get("kind") not in CHARGE_KINDS:
continue
for fee in t.get("fees") or []:
total += to_cents(fee["amount"]["amount"])
return total if total > 0 else None
const CHARGE_KINDS = new Set(["SALE", "CAPTURE"]);
export function toCents(amount) {
return Math.round(parseFloat(amount) * 100);
}
export function feeCentsForOrder(order) {
// Sum the processing fee on successful sale and capture transactions.
// Returns the fee in cents, or null if there is nothing new to record.
if ((order.feeMetafield || {}).value != null) return null; // already recorded
let total = 0;
for (const t of order.transactions || []) {
if (t.status !== "SUCCESS" || !CHARGE_KINDS.has(t.kind)) continue;
for (const fee of t.fees || []) total += toCents(fee.amount.amount);
}
return total > 0 ? total : null;
}
Write the fee back with orderUpdate
When an order has a fee to record, call the orderUpdate mutation with a single metafield holding the fee in cents as a number_integer. This never touches the order's price, total, or payment, it only adds a value next to the order that your reports can subtract from the total to get what you actually kept. Always read back userErrors. If Shopify refuses, the error tells you why, and the script should stop on it rather than pretend it worked.
SET_FEE_MUTATION = """
mutation($id: ID!, $ns: String!, $key: String!, $value: String!) {
orderUpdate(input: {
id: $id
metafields: [{ namespace: $ns, key: $key, type: "number_integer", value: $value }]
}) {
order { id }
userErrors { field message }
}
}"""
def write_fee(order_id, namespace, key, fee_cents):
result = gql(SET_FEE_MUTATION, {
"id": order_id, "ns": namespace, "key": key, "value": str(fee_cents),
})["orderUpdate"]
if result["userErrors"]:
raise RuntimeError(result["userErrors"])
const SET_FEE_MUTATION = `
mutation($id: ID!, $ns: String!, $key: String!, $value: String!) {
orderUpdate(input: {
id: $id
metafields: [{ namespace: $ns, key: $key, type: "number_integer", value: $value }]
}) {
order { id }
userErrors { field message }
}
}`;
async function writeFee(orderId, namespace, key, feeCents) {
const result = (await gql(SET_FEE_MUTATION, {
id: orderId, ns: namespace, key, value: String(feeCents),
})).orderUpdate;
if (result.userErrors.length) throw new Error(JSON.stringify(result.userErrors));
}
Wire it together with a dry run guard
The loop ties every piece together. Notice the dry run guard. On the first few runs, leave DRY_RUN on so the script only reports which orders it would update and for how much. Read the output, agree with it, then switch it off to let it write. Run it on a schedule that matches your reporting cycle, for example once a day.
Always start with DRY_RUN=true, and let the metafield check protect you from writing the same fee twice. This script only reads transactions and writes one metafield, it never edits a price, a total, or a payment, so there is nothing here that can change what a customer was charged.
The full code
Here is the complete script in one file for each language. It reads settings from the environment, logs what it does, respects the dry run flag, and is safe to run again and again because it only writes the fee once per order and works entirely in cents so nothing rounds wrong.
View this code on GitHub Full runnable folder with tests in the shopify-fixes repo.
"""Record the Shopify Payments processing fee onto its order, safely.
The order total is what the customer paid, not what you kept. The fee lives on
the order's own successful transactions as a TransactionFee, never on the order.
This sums the fee on each recent order's successful sale and capture transactions
and writes it back as a metafield in cents, once, so reports can compute net
revenue without a second trip to the payout report. Read only apart from the
metafield write. Run on a schedule. Safe to run again and again.
"""
import os
import logging
import requests
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
log = logging.getLogger("record_processing_fee")
SHOP = os.environ["SHOPIFY_SHOP"]
TOKEN = os.environ["SHOPIFY_ACCESS_TOKEN"]
API_VERSION = os.environ.get("SHOPIFY_API_VERSION", "2025-01")
ENDPOINT = f"https://{SHOP}/admin/api/{API_VERSION}/graphql.json"
LOOKBACK_DAYS = int(os.environ.get("LOOKBACK_DAYS", "14"))
FEE_NAMESPACE = os.environ.get("FEE_METAFIELD_NAMESPACE", "recon")
FEE_KEY = os.environ.get("FEE_METAFIELD_KEY", "processing_fee_cents")
DRY_RUN = os.environ.get("DRY_RUN", "true").lower() == "true"
CHARGE_KINDS = {"SALE", "CAPTURE"}
ORDERS_QUERY = """
query($cursor: String, $q: String!, $ns: String!, $key: String!) {
orders(first: 25, after: $cursor, query: $q) {
pageInfo { hasNextPage endCursor }
nodes {
id
name
feeMetafield: metafield(namespace: $ns, key: $key) { value }
transactions(first: 10) {
kind
status
fees { amount { amount currencyCode } }
}
}
}
}"""
SET_FEE_MUTATION = """
mutation($id: ID!, $ns: String!, $key: String!, $value: String!) {
orderUpdate(input: {
id: $id
metafields: [{ namespace: $ns, key: $key, type: "number_integer", value: $value }]
}) {
order { id }
userErrors { field message }
}
}"""
def gql(query, variables=None):
r = requests.post(
ENDPOINT,
json={"query": query, "variables": variables or {}},
headers={"X-Shopify-Access-Token": TOKEN, "Content-Type": "application/json"},
timeout=30,
)
r.raise_for_status()
body = r.json()
if body.get("errors"):
raise RuntimeError(body["errors"])
return body["data"]
def to_cents(amount):
return round(float(amount) * 100)
def fee_cents_for_order(order):
"""Sum the processing fee on successful sale and capture transactions.
Returns the fee in cents, or None if there is nothing new to record.
"""
if (order.get("feeMetafield") or {}).get("value") is not None:
return None # already recorded, do not overwrite
total = 0
for t in order.get("transactions") or []:
if t.get("status") != "SUCCESS" or t.get("kind") not in CHARGE_KINDS:
continue
for fee in t.get("fees") or []:
total += to_cents(fee["amount"]["amount"])
return total if total > 0 else None
def recent_orders():
q = f"created_at:>-{LOOKBACK_DAYS}d"
cursor = None
while True:
data = gql(ORDERS_QUERY, {"cursor": cursor, "q": q, "ns": FEE_NAMESPACE, "key": FEE_KEY})["orders"]
for node in data["nodes"]:
yield node
if not data["pageInfo"]["hasNextPage"]:
return
cursor = data["pageInfo"]["endCursor"]
def write_fee(order_id, fee_cents):
result = gql(SET_FEE_MUTATION, {
"id": order_id, "ns": FEE_NAMESPACE, "key": FEE_KEY, "value": str(fee_cents),
})["orderUpdate"]
if result["userErrors"]:
raise RuntimeError(result["userErrors"])
def run():
recorded = 0
for order in recent_orders():
fee_cents = fee_cents_for_order(order)
if fee_cents is None:
continue
log.info("Order %s fee %d cents. %s", order["name"], fee_cents,
"would record" if DRY_RUN else "recording")
if not DRY_RUN:
write_fee(order["id"], fee_cents)
recorded += 1
log.info("Done. %d order(s) %s.", recorded, "to record" if DRY_RUN else "recorded")
if __name__ == "__main__":
run()
/**
* Record the Shopify Payments processing fee onto its order, safely.
*
* The order total is what the customer paid, not what you kept. The fee lives on
* the order's own successful transactions as a TransactionFee, never on the order.
* This sums the fee on each recent order's successful sale and capture transactions
* and writes it back as a metafield in cents, once, so reports can compute net
* revenue without a second trip to the payout report. Run on a schedule.
*
* Guide: https://www.allanninal.dev/shopify/processing-fee-not-recorded-on-the-order/
*/
import { pathToFileURL } from "node:url";
const SHOP = process.env.SHOPIFY_SHOP || "example.myshopify.com";
const TOKEN = process.env.SHOPIFY_ACCESS_TOKEN || "shpat_dummy";
const API_VERSION = process.env.SHOPIFY_API_VERSION || "2025-01";
const ENDPOINT = `https://${SHOP}/admin/api/${API_VERSION}/graphql.json`;
const LOOKBACK_DAYS = Number(process.env.LOOKBACK_DAYS || 14);
const FEE_NAMESPACE = process.env.FEE_METAFIELD_NAMESPACE || "recon";
const FEE_KEY = process.env.FEE_METAFIELD_KEY || "processing_fee_cents";
const DRY_RUN = (process.env.DRY_RUN || "true").toLowerCase() === "true";
const CHARGE_KINDS = new Set(["SALE", "CAPTURE"]);
export function toCents(amount) {
return Math.round(parseFloat(amount) * 100);
}
export function feeCentsForOrder(order) {
// Sum the processing fee on successful sale and capture transactions.
// Returns the fee in cents, or null if there is nothing new to record.
if ((order.feeMetafield || {}).value != null) return null; // already recorded
let total = 0;
for (const t of order.transactions || []) {
if (t.status !== "SUCCESS" || !CHARGE_KINDS.has(t.kind)) continue;
for (const fee of t.fees || []) total += toCents(fee.amount.amount);
}
return total > 0 ? total : null;
}
async function gql(query, variables = {}) {
const res = await fetch(ENDPOINT, {
method: "POST",
headers: { "X-Shopify-Access-Token": TOKEN, "Content-Type": "application/json" },
body: JSON.stringify({ query, variables }),
});
if (!res.ok) throw new Error(`Shopify ${res.status}`);
const body = await res.json();
if (body.errors) throw new Error(JSON.stringify(body.errors));
return body.data;
}
const ORDERS_QUERY = `
query($cursor: String, $q: String!, $ns: String!, $key: String!) {
orders(first: 25, after: $cursor, query: $q) {
pageInfo { hasNextPage endCursor }
nodes {
id
name
feeMetafield: metafield(namespace: $ns, key: $key) { value }
transactions(first: 10) {
kind
status
fees { amount { amount currencyCode } }
}
}
}
}`;
const SET_FEE_MUTATION = `
mutation($id: ID!, $ns: String!, $key: String!, $value: String!) {
orderUpdate(input: {
id: $id
metafields: [{ namespace: $ns, key: $key, type: "number_integer", value: $value }]
}) {
order { id }
userErrors { field message }
}
}`;
async function* recentOrders() {
const q = `created_at:>-${LOOKBACK_DAYS}d`;
let cursor = null;
while (true) {
const data = (await gql(ORDERS_QUERY, { cursor, q, ns: FEE_NAMESPACE, key: FEE_KEY })).orders;
for (const node of data.nodes) yield node;
if (!data.pageInfo.hasNextPage) return;
cursor = data.pageInfo.endCursor;
}
}
async function writeFee(orderId, feeCents) {
const result = (await gql(SET_FEE_MUTATION, {
id: orderId, ns: FEE_NAMESPACE, key: FEE_KEY, value: String(feeCents),
})).orderUpdate;
if (result.userErrors.length) throw new Error(JSON.stringify(result.userErrors));
}
export async function run() {
let recorded = 0;
for await (const order of recentOrders()) {
const feeCents = feeCentsForOrder(order);
if (feeCents === null) continue;
console.log(`Order ${order.name} fee ${feeCents} cents. ${DRY_RUN ? "dry run" : "recording"}`);
if (!DRY_RUN) await writeFee(order.id, feeCents);
recorded++;
}
console.log(`Done. ${recorded} order(s) ${DRY_RUN ? "to record" : "recorded"}.`);
}
if (import.meta.url === pathToFileURL(process.argv[1]).href) {
run().catch((err) => { console.error(err); process.exit(1); });
}
Add a test
The decision rule is the part most worth testing, because it decides how much money the script says you kept. Because we kept fee_cents_for_order pure, the test needs no network and no Shopify account. It just feeds in plain objects and checks the answer.
from record_processing_fee import fee_cents_for_order, to_cents
def fee(amount):
return {"amount": {"amount": amount, "currencyCode": "USD"}}
def txn(fees, kind="SALE", status="SUCCESS"):
return {"kind": kind, "status": status, "fees": fees}
def order(txns, existing_value=None):
return {
"feeMetafield": {"value": existing_value} if existing_value is not None else None,
"transactions": txns,
}
def test_to_cents_rounds():
assert to_cents("1.49") == 149
def test_sums_fee_on_successful_sale():
assert fee_cents_for_order(order([txn([fee("1.50")])])) == 150
def test_sums_fees_across_capture_and_sale():
o = order([txn([fee("1.00")], kind="SALE"), txn([fee("0.50")], kind="CAPTURE")])
assert fee_cents_for_order(o) == 150
def test_ignores_failed_transactions():
o = order([txn([fee("1.50")], status="FAILURE")])
assert fee_cents_for_order(o) is None
def test_ignores_refund_transactions():
o = order([txn([fee("1.50")], kind="REFUND")])
assert fee_cents_for_order(o) is None
def test_skips_when_already_recorded():
o = order([txn([fee("1.50")])], existing_value="150")
assert fee_cents_for_order(o) is None
def test_none_when_no_fee_present():
o = order([txn([])])
assert fee_cents_for_order(o) is None
import { test } from "node:test";
import assert from "node:assert/strict";
import { feeCentsForOrder, toCents } from "./record-processing-fee.js";
const fee = (amount) => ({ amount: { amount, currencyCode: "USD" } });
const txn = (fees, { kind = "SALE", status = "SUCCESS" } = {}) => ({ kind, status, fees });
const order = (transactions, existingValue = null) => ({
feeMetafield: existingValue !== null ? { value: existingValue } : null,
transactions,
});
test("toCents rounds", () => {
assert.equal(toCents("1.49"), 149);
});
test("sums fee on successful sale", () => {
assert.equal(feeCentsForOrder(order([txn([fee("1.50")])])), 150);
});
test("sums fees across capture and sale", () => {
const o = order([txn([fee("1.00")], { kind: "SALE" }), txn([fee("0.50")], { kind: "CAPTURE" })]);
assert.equal(feeCentsForOrder(o), 150);
});
test("ignores failed transactions", () => {
const o = order([txn([fee("1.50")], { status: "FAILURE" })]);
assert.equal(feeCentsForOrder(o), null);
});
test("ignores refund transactions", () => {
const o = order([txn([fee("1.50")], { kind: "REFUND" })]);
assert.equal(feeCentsForOrder(o), null);
});
test("skips when already recorded", () => {
const o = order([txn([fee("1.50")])], "150");
assert.equal(feeCentsForOrder(o), null);
});
test("null when no fee present", () => {
const o = order([txn([])]);
assert.equal(feeCentsForOrder(o), null);
});
Case studies
The dashboard that always looked too healthy
A skincare brand built a daily margin dashboard straight from order totals minus cost of goods. It looked fine until finance reconciled a month of payouts against the bank and found several thousand dollars unaccounted for. The dashboard had never once subtracted a processing fee, because nothing in the order said one existed.
They ran the script in dry run, saw the fee it would record on every order for the past month, and let it write the metafield for real. The dashboard now subtracts processing_fee_cents from the order total, and the numbers finally match the bank.
The bookkeeper who kept a spreadsheet of fees by hand
A small studio synced orders to its accounting software every night, but the processing fee had to be entered as a manual journal line because nothing carried it automatically. The bookkeeper kept a spreadsheet, checked the Shopify Payments payout report line by line, and typed in each fee once a week.
Now the daily job records the fee on the order the same day it happens. The accounting sync reads the metafield along with the order and posts the fee as its own line automatically, and the spreadsheet is gone.
After this runs on a schedule, every order that had a real processing fee carries it as a metafield the same day, in cents, written exactly once. Reports and accounting syncs can subtract the fee from the order total and get a true net figure instead of a guess, and nobody has to reconcile the payout report by hand again.
FAQ
Why does my Shopify order not show the processing fee?
The order total is the amount the customer paid, not the amount you kept. Shopify Payments deducts its processing fee from the payout separately, and that fee lives on the transaction as a TransactionFee, not on the order itself, so nothing writes it back to the order for you.
Is it safe to write the processing fee back onto the order with a script?
Yes, when the script only reads the fee from the order's own successful transactions, writes a single metafield that holds the fee in cents, skips any order that already has that metafield set, and runs in dry run first. It never changes a price, a total, or a payment.
Where does Shopify actually store the processing fee for an order?
Each OrderTransaction on the order can carry a fees array of TransactionFee objects, present for Shopify Payments transactions. Each fee has its own amount. Summing the fees on the successful sale and capture transactions gives you the total processing fee for that order.
Related field notes
Citations
On the problem:
- Shopify Help Center: Shopify Payments fees and how they are charged. help.shopify.com/en/manual/payments/shopify-payments/payouts/fees-on-payouts
- Shopify Help Center: understanding your payout and transaction fees. help.shopify.com/en/manual/payments/shopify-payments/payouts
- Shopify Community: reconciling order totals against Shopify Payments payouts. community.shopify.com payments shipping fulfillment
On the solution:
- Shopify Admin GraphQL: the
TransactionFeeobject and theOrderTransaction.feesfield. shopify.dev/docs/api/admin-graphql/latest/objects/TransactionFee - Shopify Admin GraphQL: the
orderUpdatemutation and its metafields input. shopify.dev/docs/api/admin-graphql/latest/mutations/orderUpdate - Shopify Admin GraphQL: the
ordersquery and its search syntax. shopify.dev/docs/api/admin-graphql/latest/queries/orders
Stuck on a tricky one?
If you have a problem in Shopify orders, payments, subscriptions, inventory, or fulfillment that you would rather hand off, this is the kind of work I do. Message me and we can work through it together.
Did this fix your reconciliation?
If this saved you a spreadsheet of fees or a wrong margin report, you can buy me a coffee. It is the best way to keep these field notes free and growing.
Buy me a coffee on Ko-fi