# Reports

Per-tier rollups of sales and attendance, computed on demand and returned as
JSON — or as a **CSV download** with `format=csv`. Amounts are in **major
units of your account currency** (`49.99`, not `4999`).

Both endpoints require the **`reports:read`** scope.

> **Counting semantics.** `tickets_issued` counts **all** tickets created in
> the window — including ones later refunded or revoked. `tickets_refunded`
> counts tickets whose refund completed (the money moved); `tickets_revoked`
> counts tickets with a revocation — the two are independent, so one ticket
> can appear in both. `gross` is the pre-refund gross of all issued tickets.

---

## GET /v1/reports/sales

Per-tier sales rollup — issued / refunded / revoked counts and gross value —
over a date window on ticket **creation time**, optionally narrowed to one
event. Tiers sort alphabetically (untiered tickets group under `null`, first).

**Scope:** `reports:read`

### Query params

| Param | Type | Required | Description |
| --- | --- | --- | --- |
| `event_id` | string | No | Narrow the report to one event; omit for the whole account. |
| `from` | string | No | Window start, ISO 8601 (date-only means midnight UTC). Default: 30 days before `to`. |
| `to` | string | No | Window end, ISO 8601, inclusive. Default: now. Max window: 366 days. |
| `format` | string | No | `json` (default) or `csv`. |

> **Date-only `to` excludes that day's activity after 00:00 UTC.** The match is
> inclusive on both ends, but `"to": "2026-07-11"` means `2026-07-11T00:00:00Z`
> — pass a full timestamp (or the next day) for end-of-day semantics.

### Example request

```bash
curl "https://api.ticketconnect.example/v1/reports/sales?event_id=evt_a1b2c3d4e5f6a7b8c9d0e1f2&from=2026-06-01&to=2026-07-01T00:00:00Z" \
  -H "Authorization: Bearer sk_test_your_key_here"
```

### Example response

```json
{
  "object": "report",
  "data": {
    "report": "sales",
    "event_id": "evt_a1b2c3d4e5f6a7b8c9d0e1f2",
    "from": "2026-06-01T00:00:00.000Z",
    "to": "2026-07-01T00:00:00.000Z",
    "currency": "USD",
    "tiers": [
      { "tier": "General Admission", "tickets_issued": 1180, "tickets_refunded": 22, "tickets_revoked": 3, "gross": 57810.20 },
      { "tier": "VIP", "tickets_issued": 60, "tickets_refunded": 1, "tickets_revoked": 0, "gross": 8940.00 }
    ],
    "totals": { "tickets_issued": 1240, "tickets_refunded": 23, "tickets_revoked": 3, "gross": 66750.20 }
  },
  "generated_at": "2026-07-11T09:00:00.000Z"
}
```

With `format=csv` the response is `text/csv` (with a
`Content-Disposition: attachment` header and a dated filename): one row per
tier plus a `TOTAL` row, columns `Tier, Tickets Issued, Tickets Refunded,
Tickets Revoked, Gross, Currency`. Column order is fixed — safe to consume
positionally — and cell values are sanitized against spreadsheet formula
injection.

> Errors: `400 invalid_date_window` (unparsable `from`/`to`, `from` after
> `to`, or a window over 366 days), `400 invalid_format`, `404 not_found`
> (unknown or foreign `event_id`).

---

## GET /v1/reports/attendance

Issued vs checked-in counts per tier, no-show count, and an hourly UTC
check-in timeline — for **one event** (`event_id` is required).

`no_show` counts tickets that are still valid (not refunded, not revoked) and
were never checked in, so `checked_in + no_show ≤ tickets_issued`.

**Scope:** `reports:read`

### Query params

| Param | Type | Required | Description |
| --- | --- | --- | --- |
| `event_id` | string | Yes | The event to report on. |
| `format` | string | No | `json` (default) or `csv`. |

### Example request

```bash
curl "https://api.ticketconnect.example/v1/reports/attendance?event_id=evt_a1b2c3d4e5f6a7b8c9d0e1f2" \
  -H "Authorization: Bearer sk_test_your_key_here"
```

### Example response

```json
{
  "object": "report",
  "data": {
    "report": "attendance",
    "event_id": "evt_a1b2c3d4e5f6a7b8c9d0e1f2",
    "tiers": [
      { "tier": "General Admission", "tickets_issued": 1180, "checked_in": 934, "no_show": 221 },
      { "tier": "VIP", "tickets_issued": 60, "checked_in": 46, "no_show": 13 }
    ],
    "totals": { "tickets_issued": 1240, "checked_in": 980, "no_show": 234 },
    "timeline": [
      { "hour": "2026-08-15T17:00:00.000Z", "checkins": 412 },
      { "hour": "2026-08-15T18:00:00.000Z", "checkins": 486 }
    ]
  },
  "generated_at": "2026-07-11T09:00:00.000Z"
}
```

The `timeline` buckets check-ins by UTC hour, sorted ascending, and is
**sparse** — hours with zero check-ins are omitted.

With `format=csv` the response is one flat table (`Row Type, Tier, Tickets
Issued, Checked In, No Show, Hour, Check-ins`): the tier rows, a `TOTAL` row,
then the hourly timeline rows.

> Errors: `400 parameter_missing` (no `event_id`), `400 invalid_format`,
> `404 not_found` (unknown or foreign `event_id`).
