netProceeds Is Not Profit #
You run the Data Kiosk economics query, get a clean response back, and notice two things at once. Every cost field is empty:
"cost": {
"costOfGoodsSold": null,
"miscellaneousCost": null,
"fbaCost": null,
"mfnCost": null
}
And netProceeds has a number in it anyway.
Both cannot be true of a profit figure. netProceeds is net sales minus Amazon's fees minus your ad spend. Your cost of goods is not in it, and no part of the response tells you that.
In one line
netProceeds.total = sales.netProductSales − Σ fees − Σ ads. Cost of goods is never subtracted.
This matters because the field is not null. It is a number with a currency code attached, sitting in a block called net proceeds. Point a dashboard at it and every margin you display is overstated by exactly what you paid your supplier. Silently, and consistently, on every row.
Verified against a live seller account in August 2026 on the Seller Economics schema v2024-03-15. The identity held on every row returned, to a residual of 0.0000.
What One Economics Query Actually Returns #
Before the catch, the part that is genuinely good. A single economics query returns most of a per-ASIN P&L in one response, aggregated the way you asked for it.
| In the response | Where you would otherwise get it |
|---|---|
| Units, revenue, refunds, average selling price | Orders API or Sales API |
| Every fee, itemised, with per-unit amounts | Finances API, at transaction level, aggregated by you |
| Sponsored Products spend | Advertising API: different portal, different application, different approval |
| netProceeds, per unit and total | computed by you, then reconciled |
You choose the grain in the query itself: DAY, WEEK, MONTH or RANGE, against PARENT_ASIN, CHILD_ASIN or MSKU. That is the difference between Data Kiosk and the Reports API: with a report, Amazon picked the columns and the buckets; here you do.
Key mental model: Data Kiosk borrows Amazon Marketing Cloud's workflow: you author a query, submit it, poll, then download the result. The freedom is narrower though. AMC gives you SQL over tables; Data Kiosk gives you field selection over a fixed schema, so you shape the report but cannot widen its scope. The asynchronous shape is identical to reports, so the create-poll-download pattern applies unchanged.
Why costOfGoodsSold Comes Back Null #
It is not a bug, a permission gap, or a missing role. Amazon does not know what you paid your supplier. There is no transaction on your seller account that records it, because the purchase happened somewhere else entirely.
The schema says so plainly. The PerUnitCost type carries this description: the item level per unit cost provided by seller. Fields in this type will be null if not provided by the seller.
Cost of goods reaches Amazon only if someone types it into Seller Central, per SKU, under Manage Inventory. Most accounts have never had it entered. On an account that has not, all four cost fields return null on every row, in every marketplace, for every date range, and nothing in the response marks the resulting netProceeds as incomplete.
Gotcha: a null cost field is loud, so you notice it and handle it. A populated netProceeds built from those nulls is silent. It is the second one that reaches a dashboard and stays there.
How to Verify the Formula on Your Own Account #
Do not take the formula on trust. It is three lines of arithmetic against a response you already have.
Request sales, fees, ads and netProceeds together, aggregated monthly by child ASIN:
query Economics {
analytics_economics_2024_03_15 {
economics(
startDate: "2026-05-01"
endDate: "2026-07-31"
aggregateBy: { date: MONTH, productId: CHILD_ASIN }
marketplaceIds: ["A1F83G8C2ARO7P"]
) {
childAsin
sales { netProductSales { amount } netUnitsSold }
fees { feeTypeName charges { aggregatedDetail { totalAmount { amount } } } }
ads { adTypeName charge { totalAmount { amount } } }
cost { costOfGoodsSold { amount } }
netProceeds { total { amount } }
}
}
}
Then check the identity row by row:
def amount(node):
return float((node or {}).get("amount") or 0.0)
for row in rows:
net_sales = amount(row["sales"]["netProductSales"])
fee_total = sum(amount(c["aggregatedDetail"]["totalAmount"])
for f in row.get("fees") or []
for c in f.get("charges") or [])
ad_total = sum(amount(a["charge"]["totalAmount"])
for a in row.get("ads") or [])
reported = amount(row["netProceeds"]["total"])
derived = net_sales - fee_total - ad_total
assert abs(reported - derived) < 0.01, row["childAsin"]
It holds exactly. Not approximately, not within rounding. The residual is zero on every row.
The sign convention that breaks the first attempt
Fees and ad charges come back positive. They are charges, not signed ledger adjustments, so you subtract them yourself. Only reimbursements and refunded fees, such as FBAInventoryReimbursement and RefundedReferralFee, carry a negative sign.
Write net_sales + fee_total + ad_total on the assumption that Amazon has already signed them and the identity fails on most rows while still matching on the handful where charges happen to net out. That partial match is worse than a clean failure, because it looks like a rounding problem rather than a sign error.
Where the Sponsored Products Spend Comes From #
The surprising part of the response is that ad spend is in it at all. No Ads API application, no profile_id, no second OAuth flow. Three SP-API credentials and a Data Kiosk query return Sponsored Products spend per ASIN per month.
That crosses a real boundary. Advertising and Selling Partner are separate developer programmes with their own application and approval process. So it is worth knowing why the wall has a hole in it.
It is not the advertising system. It is the billing ledger. In the schema, ads[].charge is typed AggregatedDetail, the identical type used by ReferralFee and FbaFulfilmentFee. Request the full type on both and the same sub-fields populate:
| Sub-field | ads[].charge | ReferralFee |
|---|---|---|
amount | populated | populated |
amountPerUnit | populated | populated |
promotionAmount | populated | populated |
taxAmount | populated | populated |
totalAmount | populated | populated |
quantity | populated | populated |
amountPerUnitDelta | null | null |
Same shape, same one null. Amazon's own wording follows: the help page linked from the schema is titled supported fee types and advertisement charge types. To the ledger, your ad spend is a charge like any other.
What quantity means on an ad charge
quantity is populated on ad rows, and it is not your unit count. On rows checked in August 2026 it ran into the thousands where units sold were in the hundreds, and on one row it was lower than units sold, which rules out any units-based reading. Divide totalAmount by quantity and the result lands just under £1 on every row, holding within about 15% across the sample.
That is a cost per click. Sponsored Products bills per click, so quantity is billable clicks and amountPerUnit is your effective CPC, available for free, without touching the Ads API.
The limit worth knowing: you get spend, clicks and CPC, because those are facts on a bill. You get no impressions, no ACOS, no keywords, no campaigns and no ad groups, because none of those are charges. For performance data the Ads API is still the only route.
Building a Dashboard That Does Not Drift #
The lesson generalises past this one field. A populated number from a first-party API is not the same as the number you want, and the gap is rarely documented.
- Never surface a vendor's derived field as your own metric
Store
netProceedsif you like, but label it net proceeds. The moment it appears in a column headed Profit or Margin, you have made a claim Amazon never made. - Recompute from components you control
You have net sales, itemised fees and ad spend in the same row. Compute margin yourself from those plus your own cost of goods, held in your own system where you can correct it.
- Assert the identity in your pipeline, not once by hand
The check above is cheap enough to run on every ingest. If Amazon ever changes what goes into netProceeds, a failing assertion tells you the week it happens rather than the quarter someone notices.
- Treat null and zero as different
A null cost field means unknown. Coercing it to zero at ingest converts a known unknown into a confident wrong answer, which is the worse of the two.
This is the same class of problem as a dashboard whose totals never quite agree with Seller Central: not a bug, but two systems computing subtly different things. More on that in why your custom dashboard does not match Seller Central.
Building seller reporting and want the numbers to survive an audit rather than merely render? Tell me what you are pulling and I will tell you where it drifts.
FAQ #
Does Amazon Data Kiosk netProceeds include cost of goods?
No. netProceeds is net product sales minus Amazon fees minus advertising charges. Cost of goods is excluded, and on accounts that have never entered COGS in Seller Central the entire cost block returns null while netProceeds still returns a value.
Why is costOfGoodsSold null in the Data Kiosk economics dataset?
Because it is seller-supplied. The PerUnitCost type in the Seller Economics v2024-03-15 schema states that its fields are null if not provided by the seller. Amazon has no record of what you paid your supplier unless it is entered per SKU in Seller Central.
Can I get Sponsored Products spend without the Advertising API?
Yes. The Data Kiosk economics dataset returns Sponsored Products charges per ASIN using SP-API credentials alone, because the figures come from your seller billing ledger rather than the advertising system. You get spend, billable clicks and an effective cost per click, but no impressions, ACOS, keywords or campaign structure.
Why do Data Kiosk fees come back as positive numbers?
They are charges rather than signed ledger adjustments, so you subtract them when computing net proceeds. Reimbursements and refunded fees such as FBAInventoryReimbursement and RefundedReferralFee are the exceptions and carry negative values.
How long does a Data Kiosk query take to run?
It is asynchronous, like the Reports API. Queries in August 2026 moved from IN_QUEUE to IN_PROGRESS to DONE in roughly 33 seconds for a three-month, ASIN-level economics request. Results carry a 30-day retention window, so warehouse anything you intend to keep.
Related guides
Amazon SP-API
The SP-API Reports Pattern Everyone Gets Wrong (Async, Step by Step)
The SP-API Reports API is asynchronous — request, poll, download, decompress — and most broken integrations either treat it like a synchronous call or poll it straight into a throttle. Here's the correct four-step flow, the status states that trip people up, and the gzip gotcha at the end.
Read guide →Amazon SP-API
Amazon Vendor Central API: What 1P Vendors Can Actually Automate
Vendor Central (1P) has a real API surface through the SP-API vendor family — retail analytics, purchase orders, shipments, invoices, and direct fulfillment. Here's what's genuinely automatable, what still isn't, and where the fastest wins are for a 1P vendor.
Read guide →Amazon SP-API
Amazon SP-API 403 Unauthorized: Every Cause and How to Fix It
A 403 "Access to requested resource is denied" from the SP-API almost never means your credentials are wrong — it usually means a role, a token, a region, or an authorization is mismatched. Here are the eight causes that produce a 403, in the order worth checking them, with the exact fix for each.
Read guide →Don't want to build this yourself?
I set this up for sellers and agencies every week. Book a free 30-minute audit of your Amazon data & PPC setup — you'll leave with a plan either way, whether we work together or not.