Replicating Dashboard "Sales Summary" + "Category Sales" from the Orders API

Replicating Dashboard “Sales Summary” + “Category Sales” from the Orders API

Category: Orders API / Reporting

I pull orders via SearchOrders, store them in my own DB, and compute daily per-location reports myself. I want to match the Dashboard “Sales Summary” and “Category Sales” using only Orders API (+ Payments API where needed).

I don’t want the Reporting API — I already store the raw orders and want the exact field-level formulas.

Our numbers today are close but not exact. Pseudocode below shows what we currently do — please point at what’s wrong.


What we fetch + filter today

POST /v2/orders/search, looped per (location_id, business_date), paged by cursor:

{ "location_ids": ["<LOC>"],
  "query": { "filter": { "date_time_filter": {
    "created_at": { "start_at": "<startUtc>", "end_at": "<endUtc>" } } } } }

Filter after fetch: keep order if state ∈ {OPEN, COMPLETED} AND net_amount_due_money == 0 (our “closed check”).

Fields we read:

  • Order: state, created_at, source.name, net_amount_due_money, total_money, total_tax_money, total_tip_money, total_discount_money, total_service_charge_money, return_amounts.{total,tax,tip,discount}_money, tenders[].{id,type,amount_money,other_details.source}, refunds[].{tender_id,amount_money}

  • Line item: {name, variation_name, quantity, gross_sales_money, catalog_object_id} + catalog drill-down variation → item → reporting_category → name

money() below = amount in cents; absent → 0.


Questions (with current pseudocode)

1. Reporting day = 3 AM–3 AM Pacific

startUtc = toUtc(date     @ 03:00 PT);  endUtc = toUtc(date+1 @ 03:00 PT)
keep created_at in [startUtc, endUtc)
  • Does the Dashboard bucket on created_at, closed_at, or something else?

  • Is the day-cutoff (reporting hours) seller-configurable and readable via API?

2. Closed checks

isClosedCheck = state ∈ {OPEN, COMPLETED} AND money(net_amount_due_money) == 0
  • Is net_amount_due_money == 0 the right “closed check” rule? Is the state guard needed?

  • Does Sales Summary include only closed checks, or open amounts too?

3. Category Sales (items sold + gross)

cat = reportingCategory(line_item.catalog_object_id)
group by cat: sum(int(quantity)), sum(money(gross_sales_money))
  • Group by reporting_category or legacy category_id?

  • Is gross_sales_money before discounts and tax?

  • How do returned line items affect category qty/gross? How is “Uncategorized” handled?

4. Gross Sales + order count

orderGross  = (money(total_money) - money(total_tax_money) - money(total_tip_money)) + money(total_discount_money)
orderNet    = (money(total_money) - money(total_tax_money) - money(total_tip_money))
            - (money(return_amounts.total) - money(return_amounts.tax) - money(return_amounts.tip))
orderCount += (orderNet >= 0) ? +1 : -1
  • Should Gross Sales instead be Σ line_items.gross_sales_money (before discounts/returns/tax/tip)?

  • How are fully-returned / exchanged orders counted?

5. Item sales + service charges

serviceCharges += money(total_service_charge_money)   // tracked only, not in gross/net
  • Is “Item Sales” = sum of line-item gross (pre-discount), separate from “Gross Sales”?

  • Are service charges in Gross Sales or separate? Is tax-on-service-charge included?

6. Returns, Discounts & Comps

returns   += (money(return_amounts.total) - money(return_amounts.tax) - money(return_amounts.tip)) + money(return_amounts.discount)
discounts += money(total_discount_money) - money(return_amounts.discount)
  • Is return_amounts.total_money the right “Returns” basis (tax-inclusive)?

  • Are comps separate from discounts? If so, which field identifies a comp?

7. Net Sales

netSales += orderNet      // see §4
  • Is Net Sales = Gross − Discounts&Comps − Returns, excluding tax/tip?

8. Deferred sales — we don’t compute this yet

  • Is “Deferred Sales” = gift cards sold? Identify via line-item item_type, or the Gift Cards API?

  • Is it excluded from Gross/Net and added into Total Sales?

9. Taxes

taxes += money(total_tax_money) - money(return_amounts.tax)
  • Correct? Include tax on service charges?

10. Tips

tips += money(total_tip_money) - money(return_amounts.tip)
  • Correct?

11. Refunds by amount — we use it only to net payments, not as its own line

for refund in refunds: payments[ tenderMap[refund.tender_id] ] -= money(refund.amount_money)
  • Is “Refunds by amount” = Σ refunds[].amount_money?

  • Confirm it’s distinct from Returns (e.g. exchanges: returned-item value ≠ net money refunded), so it must not be derived from return_amounts?

12. Total Sales — formula to validate

total_sales = net_sales + deferred_sales + taxes + tips - refunds_by_amount
  • Is this the Dashboard’s definition? If not, what is it?

13. Total payments collected — not emitted yet

  • Is it Σ tenders[].amount_money - Σ refunds[].amount_money?

  • Or from Payments API (Payment.total_money), joined via tender.payment_id / Payment.order_id?

14. Payment methods (card / cash / gift card redeemed / other)

for tender in tenders:
    method = (type == OTHER && other_details.source) ? source : type
    payments[method] += money(tender.amount_money)
// CASH→Cash, CARD→Credit, SQUARE_GIFT_CARD→GiftCard, DOORDASH/UBEREATS/…→channel, else Other
// refunds subtracted per method via refund.tender_id (see §11)
  • Split driven by tenders[].type (+ other_details.source for OTHER) — correct?

  • Is “gift card redeemed” just type == SQUARE_GIFT_CARD (vs gift-card sales in §8)?

15. Fees (processing / cost-plus) — not captured yet

  • Orders carry no fees, right? Use Payments API Payment.processing_fee[] (amount_money, type ∈ {INITIAL, ADJUSTMENT})?

  • Does type = ADJUSTMENT = cost-plus/interchange adjustment, and can it post on a later date (so I should re-pull a trailing window)?

  • Any API for the fee schedule itself? Are fees credited back on refunds?


TL;DR: I want the exact field-level formula for each Sales Summary line + Category Sales column, the right timestamp/day-cutoff, the closed-check rule, and confirmation that deferred sales and fees need the Gift Cards / Payments APIs.

Thanks for the thorough write-up!

Before I dive into field-level corrections, I want to gently push back on one thing: have you scoped this effort with the Reporting API? I know you mentioned not wanting it, but it was designed explicitly for this use case. We have prebuilt Views that automatically compute the same Sales Summary and Category Sales figures the Dashboard shows so you don’t have to sort out manual formulas, edge cases around comps vs discounts, no guessing at day cutoffs, etc.

The Reporting API is both faster to implement and self-maintaining as it inherits Dashboard formula changes automatically. Could you share a bit more about your hesitation around using it?

Hi Ashley, thanks for the nudge — that’s fair, and I’m genuinely open to switching if it checks out. Let me share where my hesitation comes from, framed as the conditions I’d need to satisfy:

1. Custom business-day cutoff (3 AM–3 AM PT). My original blocker was that the reporting timeframe defaults to a calendar day (12:00 AM–11:59 PM). Our business day runs 3:00 AM–3:00 AM Pacific (sales between midnight and 3 AM belong to the prior day). Can the Reporting API bucket on a custom day-cutoff like that, or apply an offset — and does it match whatever the Dashboard “reporting hours” setting does? If I can’t reproduce the 3 AM boundary exactly, the totals won’t line up with what we report internally.

2. Stability. Is the Reporting API still in beta/alpha? A big reason I built on the Orders API is longevity — I don’t want a report that suddenly breaks or changes shape on us. What’s the current support status?

3. Granularity — per-order detail with exact timestamps. This is probably the deciding factor. We don’t just need daily roll-ups; we need hourly-bucketed sales and product mix, which requires order-level (and ideally line-item-level) rows carrying the exact order datetime. The Views look pre-aggregated — can the Reporting API return per-order / per-line-item granularity with timestamps, or is it summary-only? If it’s only aggregated, we’d still need the Orders API for the hourly breakdown.

4. Migration cost. Our pipeline is already built around storing raw orders. That’s not a hard no — if the three points above check out (custom cutoff, stability, and order-level granularity), we’d seriously consider moving the summary figures to the Reporting API and keeping Orders only where we need the detail.

Appreciate any guidance on those.

Gentle bump — any thoughts on the custom 3 AM PT cutoff and per-order timestamp granularity? Thanks!

The Reporting API does support item-level and order-level detail. You’ll want to query the cubes directly (e.g., Orders, ItemTransactions) rather than the prebuilt views to get that granularity.

I’m still investigating the reporting timeframe portion but will follow up shortly.

The API follows Square’s date-based versioning, and as a public beta, changes would be versioned rather than breaking your integration.

Relevant docs:

Time Dimensions
Working with Cubes
Reporting API Overview

Following up on the reporting timeframe question:

The *.reporting_day dimensions in Reporting API are meant to respect the seller’s configured day-start time set in their Square Dashboard (Dashboard → Reports → Settings → Reporting timeframes), but that wiring isn’t fully complete yet, so please treat them as preview for now.

For your 3 AM PT cutoff in the meantime, you could query the cube at a finer grain using the local timestamp dimension and apply your own day boundary logic on your end.

Update: I went ahead and implemented the Reporting API for the Sales Summary (querying the Orders cube on Orders.sale_timestamp with a 2026-05-26T10:00:00 → 2026-05-27T09:59:59 UTC range, which correctly gives me the 3 AM–3 AM PT business day — so thank you, the custom cutoff works).

Most measures match the Dashboard exactly:

Dashboard line Dashboard API measure API
Gross sales $6,087.41 top_line_product_sales 6087.41 :white_check_mark:
Returns $6.40 itemized_returns 6.40 :white_check_mark:
Discounts & comps ($148.11) discount_and_comps_sum -148.11 :white_check_mark:
Net sales $5,932.90 (550) net_sales / _count 5932.90 / 550 :white_check_mark:
Taxes $28.14 sales_tax_amount 28.14 :white_check_mark:
Tips $71.87 tips_amount 71.87 :white_check_mark:
Deferred sales $0.00 gift_card_sales_amount 0.00 :white_check_mark:

But three things don’t reconcile, and I’d appreciate help:

1. Orders.total_sales_amount contradicts its own documented formula. The measure’s description says: gross − discounts/comps − returns + taxes + tips + gift cards − refunds. Applying that to the sibling measures above:

6087.41 − 148.11 − 6.40 + 28.14 + 71.87 + 0 − 0 = 6032.91

That equals the Dashboard’s “Total sales” ($6,032.91) exactly. But the API returns Orders.total_sales_amount = 6040.54 — $7.63 higher. Why does the field disagree with both its documented formula and the Dashboard?

2. Orders.total_collected_amount doesn’t match Dashboard “Total payments collected”, and the amounts look swapped vs. the counts.

Dashboard line Dashboard $ Dashboard count API measure API $ API count
Total sales 6032.91 549 total_sales_amount 6040.54 549
Total payments collected 6039.31 551 total_collected_amount 6032.91 551

The counts line up by name (549↔549, 551↔551), but the amounts are off — and oddly, the Dashboard’s “Total sales” value (6032.91) is exactly what the API reports for total_collected_amount. Also note Dashboard “Total payments collected” − “Total sales” = 6039.31 − 6032.91 = $6.40, exactly itemized_returns. So which measure is supposed to map to which Dashboard line, and is there a known labeling/offset issue between total_sales_amount and total_collected_amount?

3. The avg_* measures don’t equal amount / *_count. All three averages divide by the same constant (~581), which matches neither the per-measure _count (550/549/551) nor the gross-sales count (561):

net_sales 5932.90 / 10.21153 = 581

total_sales 6040.54 / 10.39680 = 581

total_collected 6032.91 / 10.38367 = 581

Is avg_* defined as amount / (total order count for the period) rather than amount / measure_count? If so, is that total order count (581 here) exposed as its own measure so I can reproduce the averages?

Thanks in advance — #1 and #2 are the blockers for me trusting Total Sales / Total Collected.

Still looking through these and will follow back up with you early next week!