# Email reports

Scheduled digest emails with a site's stats, one subscription per recipient. Reports are generated and mailed by Totallytics; recipients need no account.

Base URL: `https://analytics.bitgate.dev`. [Download OpenAPI](/openapi.json) or [read this page as Markdown](/docs/email-reports.md).

## Access

All `/api/sites/{hostname}/reports` endpoints require the site owner Firebase ID bearer token, even for public sites. The unsubscribe endpoints are public: the token in the link is the capability.

| Method | Route                                                | Result                                    |
| ------ | ---------------------------------------------------- | ----------------------------------------- |
| GET    | `/api/sites/{hostname}/reports`                      | List subscriptions                        |
| POST   | `/api/sites/{hostname}/reports`                      | Add a recipient (`201`)                   |
| PATCH  | `/api/sites/{hostname}/reports/{id}`                 | Update frequency, schedule, sections      |
| DELETE | `/api/sites/{hostname}/reports/{id}`                 | Remove a recipient                        |
| POST   | `/api/sites/{hostname}/reports/{id}/test`            | Queue a test email for the current window |
| GET    | `/api/sites/{hostname}/reports/preview`              | Render the email as HTML                  |
| GET    | `/api/reports/unsubscribe/{token}`                   | Confirmation page (HTML)                  |
| POST   | `/api/reports/unsubscribe/{token}`                   | One-click unsubscribe (HTML)              |

## Cadence and windows

| Frequency | Sends                | Covers                    |
| --------- | -------------------- | ------------------------- |
| `daily`   | every day            | yesterday (local)         |
| `weekly`  | Mondays              | previous Monday to Sunday |
| `monthly` | the 1st of the month | previous calendar month   |

`send_hour` is a local wall-clock hour (0-23) in the subscription's IANA `timezone`; daylight-saving changes follow the clock, not a fixed UTC offset. Every report covers the last *fully elapsed* period, so a weekly report mailed Monday 09:00 Europe/Amsterdam always covers the Monday-Sunday that just ended in that timezone.

## Sections

| Key         | Default   | Contents                                              |
| ----------- | --------- | ----------------------------------------------------- |
| `overview`  | always on | Totals, change vs previous period, chart, live count  |
| `pages`     | on        | Top 5 pages with change vs previous period            |
| `referrers` | on        | Top 5 referrers with change                           |
| `countries` | off       | Top 5 countries with change                           |
| `events`    | off       | Top 5 events with change                              |
| `alerts`    | on        | Spike/dip callout for abnormal days                   |

The alert section flags a day when its pageviews land more than 2.5 standard deviations and at least 50% away from the trailing 28-day mean, and names the top referrers driving a spike. Sites need about two weeks of history before alerts can trigger.

## Create a subscription

`POST /api/sites/{hostname}/reports` accepts `email`, `frequency`, and optional `timezone` (default `UTC`), `send_hour` (default `9`) and `sections`. One subscription per site, email and frequency; duplicates return `409`. At most 25 recipients per site.

```bash
curl -X POST "$TT_HOST/api/sites/$TT_HOSTNAME/reports" \
  -H "Authorization: Bearer $TT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"email":"teammate@company.com","frequency":"weekly","timezone":"Europe/Amsterdam","send_hour":9}'
```

`PATCH` accepts any subset of `frequency`, `timezone`, `send_hour`, `enabled`, `sections`. Setting `enabled` to `false` pauses without deleting.

`POST .../test` queues a real email for the current last-full-period window and returns `202` with the window it used. Test sends never consume or block the scheduled window.

`GET .../preview?frequency=weekly&tz=Europe/Amsterdam&sections=pages,referrers,alerts` renders the email HTML inline; omit `sections` to see everything on.

## Delivery and unsubscribe

Mail is sent from `reports@totallytics.com` over a Postmark broadcast stream with `List-Unsubscribe` and RFC 8058 one-click headers. Unsubscribing sets `enabled` to `false` and keeps history; re-enable from the site's settings. A permanent failure (invalid address or previously hard-bounced recipient) disables the subscription immediately; transient failures retry three times, then the send is marked failed and `fail_count` rises. Subscriptions with five consecutive failures stop being scheduled until re-enabled.
