REST API reference
The full API surface — every endpoint, every field, every status code — lives on your running TravStats instance:
- Swagger UI —
https://your-instance/api/v1/docs - OpenAPI 3.0 JSON —
https://your-instance/api/v1/openapi.json
This page covers the conventions and gotchas that cost time when you don’t know them. For a complete reference, browse the Swagger UI.
Conventions
Section titled “Conventions”Base URL
Section titled “Base URL”https://<your-instance>/api/v1The version prefix is mandatory. Future v2 will live at
/api/v2 and v1 will keep working alongside it for at least one
major release cycle.
Authentication
Section titled “Authentication”Two paths, same credentials:
- Cookie session —
POST /auth/loginwith username + password. The response sets anHttpOnlySecurecookie used on subsequent requests. Used by the web UI; rarely useful for scripts. - Personal Access Token —
Authorization: Bearer ts_pat_…header on every request. The recommended path for everything programmatic. See Personal Access Tokens.
Both paths return 401 Unauthorized for missing / invalid /
expired credentials.
Response envelope
Section titled “Response envelope”Most endpoints return either a JSON object directly or an array
directly — no wrapping {success, data, error} envelope. This keeps
client code simple:
const flights = await fetch(`${BASE}/flights`, { headers }).then(r => r.json());flights.forEach(f => console.log(f.flightNumber));Errors come back with a non-2xx status and a JSON body:
{ "error": "Validation failed", "details": [ { "path": ["departureLocal"], "message": "Invalid date format" } ]}The details array (Zod issue list) is present on validation
errors only.
Status codes
Section titled “Status codes”| Code | Meaning |
|---|---|
200 OK | Read or update succeeded |
201 Created | New resource created |
400 Bad Request | Validation failed (always returns Zod issue list in details) |
401 Unauthorized | Missing or invalid auth |
403 Forbidden | Authenticated but lacks required scope (e.g. read token tried to mutate) |
404 Not Found | Resource doesn’t exist or you don’t own it |
409 Conflict | Duplicate detection — same flight already exists for that user |
429 Too Many Requests | Rate limit hit (response includes Retry-After) |
500 Internal Server Error | Bug in TravStats — please file an issue with the request that triggered it |
Pagination
Section titled “Pagination”GET /flights and similar list endpoints accept:
?limit=<n>— default 100, max 10000?offset=<n>— default 0?orderBy=<field>— defaultdepartureTimedesc
Total count comes back in the response header X-Total-Count. No
cursor pagination — TravStats datasets are small enough that
offset-based works fine.
Flights
Section titled “Flights”List flights
Section titled “List flights”GET /api/v1/flights?limit=100&orderBy=-departureTimeAuthorization: Bearer ts_pat_…Returns an array of flight objects. Filter via query params:
| Param | Example | Effect |
|---|---|---|
from | ?from=2026-01-01 | Flights on or after the date |
to | ?to=2026-12-31 | Flights on or before the date |
airline | ?airline=LH | Single airline IATA |
tag | ?tag=business | Tagged with this label |
country | ?country=DE | Either departure or arrival in this country |
Create a flight (the (local, timezone) contract)
Section titled “Create a flight (the (local, timezone) contract)”POST /api/v1/flightsAuthorization: Bearer ts_pat_…Content-Type: application/json
{ "departure": {"iata":"FRA","lat":50.0379,"lon":8.5622}, "arrival": {"iata":"JFK","lat":40.6413,"lon":-73.7781}, "departureLocal": "2026-05-01T08:00", "depTimezone": "Europe/Berlin", "arrivalLocal": "2026-05-01T11:00", "arrTimezone": "America/New_York", "flightNumber": "LH400", "airline": "LH", "aircraft": "A340-600", "seat": "12A", "class": "economy", "tags": ["business"]}Important: Times are given as (local datetime, IANA timezone)
pairs — not as ISO datetime strings. The server runs fromZonedTime
to derive canonical UTC and stores both. This was a behaviour change
in v1.2.0 (clients sending raw ISO times to the create endpoint now
get a 400).
Optional flags:
?merge=true— fill missing fields on an existing matching flight instead of creating a duplicate. Used by re-imports of boarding passes / emails. Match key: flight number + calendar day + matching airports. Curated fields are never overwritten.
Returns the created flight (201 Created) including the assigned
id and the canonical UTC departureTime / arrivalTime derived
from your local-time pair.
Update a flight
Section titled “Update a flight”PUT /api/v1/flights/{id}Same body shape as create (omit fields you don’t want to change).
The update endpoint also rejects raw departureTime /
arrivalTime fields — use the local + timezone pair.
Delete a flight
Section titled “Delete a flight”DELETE /api/v1/flights/{id}Cascade-deletes the booking row, achievement entries triggered by
this flight, and any pending_flight_updates for it.
Get one flight
Section titled “Get one flight”GET /api/v1/flights/{id}Returns the flight object. 404 if it doesn’t exist or isn’t
yours.
Geographic shape (for maps)
Section titled “Geographic shape (for maps)”GET /api/v1/flights/geoReturns flights as GeoJSON FeatureCollection with each flight as
a LineString from departure to arrival. Used by the dashboard map
modes; useful for piping straight into deck.gl / MapLibre / leaflet.
Trips & bookings
Section titled “Trips & bookings”GET /api/v1/tripsPOST /api/v1/tripsGET /api/v1/trips/{id}PUT /api/v1/trips/{id}DELETE /api/v1/trips/{id}
GET /api/v1/trips/bookingsPOST /api/v1/trips/bookingsDELETE /api/v1/trips/bookings/{id}Trips group flights and bookings under a higher-level theme. A trip has a name, a date range, and an array of associated flight IDs.
Statistics
Section titled “Statistics”All under /api/v1/stats/*, all GET-only:
| Endpoint | Returns |
|---|---|
/stats/summary | Total flights, distance, time, countries — the dashboard hero numbers |
/stats/routes | Top routes by frequency, formatted for the dashboard table |
/stats/airlines | Top airlines + per-airline distance/duration |
/stats/countries | Visited countries with flight counts |
/stats/seats | Seat preference distribution (window/aisle/middle) |
/stats/business | Distance / hours / flights tagged business |
/stats/fun | Quirky stats — fastest turnaround, longest flight, etc. |
/stats/unique | One-of-a-kind milestones |
All accept the same query-param filters as /flights/list (from,
to, airline, tag, country).
Airports
Section titled “Airports”GET /api/v1/airports/search?q=fraGET /api/v1/airports/FRA (by IATA or ICAO)GET /api/v1/airports/coords/nearest?lat=50&lon=8&radius=50The search endpoint supports IATA, ICAO, name, city — autocomplete behavior in the UI.
Parsers
Section titled “Parsers”Parse a booking email
Section titled “Parse a booking email”POST /api/v1/parse-emailAuthorization: Bearer ts_pat_…Content-Type: application/json
{ "subject": "Buchungsbestätigung Ihres Flugs"}Body content is limited to 10 MB. Returns parsed flights + the
parser metadata (parserUsed: template / user-template /
ollama, confidence 0–1).
Parse a boarding pass
Section titled “Parse a boarding pass”POST /api/v1/parse-boardingpassAuthorization: Bearer ts_pat_…Content-Type: application/json
{ "imageBase64": "iVBORw0KGgoAAAANSUhEUgAA…", "enrichWithApi": true}Image limited to 20 MB base64. Returns parsed flight + which vision
parser fired (provider: ollama / openai / claude /
tesseract / manual).
Achievements
Section titled “Achievements”GET /api/v1/achievementsReturns the catalogue of all 58 achievements with each one’s locked or unlocked status for the current user. Useful for building external “progress” dashboards.
POST /api/v1/auth/register (only available when registration mode = open)POST /api/v1/auth/loginPOST /api/v1/auth/logoutPOST /api/v1/auth/change-passwordCookie-based — these set the HttpOnly session cookie. PATs work
on every other endpoint.
GET / POST / PUT /api/v1/admin/* covers:
/admin/users— CRUD on user accounts, invite users/admin/settings— get/update instance settings (excluded fields: encryption keys)/admin/audit— paginated audit-log query/admin/backups— list backups, trigger manual backup, restore from one
All require an admin-scoped PAT (or a cookie session for an admin
user).
Health check
Section titled “Health check”GET /healthReturns 200 with this body once the database is reachable and migrations are applied:
{ "status": "ok", "timestamp": "2026-05-02T12:34:56.789Z"}If the app is starting up, in a migration, or the DB is unreachable,
the endpoint returns 503 and the response body is omitted (the
HTTP status alone is the signal). Used by the Docker healthcheck
and any external monitoring (UptimeKuma, Statping, healthchecks.io).
No auth required — safe to expose.
Version + update info
Section titled “Version + update info”GET /api/v1/versionReturns the current running version and (if reachable) the latest stable GitHub release for the update banner:
{ "version": "1.3.0", "buildVersion": "1.3.0", "latestAvailable": "1.3.1", "updateAvailable": true, "releaseUrl": "https://github.com/Abrechen2/TravStats/releases/tag/v1.3.1", "releaseNotes": "## What's new...", "publishedAt": "2026-05-15T10:00:00.000Z"}GitHub is hit at most once per 6 hours (in-process cache). If GitHub
is unreachable (air-gapped install), latestAvailable /
releaseUrl / releaseNotes / publishedAt are null and
updateAvailable is false. No auth required.
Suggestions (autocomplete)
Section titled “Suggestions (autocomplete)”Fast lookup endpoints used by the flight-form autocomplete widgets. Both require any authenticated session (cookie or PAT, no scope restriction).
GET /api/v1/suggestions/airlines?q=luft&limit=10[ { "name": "Lufthansa", "iata": "LH", "icao": "DLH" }, { "name": "Lufthansa Cargo", "iata": "LH", "icao": "GEC" }]GET /api/v1/suggestions/aircraft?q=a32&limit=10[ { "label": "Airbus A320-200", "icao": "A320" }, { "label": "Airbus A321neo", "icao": "A21N" }, { "label": "Airbus A330-300", "icao": "A333" }]q is the partial query; limit defaults to 10, capped at 50. Both
search the seeded airline / aircraft datasets — fast (no DB query
on the hot path), case-insensitive, accent-insensitive.
Versioning
Section titled “Versioning”The current API version is v1, frozen at the response shape
documented in this page. Breaking changes go to /api/v2/ and v1
stays available for at least one major release cycle after v2 ships.
Field additions to existing endpoints are not breaking — new fields land in v1 responses immediately.
The CHANGELOG flags any API-affecting change in each release.