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"
ParameterRequiredNotes
fromYesFirst date included, YYYY-MM-DD, on the selected date_basis
toYesLast date included. Must not be before from. Inclusive on both ends, so one day is from=to
date_basisNovisit (default) or transaction. Which date the figures are grouped and filtered on. visit excludes undated tickets; transaction counts every ticket
debtor_codeDependsWhich of your merchants to report on. Optional when your token covers exactly one; required otherwise, and must be one the token names
skuNoComma-separated ticket types to restrict the ticket-level figures to. See Filtering by ticket type
scopeNoComma-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 axis

Open tickets and physical goods have no visit date, so date_basis=visit leaves them out completely — they appear in no day, month, ticket-type row or summary total. If you need every ticket, including undated ones, use date_basis=transaction. It is the only axis that counts everything.

date_basis=visit (default)date_basis=transaction
A sale counts onThe ticket's visit dateThe day the order completed
A refund counts onThe ticket's visit dateThe day the refund was processed
A closed periodCan still move — see belowNever changes
Tickets without a visit dateNot included — transaction basis onlyDated like any other sale
FreshnessRebuilt every six hoursLive, 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 forCapacity, attendance planning, per-day demandBookkeeping, 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

FieldComputed as
averages.ticket_pricesales / tickets_sold — the average price actually paid per ticket, after discount
averages.order_valueThe total of each completed order, averaged over orders
averages.tickets_per_orderTickets in completed orders, divided by orders. A decimal string, two places
unit_price.min / maxThe lowest and highest price paid for one ticket of that type in the period, after discount
unit_price.averagesales / 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

You wantSend
Period totals onlyscope=summary
Totals split by ticket typescope=summary,ticket_types
A daily chartscope=daily (add ticket_types for a stacked one)
Month-end figures for a yearscope=monthly&date_basis=transaction with a twelve-month range
Expected attendance per dayscope=daily — the visit axis is the default
EverythingOmit 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 timedebtor_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"
done

Omitting 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

StatustypeWhat happenedWhat to do
400from or to missing or not YYYY-MM-DD; to before from; unknown scope or date_basis valueFix the request
400debtor-code-requiredToken covers several merchants and no debtor_code was sentPass debtor_code
401Token missing, expired or invalidFetch a new token, retry once
403debtor_code is not one the token names, or the token lacks the reports scopeCheck the code; otherwise contact us
502report-mixed-currenciesThe range spans more than one currencyNarrow the range
502sales-report-failedThe reporting store did not answerRetry with backoff
500Unexpected faultRetry, 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.


Did this page help you?