The sales report
Sales, refunds, discounts and averages for your merchant over a date range — as a total, per month, per day and per ticket type, on the day things happened or on the visit date.
One endpoint answers every financial question the API can answer for a merchant: GET /merchant-accounts/reports/sales. You give it a date range and it returns what sold, what was refunded, what was discounted, and the averages — for the period as a whole, broken down by month and by day, and split by ticket type at each of those levels.
Figures are bucketed by calendar day, in UTC, and you choose which day. The default, date_basis=visit, puts a sale and its refund on the ticket's visit date. date_basis=transaction puts a sale on the day the order completed and a refund on the day it was processed. Two date axes explains when to use which.
The request
curl -G https://api.acc.funtrips.io/v2/merchant-accounts/reports/sales \
-H "Authorization: Bearer $MERCHANT_TOKEN" \
--data-urlencode "from=2026-08-01" \
--data-urlencode "to=2026-08-31" \
--data-urlencode "scope=summary,ticket_types,daily"| Parameter | Required | Notes |
|---|---|---|
from | Yes | First date included, YYYY-MM-DD, on the selected date_basis |
to | Yes | Last date included. Must not be before from. Inclusive on both ends, so one day is from=to |
date_basis | No | visit (default) or transaction. Which date the figures are grouped and filtered on. visit excludes undated tickets; transaction counts every ticket |
debtor_code | Depends | Which of your merchants to report on. Optional when your token covers exactly one; required otherwise, and must be one the token names |
sku | No | Comma-separated ticket types to restrict the ticket-level figures to. See Filtering by ticket type |
scope | No | Comma-separated sections to return: summary, ticket_types, monthly, daily. Default is all four |
There is no maximum range. A year is a fine request; ask for what you need and let scope keep the response small.
The response
Trimmed to one day and one ticket type:
{
"data": {
"debtor_code": "DEB-10428",
"date_basis": "visit",
"period": { "from": "2026-08-01", "to": "2026-08-31" },
"summary": {
"metrics": {
"orders": 412,
"refunded_orders": 7,
"tickets_sold": 1093,
"tickets_refunded": 15,
"sales": { "value": "24862.50", "currency": "EUR" },
"refunds": { "value": "337.50", "currency": "EUR" },
"discounts": {
"total": { "value": "1846.00", "currency": "EUR" },
"discounted_tickets": 302,
"max_unit_discount": { "value": "12.50", "currency": "EUR" },
"average_per_ticket": { "value": "1.69", "currency": "EUR" }
},
"averages": {
"ticket_price": { "value": "22.75", "currency": "EUR" },
"order_value": { "value": "61.42", "currency": "EUR" },
"tickets_per_order": "2.65"
}
},
"by_ticket_type": [
{
"sku": "DAY-ADULT",
"product_title": "Day ticket — adult",
"tickets_sold": 640,
"tickets_refunded": 9,
"sales": { "value": "16000.00", "currency": "EUR" },
"refunds": { "value": "225.00", "currency": "EUR" },
"discounts": { "…": "same shape as above" },
"unit_price": {
"min": { "value": "20.00", "currency": "EUR" },
"max": { "value": "27.50", "currency": "EUR" },
"average": { "value": "25.00", "currency": "EUR" }
}
}
]
},
"monthly": [ { "month": "2026-08", "metrics": { "…": "…" }, "by_ticket_type": [ "…" ] } ],
"daily": [ { "date": "2026-08-01", "metrics": { "…": "…" }, "by_ticket_type": [ "…" ] } ]
},
"meta": { "correlation_id": "…" }
}summary, monthly and daily each carry the same metrics block, so one parser serves all three. by_ticket_type appears inside each of them when ticket_types is in scope. date_basis echoes the axis the figures are on, so a stored report is never ambiguous.
Two date axes
The same ticket has two dates: the day it was bought and the day it is for. Which one you group on decides what the report is good for.
Undated tickets are not on the visit axisOpen tickets and physical goods have no visit date, so
date_basis=visitleaves them out completely — they appear in no day, month, ticket-type row or summary total. If you need every ticket, including undated ones, usedate_basis=transaction. It is the only axis that counts everything.
date_basis=visit (default) | date_basis=transaction | |
|---|---|---|
| A sale counts on | The ticket's visit date | The day the order completed |
| A refund counts on | The ticket's visit date | The day the refund was processed |
| A closed period | Can still move — see below | Never changes |
| Tickets without a visit date | Not included — transaction basis only | Dated like any other sale |
| Freshness | Rebuilt every six hours | Live, current day partial |
| Answers | "How many tickets are valid for each day, net of refunds?" | "What did we sell and refund this month?" |
| Use it for | Capacity, attendance planning, per-day demand | Bookkeeping, month-end close, reconciliation over time |
Visit is the operational view. A ticket bought in March for a visit on 14 August sits under 14 August, and if it is refunded in July it comes back out of 14 August — so tickets_sold − tickets_refunded for a visit day is the number of tickets still valid for that day. Two consequences follow:
- Rows for future visit dates keep changing as tickets sell and are refunded, right up to the day itself. A visit day in the past can still move too, if a refund for it is processed afterwards.
- The money figures are informational. Revenue attributed to a visit date is not what you were paid for that date, and it will not tie out to a payout. Reconcile on the transaction basis; plan on the visit basis.
Transaction is the accounting view. A day's figures are final once the day is over, so you can archive August and never re-read it. It is not the default: if you are reconciling money, pass date_basis=transaction explicitly rather than relying on the default.
from and to filter on the selected axis. On the visit basis a request for 1–31 August returns tickets for August, whenever they were bought.
Tickets without a visit date
Open tickets and physical goods have no fixed visit day, so they cannot be placed on the visit axis. On the visit basis they are left out entirely — not counted in any day, month or summary figure. On the transaction basis they are dated like any other sale and counted in full.
An order is placed on the visit axis by the earliest visit date among its tickets. An order made up only of undated tickets is not on that axis at all.
One consequence: if you sell open tickets, the two axes will not add up to each other. The transaction basis is the complete one; the visit basis is the dated one.
Reading the numbers
The schema tells you the shapes. These are the rules the shapes cannot express.
Sales and refunds are separate
sales is what sold in the period. refunds is what was refunded in the period. They are not netted — net revenue is sales − refunds, and you compute it. The same holds per ticket type: tickets_sold and tickets_refunded are independent counts.
On the transaction basis the two are dated differently: an order sold in March and refunded in April appears in March's orders and sales, and in April's refunded_orders and refunds. Both months are honest for their own dates; neither is restated. On the visit basis both land on the visit date, which is what makes per-day netting meaningful there.
Discounts are net; the maximum is not
discounts.total is the discount granted on the tickets in the period, net of refunds — a refunded ticket takes its discount back out. discounted_tickets is netted the same way. average_per_ticket is total / tickets_sold.
max_unit_discount is the largest discount granted on any single ticket in the period, whether or not that ticket was later refunded. A maximum cannot be reversed, so it is not.
Averages
| Field | Computed as |
|---|---|
averages.ticket_price | sales / tickets_sold — the average price actually paid per ticket, after discount |
averages.order_value | The total of each completed order, averaged over orders |
averages.tickets_per_order | Tickets in completed orders, divided by orders. A decimal string, two places |
unit_price.min / max | The lowest and highest price paid for one ticket of that type in the period, after discount |
unit_price.average | sales / tickets_sold for that type |
An order's total is what the customer paid for the whole basket, so order_value is not simply sales / orders — the basket can include costs that are not ticket revenue.
Every average is 0.00 when its denominator is zero. A ticket type that saw only refunds in the period has unit_price of 0.00 throughout, because no sale was observed to take a price from.
Only days with activity appear
daily lists the days that had at least one sale or refund, in date order. A quiet day is absent, not zero. If you chart the month, fill the gaps yourself. monthly behaves the same way, keyed by YYYY-MM.
by_ticket_type is sorted by sku, and lists only the ticket types with activity at that level.
One currency per report
Every amount in a report is in the same currency, taken from the orders in the range. A range that spans two currencies cannot be aggregated and is refused with 502 and type: urn:qup:error:report-mixed-currencies. Narrow the range until each report is single-currency. Merchants selling in one currency — nearly all — never see this.
Freshness
On the transaction basis figures are live. The current day may still be partial, so pull yesterday, not today, for anything you keep. Older days are stable except where a refund lands — a refund is dated on its own day, so it never rewrites an earlier one, but a recent day can gain a refund after you first read it. If you mirror the report, re-read the last few days on each run.
On the visit basis figures are rebuilt every six hours and can trail the transaction basis by up to six hours. Rows for dates still to come are, by nature, not final. Treat a visit-basis read as a snapshot, and re-read rather than cache.
Filtering by ticket type
sku restricts the ticket-level figures — tickets_sold, tickets_refunded, sales, refunds, discounts, unit_price, ticket_price — to the listed ticket types. Pass several as a comma-separated list.
It does not restrict the order-level figures: orders, refunded_orders, order_value and tickets_per_order always cover the merchant's full order volume. An order can contain ticket types outside the filter, and there is no meaningful "orders for this SKU" count without double-counting mixed baskets. If you need per-type order counts, leave sku off and take them from by_ticket_type.
Unknown SKUs are not an error; they simply match nothing.
Choosing sections with scope
scope| You want | Send |
|---|---|
| Period totals only | scope=summary |
| Totals split by ticket type | scope=summary,ticket_types |
| A daily chart | scope=daily (add ticket_types for a stacked one) |
| Month-end figures for a year | scope=monthly&date_basis=transaction with a twelve-month range |
| Expected attendance per day | scope=daily — the visit axis is the default |
| Everything | Omit scope |
ticket_types is a modifier: it adds by_ticket_type to whichever of the other sections are present. On its own it returns the summary with its ticket-type breakdown.
Several merchants on one credential
A token that names several debtor codes reads them one at a time — debtor_code is required on every call, and there is no combined report. Loop over your debtor codes; the figures are independent and the calls can run in parallel.
for dc in DEB-10428 DEB-10431; do
curl -sG https://api.acc.funtrips.io/v2/merchant-accounts/reports/sales \
-H "Authorization: Bearer $MERCHANT_TOKEN" \
--data-urlencode "from=2026-08-01" --data-urlencode "to=2026-08-31" \
--data-urlencode "debtor_code=$dc" --data-urlencode "scope=summary" \
> "sales-$dc-2026-08.json"
doneOmitting debtor_code on such a token is a 400 with type: urn:qup:error:debtor-code-required. Naming one the token does not cover is a bare 403.
Two things this report is not
It is not a payout statement. Transaction-basis figures are by sale date; payouts follow their own schedule and net out fees and commission. Expect the month's sales − refunds to reconcile with your payouts over time, not line for line. Visit-basis money figures do not reconcile with payouts at all — they are attributed to a date you were not paid on.
It is not an order list. There is no per-order or per-ticket detail here, and no customer data — only aggregates. That is what makes it safe to hand to a dashboard.
Errors
| Status | type | What happened | What to do |
|---|---|---|---|
400 | — | from or to missing or not YYYY-MM-DD; to before from; unknown scope or date_basis value | Fix the request |
400 | debtor-code-required | Token covers several merchants and no debtor_code was sent | Pass debtor_code |
401 | — | Token missing, expired or invalid | Fetch a new token, retry once |
403 | — | debtor_code is not one the token names, or the token lacks the reports scope | Check the code; otherwise contact us |
502 | report-mixed-currencies | The range spans more than one currency | Narrow the range |
502 | sales-report-failed | The reporting store did not answer | Retry with backoff |
500 | — | Unexpected fault | Retry, then quote the correlation id |
An empty period is not an error: a range with no activity returns 200 with zero totals and empty daily, monthly and by_ticket_type arrays.
Updated about 4 hours ago
