Skip to content

REST API reference

The full API surface — every endpoint, every field, every status code — lives on your running TravStats instance:

  • Swagger UIhttps://your-instance/api/v1/docs
  • OpenAPI 3.0 JSONhttps://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.

https://<your-instance>/api/v1

The 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.

Two paths, same credentials:

  • Cookie sessionPOST /auth/login with username + password. The response sets an HttpOnly Secure cookie used on subsequent requests. Used by the web UI; rarely useful for scripts.
  • Personal Access TokenAuthorization: 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.

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.

CodeMeaning
200 OKRead or update succeeded
201 CreatedNew resource created
400 Bad RequestValidation failed (always returns Zod issue list in details)
401 UnauthorizedMissing or invalid auth
403 ForbiddenAuthenticated but lacks required scope (e.g. read token tried to mutate)
404 Not FoundResource doesn’t exist or you don’t own it
409 ConflictDuplicate detection — same flight already exists for that user
429 Too Many RequestsRate limit hit (response includes Retry-After)
500 Internal Server ErrorBug in TravStats — please file an issue with the request that triggered it

GET /flights and similar list endpoints accept:

  • ?limit=<n> — default 100, max 10000
  • ?offset=<n> — default 0
  • ?orderBy=<field> — default departureTime desc

Total count comes back in the response header X-Total-Count. No cursor pagination — TravStats datasets are small enough that offset-based works fine.

GET /api/v1/flights?limit=100&orderBy=-departureTime
Authorization: Bearer ts_pat_…

Returns an array of flight objects. Filter via query params:

ParamExampleEffect
from?from=2026-01-01Flights on or after the date
to?to=2026-12-31Flights on or before the date
airline?airline=LHSingle airline IATA
tag?tag=businessTagged with this label
country?country=DEEither 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/flights
Authorization: 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.

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 /api/v1/flights/{id}

Cascade-deletes the booking row, achievement entries triggered by this flight, and any pending_flight_updates for it.

GET /api/v1/flights/{id}

Returns the flight object. 404 if it doesn’t exist or isn’t yours.

GET /api/v1/flights/geo

Returns 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.

GET /api/v1/trips
POST /api/v1/trips
GET /api/v1/trips/{id}
PUT /api/v1/trips/{id}
DELETE /api/v1/trips/{id}
GET /api/v1/trips/bookings
POST /api/v1/trips/bookings
DELETE /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.

All under /api/v1/stats/*, all GET-only:

EndpointReturns
/stats/summaryTotal flights, distance, time, countries — the dashboard hero numbers
/stats/routesTop routes by frequency, formatted for the dashboard table
/stats/airlinesTop airlines + per-airline distance/duration
/stats/countriesVisited countries with flight counts
/stats/seatsSeat preference distribution (window/aisle/middle)
/stats/businessDistance / hours / flights tagged business
/stats/funQuirky stats — fastest turnaround, longest flight, etc.
/stats/uniqueOne-of-a-kind milestones

All accept the same query-param filters as /flights/list (from, to, airline, tag, country).

GET /api/v1/airports/search?q=fra
GET /api/v1/airports/FRA (by IATA or ICAO)
GET /api/v1/airports/coords/nearest?lat=50&lon=8&radius=50

The search endpoint supports IATA, ICAO, name, city — autocomplete behavior in the UI.

POST /api/v1/parse-email
Authorization: Bearer ts_pat_…
Content-Type: application/json
{
"emailContent": "From: [email protected]\nSubject: Buchungsbestätigung\n\n",
"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).

POST /api/v1/parse-boardingpass
Authorization: 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).

GET /api/v1/achievements

Returns 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/login
POST /api/v1/auth/logout
POST /api/v1/auth/change-password

Cookie-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).

GET /health

Returns 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.

GET /api/v1/version

Returns 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.

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.

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.