{"openapi": "3.1.0", "info": {"title": "fr24 web — jonli reyslar API", "version": "3.0.0", "description": "Standalone Flightradar24 live map + JSON API. A single background poller keeps one in-memory world snapshot; every `/api/*` read is served from it and never touches FR24.\n\nThe `/api/track` and `/api/fleet/*` endpoints need `FR24_DATABASE_URL` (PostgreSQL). By default only non-passenger classes get a position history (`FR24_TRACK_CLASSES`, current: cargo,government,helicopter,military)."}, "servers": [{"url": "/"}], "tags": [{"name": "live", "description": "in-memory world snapshot"}, {"name": "history", "description": "PostgreSQL — legs, tracks, fleet"}, {"name": "ops", "description": "health, self-test, metadata"}], "paths": {"/api/flights": {"get": {"tags": ["live"], "summary": "Flights inside a bounding box", "parameters": [{"name": "bbox", "in": "query", "required": false, "description": "south,north,west,east in degrees. Clamped to ±85 / ±180. west > east crosses the antimeridian.", "schema": {"type": "string", "default": "42,52,-8,10"}}, {"name": "limit", "in": "query", "schema": {"type": "integer", "default": 2000}}, {"name": "class", "in": "query", "schema": {"type": "string", "enum": ["cargo", "government", "helicopter", "military", "other", "passenger"]}}, {"name": "ids", "in": "query", "description": "comma-separated flight ids (max 20) always included, ignoring bbox and limit", "schema": {"type": "string"}}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"type": "object"}}}}, "400": {"description": "bad request / unavailable", "content": {"application/json": {"schema": {"type": "object", "properties": {"error": {"type": "string"}}}}}}}}}, "/api/airports": {"get": {"tags": ["live"], "summary": "Airports in a bbox, or by code list", "description": "`bbox` = viewport. `codes` = up to 12 comma-separated IATA/ICAO codes (used by the route line); returns `[lat,lon,IATA,name,size,cc,ICAO]`.", "parameters": [{"name": "bbox", "in": "query", "required": false, "description": "south,north,west,east in degrees. Clamped to ±85 / ±180. west > east crosses the antimeridian.", "schema": {"type": "string", "default": "42,52,-8,10"}}, {"name": "codes", "in": "query", "schema": {"type": "string"}, "example": "TAS,IST"}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"type": "object"}}}}, "400": {"description": "bad request / unavailable", "content": {"application/json": {"schema": {"type": "object", "properties": {"error": {"type": "string"}}}}}}}}}, "/api/search": {"get": {"tags": ["live"], "summary": "Search the live snapshot", "parameters": [{"name": "q", "in": "query", "schema": {"type": "string"}, "example": "RCH"}, {"name": "field", "in": "query", "schema": {"type": "string", "enum": ["any", "callsign", "reg", "type", "operator", "route"], "default": "any"}}, {"name": "class", "in": "query", "schema": {"type": "string", "enum": ["cargo", "government", "helicopter", "military", "other", "passenger"]}}, {"name": "limit", "in": "query", "schema": {"type": "integer", "default": 120, "maximum": 400}}, {"name": "bbox", "in": "query", "schema": {"type": "string"}}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"type": "object"}}}}}}}, "/api/track": {"get": {"tags": ["history"], "summary": "24-hour position timeline for a leg or an airframe", "description": "Pass **id** (one FR24 flight leg) or **reg** (one airframe — all its legs merged).", "parameters": [{"name": "id", "in": "query", "schema": {"type": "integer"}, "description": "FR24 flight leg id (from /api/flights)"}, {"name": "reg", "in": "query", "schema": {"type": "string"}, "example": "RA-96022"}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"type": "object"}}}}, "400": {"description": "bad request / unavailable", "content": {"application/json": {"schema": {"type": "object", "properties": {"error": {"type": "string"}}}}}}, "503": {"description": "bad request / unavailable", "content": {"application/json": {"schema": {"type": "object", "properties": {"error": {"type": "string"}}}}}}}}}, "/api/fleet/classes": {"get": {"tags": ["history"], "summary": "Recorded classes with airframe / point counts", "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"type": "object"}}}}, "503": {"description": "bad request / unavailable", "content": {"application/json": {"schema": {"type": "object", "properties": {"error": {"type": "string"}}}}}}}}}, "/api/fleet/aircraft": {"get": {"tags": ["history"], "summary": "Airframes in one class that have a track (last 24 h)", "parameters": [{"name": "class", "in": "query", "required": true, "schema": {"type": "string", "enum": ["cargo", "government", "helicopter", "military", "other", "passenger"]}, "example": "military"}, {"name": "limit", "in": "query", "schema": {"type": "integer", "default": 400, "maximum": 2000}}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"type": "object"}}}}, "400": {"description": "bad request / unavailable", "content": {"application/json": {"schema": {"type": "object", "properties": {"error": {"type": "string"}}}}}}, "503": {"description": "bad request / unavailable", "content": {"application/json": {"schema": {"type": "object", "properties": {"error": {"type": "string"}}}}}}}}}, "/api/log": {"get": {"tags": ["history"], "summary": "flight_log rows (one per leg)", "parameters": [{"name": "q", "in": "query", "schema": {"type": "string"}}, {"name": "field", "in": "query", "schema": {"type": "string", "enum": ["any", "code", "reg", "type", "model", "route", "operator"], "default": "any"}}, {"name": "class", "in": "query", "schema": {"type": "string", "enum": ["cargo", "government", "helicopter", "military", "other", "passenger"]}}, {"name": "order", "in": "query", "schema": {"type": "string", "enum": ["last_seen", "first_seen"], "default": "last_seen"}}, {"name": "limit", "in": "query", "schema": {"type": "integer", "default": 100, "maximum": 1000}}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"type": "object"}}}}, "503": {"description": "bad request / unavailable", "content": {"application/json": {"schema": {"type": "object", "properties": {"error": {"type": "string"}}}}}}}}}, "/api/meta": {"get": {"tags": ["ops"], "summary": "Poller + DB + tracking status", "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"type": "object"}}}}}}}, "/api/selftest": {"get": {"tags": ["ops"], "summary": "18 internal checks (no upstream call)", "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"type": "object"}}}}, "503": {"description": "one or more checks failed"}}}}, "/healthz": {"get": {"tags": ["ops"], "summary": "Liveness probe", "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"type": "object"}}}}}}}, "/metrics": {"get": {"tags": ["ops"], "summary": "Prometheus metrics", "description": "Counters this process already keeps: poller, HTTP, database and tracking. Reads memory only — unlike `/api/meta` it never queries PostgreSQL, so it is safe to scrape on a short interval. Deliberately outside `/api/`, so a scraper does not share the per-IP token bucket with browsers.\n\nAlert on `fr24_snapshot_age_seconds` (a stalled poller), `fr24_db_queue_depth` (a writer falling behind) and `fr24_poller_recent_error_ratio` — the last is windowed, so a recovered outage clears instead of staying high forever.", "responses": {"200": {"description": "Prometheus text exposition format", "content": {"text/plain": {"schema": {"type": "string"}}}}}}}, "/api/snapshot": {"get": {"tags": ["ops"], "summary": "Full world snapshot (replicas only)", "description": "The entire in-memory snapshot, uncapped and without the sub-sampling `/api/flights` applies — this is what a replica (`FR24_POLLER=0`) follows so that N instances add zero Flightradar24 load.\n\n**Closed unless `FR24_REPLICA_TOKEN` is set**, in which case it returns `404` — it is both a bandwidth amplifier and the whole dataset in one request. With a token configured, send `Authorization: Bearer <token>`; the comparison is constant-time.", "security": [{"replicaToken": []}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"type": "object"}}}}, "401": {"description": "bad request / unavailable", "content": {"application/json": {"schema": {"type": "object", "properties": {"error": {"type": "string"}}}}}}, "404": {"description": "replica feed disabled (no FR24_REPLICA_TOKEN)"}}}}}, "components": {"securitySchemes": {"replicaToken": {"type": "http", "scheme": "bearer", "description": "Value of FR24_REPLICA_TOKEN on the leader."}}}}