API Reference¶
Complete reference for the Tempo REST API.
Base URL¶
- Development:
http://localhost:5001 - Production: Configured per deployment
Authentication¶
Most endpoints require authentication. Tempo accepts Authorization: Bearer in two forms:
- JWT — Usually issued after login and sent automatically as the httpOnly cookie
authToken. The same JWT can be sent as a Bearer token when calling the API directly (e.g.curl). - API key — Admin-issued secret for machine clients (prefix
tmp_). Use for read-only automation and the planned CLI: set something likeTEMPO_API_KEYand sendAuthorization: Bearer <key>. The server distinguishes keys from JWTs by thetmp_prefix.
API keys work on normal protected routes (for example GET /auth/me, GET /workouts, stats, settings, shoes). Creating, listing, and revoking API keys requires a full interactive JWT session (POST/GET/DELETE /auth/api-keys); those routes do not accept an API key.
API keys (CLI and automation)¶
Create (session required — log in first so the authToken cookie is set, or use a cookie jar with POST /auth/login):
POST /auth/api-keys
Content-Type: application/json
Cookie: authToken=…
{
"label": "optional label, max 200 chars"
}
Response 201: includes id, label, keyPrefix, createdAt, and key (full secret) once. Store it immediately; later listing never returns the secret.
List (session only):
Returns metadata per key: id, label, keyPrefix, createdAt, revokedAt.
Revoke (session only):
Example: cookie jar (same pattern as the main README):
BASE=http://localhost:5001
curl -sS -c tempo-cookies.txt -X POST "$BASE/auth/login" \
-H 'Content-Type: application/json' \
-d '{"username":"your-user","password":"your-password","rememberMe":true}'
curl -sS -b tempo-cookies.txt -X POST "$BASE/auth/api-keys" \
-H 'Content-Type: application/json' \
-d '{"label":"cli"}'
Example: call GET /auth/me with an API key:
export TEMPO_API_KEY='tmp_…'
curl -sS -H "Authorization: Bearer $TEMPO_API_KEY" http://localhost:5001/auth/me
Do not log API keys or commit them to version control. Rotate by revoking the key and creating a new one.
Register¶
Register the first user (only available when no users exist):
POST /auth/register
Content-Type: application/json
{
"username": "your-username",
"password": "your-password",
"confirmPassword": "your-password"
}
New passwords must be 16–64 characters, with UTF-8 encoding at most 72 bytes. The API rejects common passwords, passwords that contain the username (when the username is at least 3 characters), and passwords with any character repeated five or more times consecutively. There is no mandatory character-class complexity.
Login¶
Authenticate and receive JWT token:
POST /auth/login
Content-Type: application/json
{
"username": "your-username",
"password": "your-password",
"rememberMe": false
}
Request Body:
- username (string, required) - Your username
- password (string, required) - Your password
- rememberMe (boolean, optional) - If true, token expires in 30 days (default: 7 days)
The JWT token is stored in an httpOnly cookie with expiration based on the rememberMe parameter.
Get Current User¶
Requires authentication via JWT (cookie or Bearer) or a valid API key (Authorization: Bearer tmp_…).
Logout¶
Clears the authentication cookie.
Check Registration Availability¶
Returns whether registration is currently available.
Workouts¶
Import Workout¶
Import a single workout file (GPX, FIT, or CSV):
Bulk Import¶
Import multiple workouts from a Strava export ZIP:
Supports files up to 500MB.
Export All Data¶
Export all user data in a portable ZIP format:
Authentication: Required
Response:
- Content-Type: application/zip
- Content-Disposition: attachment; filename="tempo-export-{timestamp}.zip"
- Body: ZIP file stream
The export includes: - All workouts with complete data (stats, metadata, JSONB fields) - Workout routes (GeoJSON) - Workout splits and time series data - Media files (photos and videos) as binary files - Raw workout files (GPX, FIT, CSV) as binary files - Shoes with GUIDs and relationships - User settings (heart rate zones, unit preferences, default shoe) - Best efforts
Export Format Structure:
tempo-export-{timestamp}.zip
├── manifest.json # Export metadata and version info
├── data/ # JSON files with all database records
│ ├── settings.json
│ ├── shoes.json
│ ├── workouts.json
│ ├── routes.json
│ ├── splits.json
│ ├── time-series.json
│ ├── media-metadata.json
│ └── best-efforts.json
├── workouts/ # Binary files organized by workout
│ ├── {workoutId}/
│ │ ├── raw/ # Original workout files
│ │ └── media/ # Photos and videos
│ │ └── {mediaId}/
│ │ └── {filename}
└── README.txt # Export format documentation
Manifest File (manifest.json):
{
"version": "1.0.0",
"tempoVersion": "2.0.0",
"exportDate": "2024-01-15T10:30:00Z",
"exportedBy": "username",
"statistics": {
"workouts": 150,
"shoes": 5,
"mediaFiles": 45,
"totalSizeBytes": 1234567890,
"settings": 1,
"routes": 150,
"splits": 1500,
"timeSeries": 45000,
"bestEfforts": 10
},
"dataFormat": {
"settings": "data/settings.json",
"shoes": "data/shoes.json",
"workouts": "data/workouts.json",
"routes": "data/routes.json",
"splits": "data/splits.json",
"timeSeries": "data/time-series.json",
"mediaMetadata": "data/media-metadata.json",
"bestEfforts": "data/best-efforts.json"
}
}
Notes: - Export may take several minutes for large datasets - Missing media files are logged as warnings but don't fail the export - GUIDs are preserved to maintain relationships - Export format version is 1.0.0 (compatible with future import feature)
List Workouts¶
Get all workouts with filtering and pagination:
Query parameters:
- startDate - Filter by start date (ISO 8601)
- endDate - Filter by end date (ISO 8601)
- page - Page number (default: 1)
- pageSize - Items per page (default: 20)
Get Workout¶
Get detailed workout information:
Update Workout¶
Update workout details (e.g., activity name, shoe assignment):
PATCH /workouts/{id}
Content-Type: application/json
{
"activityName": "New Activity Name",
"shoeId": "guid-here"
}
Fields:
- activityName (string, optional) - New activity name
- shoeId (Guid, optional, nullable) - Shoe ID to assign to this workout. Set to null to remove shoe assignment.
Delete Workout¶
Permanently deletes the workout and all associated data.
Crop Workout¶
Remove time from the start and/or end:
POST /workouts/{id}/crop
Content-Type: application/json
{
"removeFromStartSeconds": 60,
"removeFromEndSeconds": 30
}
Recalculate Relative Effort (Single Workout)¶
Recalculate Splits (Single Workout)¶
Get Recalculation Count (Relative Effort)¶
Get count of workouts eligible for relative effort recalculation:
Recalculate All Relative Effort¶
Recalculate relative effort for all qualifying workouts:
Get Recalculation Count (Splits)¶
Get count of workouts eligible for split recalculation:
Recalculate All Splits¶
Recalculate splits for all workouts:
Get Similar Routes¶
Find workouts with similar routes to a given workout:
Query Parameters:
- maxResults (int, optional) - Maximum number of results to return (default: 10)
- maxYears (int, optional) - Maximum number of years to look back (default: 2)
Returns a list of similar route matches with:
- workoutId - ID of the similar workout
- similarityScore - Route similarity score (0-100%)
- distanceDifferenceM - Distance difference in meters
- timeDifferenceS - Time difference in seconds
- paceDifferenceS - Pace difference in seconds per km
Statistics¶
Weekly Statistics¶
Yearly Statistics¶
Relative Effort Statistics¶
Combined Yearly and Weekly Stats¶
Available Periods¶
Available Years¶
Best Efforts¶
Get your fastest times for standard distances:
Returns your best effort times for all supported distances (400m, 1/2 mile, 1K, 1 mile, 2 mile, 5K, 10K, 15K, 10 mile, 20K, Half-Marathon, 30K, Marathon). Best efforts are calculated from any segment within any workout, not just workouts of that exact distance.
Response format:
{
"distances": [
{
"distance": "5K",
"distanceM": 5000,
"timeS": 1200,
"workoutId": "guid-here",
"workoutDate": "2025-01-15T10:30:00Z"
}
]
}
Recalculate Best Efforts¶
Recalculate all best efforts across all workouts:
Performs a full recalculation of all best efforts. This may take some time depending on the number of workouts and time series data.
Response format:
Media¶
Upload Media¶
Maximum file size: 50MB (configurable).
List Media¶
Get Media¶
Returns the media file.
Delete Media¶
Settings¶
Get Heart Rate Zones¶
Update Heart Rate Zones¶
PUT /settings/heart-rate-zones
Content-Type: application/json
{
"method": "Karvonen",
"age": 30,
"restingHeartRate": 60,
"zones": [...]
}
Update Zones with Recalculation¶
POST /settings/heart-rate-zones/update-with-recalc
Content-Type: application/json
{
"method": "Karvonen",
"age": 30,
"restingHeartRate": 60,
"recalculateAll": true
}
Get Unit Preference¶
Update Unit Preference¶
Get Default Shoe¶
Get the currently set default shoe:
Returns the default shoe object or null if no default is set.
Set Default Shoe¶
Set a shoe as the default for automatic assignment to new workouts:
Set shoeId to null to remove the default shoe.
Shoes¶
List Shoes¶
Get all shoes with calculated mileage:
Returns a list of all shoes with their current total mileage (calculated from assigned workouts plus initial mileage).
Create Shoe¶
Create a new shoe:
POST /shoes
Content-Type: application/json
{
"brand": "Nike",
"model": "Pegasus 40",
"initialMileageM": 0.0
}
Fields:
- brand (string, required, max 100 chars) - Shoe manufacturer
- model (string, required, max 100 chars) - Shoe model name
- initialMileageM (double, optional) - Initial mileage in meters when adding the shoe
Update Shoe¶
Update shoe details:
PATCH /shoes/{id}
Content-Type: application/json
{
"brand": "Nike",
"model": "Pegasus 41",
"initialMileageM": 50.0
}
All fields are optional. Only provided fields will be updated.
Delete Shoe¶
Delete a shoe:
When a shoe is deleted, all workouts assigned to that shoe will have their shoeId set to null. The workouts themselves are not deleted.
Get Shoe Mileage¶
Get calculated total mileage for a specific shoe:
Query parameters:
- unitPreference (string, optional) - "metric" or "imperial" (defaults to user's preference)
Returns the total mileage in the requested units (sum of all assigned workout distances plus initial mileage).
System¶
Version¶
Returns application version, build date, and git commit.
Health Check¶
Public endpoint (no authentication required).
Error Responses¶
All endpoints may return standard HTTP error codes:
400 Bad Request- Invalid request data401 Unauthorized- Missing, invalid, or revoked credentials on a protected route403 Forbidden- Insufficient permissions404 Not Found- Resource not found500 Internal Server Error- Server error
Many validation errors use a simple JSON shape such as:
401 authentication challenges (invalid JWT, invalid or revoked API key, or missing auth) typically return:
Other endpoints may include extra fields (for example details) where noted above.
OpenAPI contract¶
The canonical machine-readable API document is openapi.json in this repository (on the develop integration branch). Use it for client generation and to stay aligned with the read-only CLI.
Interactive Documentation¶
In development mode, interactive API documentation is available at /swagger. This provides:
- Complete endpoint documentation
- Request/response examples
- Interactive testing interface
- XML documentation comments
API Testing¶
A Bruno API testing collection is available in api/bruno/Tempo.Api/ with test requests for all endpoints.