Skip to content

Commit 93e67f0

Browse files
authored
docs: Add API docs link to admin dashboard, improve API reference (#21)
* docs: Add API docs link to admin dashboard, improve API reference - Add "View API Documentation →" link in API Keys section of admin dashboard - Replace localhost URLs with {base_url} placeholder throughout API docs - Add Base URL section explaining how to configure for different environments - Expand Authentication section with API key usage examples - Document that API keys are managed via Admin Dashboard * fix: Link to internal Swagger docs instead of external GitHub Pages * feat: Improve API docs and add user-scoped CSV export endpoints - Hide admin and OAuth routes from Swagger docs (internal use only) - Add proper tags to all API routers (System, Sleep, Data, Sync, API Keys, Export) - Add new user-scoped CSV export endpoints at /api/v1/users/{user_id}/export/*.csv - Add Literal types with enum dropdowns for metric_name and pattern_name parameters - Add examples and descriptions for date parameters (YYYY-MM-DD format) - Fix admin CSV exports to filter by connected user's polar_user_id (multi-tenancy fix) * docs: Update changelog and API docs for CSV export endpoints - Add unreleased section to CHANGELOG with new features - Document CSV export endpoints in API overview - Include column descriptions for each CSV format
1 parent 39a450a commit 93e67f0

File tree

14 files changed

+521
-42
lines changed

14 files changed

+521
-42
lines changed

CHANGELOG.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Added
11+
12+
**User-Scoped CSV Export API**
13+
- New endpoints for programmatic CSV downloads:
14+
- `GET /api/v1/users/{user_id}/export/sleep.csv`
15+
- `GET /api/v1/users/{user_id}/export/activity.csv`
16+
- `GET /api/v1/users/{user_id}/export/recharge.csv`
17+
- `GET /api/v1/users/{user_id}/export/cardio-load.csv`
18+
- All exports support `days` query parameter (1-365, default 30)
19+
- Protected by per-user API key authentication
20+
21+
**Improved OpenAPI Documentation**
22+
- Added proper tags to all API routers (System, Sleep, Data, Sync, API Keys, Export, Baselines, Patterns, Insights)
23+
- Added enum dropdowns for `metric_name` parameter (hrv_rmssd, sleep_score, resting_hr, training_load, training_load_ratio)
24+
- Added enum dropdowns for `pattern_name` parameter (sleep_hrv_correlation, overtraining_risk, hrv_trend, sleep_trend, etc.)
25+
- Added examples and descriptions for date parameters (YYYY-MM-DD format)
26+
- Added examples for exercise_id and anomaly check value parameters
27+
28+
### Changed
29+
30+
- Admin and OAuth routes hidden from Swagger docs (internal use only)
31+
- Admin CSV exports now filter by connected user's `polar_user_id` (multi-tenancy fix)
32+
1033
---
1134

1235
## [1.3.3] - 2026-01-21

docs/api/overview.md

Lines changed: 66 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,16 @@
22

33
REST API for accessing synced Polar health data.
44

5-
Base URL: `http://localhost:8000/api/v1`
5+
## Base URL
6+
7+
```
8+
{base_url}/api/v1
9+
```
10+
11+
Replace `{base_url}` with your server address:
12+
- **Local development**: `http://localhost:8000`
13+
- **Docker**: `http://localhost:8000` or your configured port
14+
- **Production**: Your deployed server URL (e.g., `https://polar.example.com`)
615

716
## OpenAPI Specification
817

@@ -14,14 +23,28 @@ The full OpenAPI 3.1 specification is available at:
1423

1524
```bash
1625
# Download OpenAPI spec
17-
curl http://localhost:8000/schema/openapi.json > openapi.json
26+
curl {base_url}/schema/openapi.json > openapi.json
1827
```
1928

2029
## Authentication
2130

22-
All data endpoints require a `user_id` in the URL path. For self-hosted deployments, this is your Polar user ID. For SaaS integrations, this is your application's user identifier.
31+
### API Key Authentication
32+
33+
If an API key is configured (recommended for production), include it in the `X-API-Key` header:
34+
35+
```bash
36+
curl -H "X-API-Key: your_api_key_here" {base_url}/api/v1/users/12345/sleep
37+
```
38+
39+
API keys can be created and managed from the **Admin Dashboard → API Keys** section.
40+
41+
### User ID
42+
43+
All data endpoints require a `user_id` in the URL path. For self-hosted deployments, this is your Polar user ID (visible in the admin dashboard). For SaaS integrations, this is your application's user identifier.
44+
45+
### Sync Endpoints
2346

24-
Sync endpoints require a Polar API access token via the `X-Polar-Token` header.
47+
Sync endpoints additionally require a Polar API access token via the `X-Polar-Token` header.
2548

2649
## Endpoints
2750

@@ -37,7 +60,7 @@ Sync endpoints require a Polar API access token via the `X-Polar-Token` header.
3760

3861
**Example:**
3962
```bash
40-
curl http://localhost:8000/api/v1/users/12345/sleep?days=30
63+
curl {base_url}/api/v1/users/12345/sleep?days=30
4164
```
4265

4366
**Response:**
@@ -70,7 +93,7 @@ curl http://localhost:8000/api/v1/users/12345/sleep?days=30
7093

7194
**Example:**
7295
```bash
73-
curl http://localhost:8000/api/v1/users/12345/activity?days=7
96+
curl {base_url}/api/v1/users/12345/activity?days=7
7497
```
7598

7699
**Response:**
@@ -101,7 +124,7 @@ curl http://localhost:8000/api/v1/users/12345/activity?days=7
101124

102125
**Example:**
103126
```bash
104-
curl http://localhost:8000/api/v1/users/12345/recharge?days=14
127+
curl {base_url}/api/v1/users/12345/recharge?days=14
105128
```
106129

107130
**Response:**
@@ -132,7 +155,7 @@ curl http://localhost:8000/api/v1/users/12345/recharge?days=14
132155

133156
**Example:**
134157
```bash
135-
curl http://localhost:8000/api/v1/users/12345/exercises?days=30
158+
curl {base_url}/api/v1/users/12345/exercises?days=30
136159
```
137160

138161
**Response:**
@@ -289,7 +312,7 @@ Requires compatible device with Elixir sensor platform.
289312
```bash
290313
curl -X POST \
291314
-H "X-Polar-Token: your_polar_token" \
292-
http://localhost:8000/api/v1/users/12345/sync/trigger?days=30
315+
{base_url}/api/v1/users/12345/sync/trigger?days=30
293316
```
294317

295318
**Response:**
@@ -320,10 +343,16 @@ curl -X POST \
320343
| Method | Endpoint | Description |
321344
|--------|----------|-------------|
322345
| GET | `/users/{user_id}/export/summary` | Export summary manifest |
346+
| GET | `/users/{user_id}/export/sleep.csv` | Export sleep data as CSV |
347+
| GET | `/users/{user_id}/export/activity.csv` | Export activity data as CSV |
348+
| GET | `/users/{user_id}/export/recharge.csv` | Export recharge/HRV data as CSV |
349+
| GET | `/users/{user_id}/export/cardio-load.csv` | Export cardio load data as CSV |
323350

324351
**Query Parameters:**
325352
- `days` (int, default: 30) - Number of days to include (1-365)
326353

354+
#### Export Summary
355+
327356
**Response:**
328357
```json
329358
{
@@ -343,6 +372,25 @@ curl -X POST \
343372
}
344373
```
345374

375+
#### CSV Exports
376+
377+
Download data as CSV files for use in spreadsheets, data analysis tools, or external systems.
378+
379+
**Example:**
380+
```bash
381+
curl -H "X-API-Key: pfk_..." \
382+
"{base_url}/api/v1/users/12345/export/sleep.csv?days=90" \
383+
-o sleep_data.csv
384+
```
385+
386+
**Sleep CSV columns:** `date`, `sleep_score`, `total_hours`, `deep_hours`, `light_hours`, `rem_hours`, `hrv_avg`, `heart_rate_avg`, `breathing_rate_avg`
387+
388+
**Activity CSV columns:** `date`, `steps`, `calories_active`, `calories_total`, `distance_km`, `active_minutes`
389+
390+
**Recharge CSV columns:** `date`, `hrv_avg`, `ans_charge`, `status`, `breathing_rate`, `heart_rate_avg`
391+
392+
**Cardio Load CSV columns:** `date`, `strain`, `tolerance`, `cardio_load`, `load_ratio`, `status`
393+
346394
---
347395

348396
### Baselines & Analytics
@@ -367,7 +415,7 @@ Personal baselines computed from historical data. Use these for anomaly detectio
367415
#### Get All Baselines
368416

369417
```bash
370-
curl http://localhost:8000/api/v1/users/12345/baselines
418+
curl {base_url}/api/v1/users/12345/baselines
371419
```
372420

373421
**Response:**
@@ -409,7 +457,7 @@ Uses IQR-based anomaly detection:
409457
- **Critical**: value outside Q1 - 3×IQR to Q3 + 3×IQR
410458

411459
```bash
412-
curl http://localhost:8000/api/v1/users/12345/baselines/check/hrv_rmssd/25.5
460+
curl {base_url}/api/v1/users/12345/baselines/check/hrv_rmssd/25.5
413461
```
414462

415463
**Response:**
@@ -432,7 +480,7 @@ curl http://localhost:8000/api/v1/users/12345/baselines/check/hrv_rmssd/25.5
432480
Trigger baseline recalculation from historical data:
433481

434482
```bash
435-
curl -X POST http://localhost:8000/api/v1/users/12345/baselines/calculate
483+
curl -X POST {base_url}/api/v1/users/12345/baselines/calculate
436484
```
437485

438486
**Response:**
@@ -454,7 +502,7 @@ curl -X POST http://localhost:8000/api/v1/users/12345/baselines/calculate
454502
Check feature availability based on data history:
455503

456504
```bash
457-
curl http://localhost:8000/api/v1/users/12345/analytics/status
505+
curl {base_url}/api/v1/users/12345/analytics/status
458506
```
459507

460508
**Response:**
@@ -527,7 +575,7 @@ Advanced pattern detection for correlations, trends, and risk assessment.
527575
#### Get All Patterns
528576

529577
```bash
530-
curl http://localhost:8000/api/v1/users/12345/patterns
578+
curl {base_url}/api/v1/users/12345/patterns
531579
```
532580

533581
**Response:**
@@ -577,15 +625,15 @@ curl http://localhost:8000/api/v1/users/12345/patterns
577625
#### Get Specific Pattern
578626

579627
```bash
580-
curl http://localhost:8000/api/v1/users/12345/patterns/overtraining_risk
628+
curl {base_url}/api/v1/users/12345/patterns/overtraining_risk
581629
```
582630

583631
#### Trigger Pattern Detection
584632

585633
Analyzes historical data and stores pattern results:
586634

587635
```bash
588-
curl -X POST http://localhost:8000/api/v1/users/12345/patterns/detect
636+
curl -X POST {base_url}/api/v1/users/12345/patterns/detect
589637
```
590638

591639
**Response:**
@@ -606,7 +654,7 @@ curl -X POST http://localhost:8000/api/v1/users/12345/patterns/detect
606654
Scans all metrics against stored baselines and returns any values outside normal bounds:
607655

608656
```bash
609-
curl http://localhost:8000/api/v1/users/12345/anomalies
657+
curl {base_url}/api/v1/users/12345/anomalies
610658
```
611659

612660
**Response:**
@@ -677,7 +725,7 @@ Features unlock progressively as more data becomes available:
677725
#### Example Request
678726

679727
```bash
680-
curl http://localhost:8000/api/v1/users/12345/insights
728+
curl {base_url}/api/v1/users/12345/insights
681729
```
682730

683731
#### Example Response (30+ days of data)

src/polar_flow_server/admin/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
from polar_flow_server.admin.routes import admin_routes
66

7-
admin_router = Router(path="/admin", route_handlers=admin_routes, tags=["Admin"])
7+
admin_router = Router(
8+
path="/admin", route_handlers=admin_routes, tags=["Admin"], include_in_schema=False
9+
)
810

911
__all__ = ["admin_router"]

src/polar_flow_server/admin/routes.py

Lines changed: 40 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1575,12 +1575,20 @@ async def export_sleep_csv(
15751575
session: AsyncSession,
15761576
days: int = 30,
15771577
) -> Response[bytes] | Redirect:
1578-
"""Export sleep data as CSV."""
1578+
"""Export sleep data as CSV for the connected user."""
15791579
if not is_authenticated(request):
15801580
return Redirect(path="/admin/login", status_code=HTTP_303_SEE_OTHER)
15811581

1582+
# Get connected user to filter by user_id
1583+
connected_user_stmt = select(User).where(User.is_active == True).limit(1) # noqa: E712
1584+
connected_user_result = await session.execute(connected_user_stmt)
1585+
connected_user = connected_user_result.scalar_one_or_none()
1586+
15821587
since_date = date.today() - timedelta(days=days)
1583-
stmt = select(Sleep).where(Sleep.date >= since_date).order_by(Sleep.date.asc())
1588+
stmt = select(Sleep).where(Sleep.date >= since_date)
1589+
if connected_user:
1590+
stmt = stmt.where(Sleep.user_id == connected_user.polar_user_id)
1591+
stmt = stmt.order_by(Sleep.date.asc())
15841592
result = await session.execute(stmt)
15851593
sleep_data = result.scalars().all()
15861594

@@ -1615,12 +1623,20 @@ async def export_activity_csv(
16151623
session: AsyncSession,
16161624
days: int = 30,
16171625
) -> Response[bytes] | Redirect:
1618-
"""Export activity data as CSV."""
1626+
"""Export activity data as CSV for the connected user."""
16191627
if not is_authenticated(request):
16201628
return Redirect(path="/admin/login", status_code=HTTP_303_SEE_OTHER)
16211629

1630+
# Get connected user to filter by user_id
1631+
connected_user_stmt = select(User).where(User.is_active == True).limit(1) # noqa: E712
1632+
connected_user_result = await session.execute(connected_user_stmt)
1633+
connected_user = connected_user_result.scalar_one_or_none()
1634+
16221635
since_date = date.today() - timedelta(days=days)
1623-
stmt = select(Activity).where(Activity.date >= since_date).order_by(Activity.date.asc())
1636+
stmt = select(Activity).where(Activity.date >= since_date)
1637+
if connected_user:
1638+
stmt = stmt.where(Activity.user_id == connected_user.polar_user_id)
1639+
stmt = stmt.order_by(Activity.date.asc())
16241640
result = await session.execute(stmt)
16251641
activity_data = result.scalars().all()
16261642

@@ -1655,16 +1671,20 @@ async def export_recharge_csv(
16551671
session: AsyncSession,
16561672
days: int = 30,
16571673
) -> Response[bytes] | Redirect:
1658-
"""Export recharge/HRV data as CSV."""
1674+
"""Export recharge/HRV data as CSV for the connected user."""
16591675
if not is_authenticated(request):
16601676
return Redirect(path="/admin/login", status_code=HTTP_303_SEE_OTHER)
16611677

1678+
# Get connected user to filter by user_id
1679+
connected_user_stmt = select(User).where(User.is_active == True).limit(1) # noqa: E712
1680+
connected_user_result = await session.execute(connected_user_stmt)
1681+
connected_user = connected_user_result.scalar_one_or_none()
1682+
16621683
since_date = date.today() - timedelta(days=days)
1663-
stmt = (
1664-
select(NightlyRecharge)
1665-
.where(NightlyRecharge.date >= since_date)
1666-
.order_by(NightlyRecharge.date.asc())
1667-
)
1684+
stmt = select(NightlyRecharge).where(NightlyRecharge.date >= since_date)
1685+
if connected_user:
1686+
stmt = stmt.where(NightlyRecharge.user_id == connected_user.polar_user_id)
1687+
stmt = stmt.order_by(NightlyRecharge.date.asc())
16681688
result = await session.execute(stmt)
16691689
recharge_data = result.scalars().all()
16701690

@@ -1697,12 +1717,20 @@ async def export_cardio_load_csv(
16971717
session: AsyncSession,
16981718
days: int = 30,
16991719
) -> Response[bytes] | Redirect:
1700-
"""Export cardio load data as CSV."""
1720+
"""Export cardio load data as CSV for the connected user."""
17011721
if not is_authenticated(request):
17021722
return Redirect(path="/admin/login", status_code=HTTP_303_SEE_OTHER)
17031723

1724+
# Get connected user to filter by user_id
1725+
connected_user_stmt = select(User).where(User.is_active == True).limit(1) # noqa: E712
1726+
connected_user_result = await session.execute(connected_user_stmt)
1727+
connected_user = connected_user_result.scalar_one_or_none()
1728+
17041729
since_date = date.today() - timedelta(days=days)
1705-
stmt = select(CardioLoad).where(CardioLoad.date >= since_date).order_by(CardioLoad.date.asc())
1730+
stmt = select(CardioLoad).where(CardioLoad.date >= since_date)
1731+
if connected_user:
1732+
stmt = stmt.where(CardioLoad.user_id == connected_user.polar_user_id)
1733+
stmt = stmt.order_by(CardioLoad.date.asc())
17061734
result = await session.execute(stmt)
17071735
cardio_data = result.scalars().all()
17081736

src/polar_flow_server/api/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
from polar_flow_server.api.baselines import baselines_router
66
from polar_flow_server.api.data import data_router
7+
from polar_flow_server.api.export import export_router
78
from polar_flow_server.api.health import health_router
89
from polar_flow_server.api.insights import insights_router
910
from polar_flow_server.api.keys import keys_router, oauth_router
@@ -21,6 +22,7 @@
2122
patterns_router, # Pattern detection and anomalies
2223
insights_router, # Unified insights API
2324
keys_router, # Key management (regenerate, revoke, status)
25+
export_router, # CSV data export
2426
]
2527

2628
api_v1_router = Router(path="/api/v1", route_handlers=_v1_routers)

0 commit comments

Comments
 (0)