# BravoEcho API v1 reference Machine-readable reference for the BravoEcho aviation data API. Written for coding agents and for people who prefer one file to a docs site. Everything here is the current contract; fields may be added, existing fields will not be renamed or removed without a version bump. Status, per product (the same on the REST API and, for agents, on the MCP server; see MCP): - Available (production): RAIM prediction, route check, trajectory and world map, and the RAIM reference endpoints. Counted for pricing. The flight briefing pack and alerts count as the services they use: one request for each part they run, each counted as its own service is (so today only their RAIM part is counted). - Beta (live, free while in beta, not counted or billed; answers may change shape and coverage may be partial; before a beta service starts counting, the organisation gets at least 7 days' notice by email): the NOTAM archive (search, route, detail, history), the weather archive (METAR, TAF, hazards, weather along a route, winds aloft along a route), the airspace a flight enters (special use airspace, Class B, C and D, TFRs; United States), airports, navaids and route strings, mass and balance and a navlog on the aircraft data you send, and alerts by webhook on a flight. Every `/v1` response says which it is in two headers: `X-Product-Status` (`ga` or `beta`) and `X-Metered` (`true` or `false`). The OpenAPI 3.1 description, https://bravoecho.ai/openapi.json, has every Available and Beta endpoint with `x-status: available | beta` on each operation. Coverage: RAIM is worldwide. METAR and TAF are worldwide (every station NOAA's Aviation Weather Center reports). NOTAMs (the FAA's domestic set and the international NOTAMs the FAA distributes), route strings (named fixes, airways and procedures come from the FAA CIFP, US only) and en-route weather advisories (G-AIRMET and CWA are US products; international SIGMETs are worldwide) are fullest in the United States for now. Server-side only: call the API from server-side code (a backend route or an edge function), never from a browser, and keep the key in a secret. The API sends no CORS headers, so a browser on another origin cannot read its answers; a key in a web page or a client bundle is a leaked key. ## Getting a key Keys come from the console at https://app.bravoecho.ai, no email needed. 1. Check whether the environment already has `BRAVOECHO_KEY`. 2. If not, the human creates an account and an organisation at https://app.bravoecho.ai/register, opens API keys, and creates one. The organisation starts on the free band (1,000 counted requests a month across all its keys; beta products are not counted). Wait for the key before writing code that calls the API. 3. Read the key from the environment or the platform's secret store (`BRAVOECHO_KEY`), in server-side code only. Never put it in source, config files, logs, a web page or a client bundle. The console also runs the same checks in a browser (RAIM prediction, route check, world map with PNG export, NOTAM search, NOTAMs en route, airspace en route, airports, aerodrome weather, weather en route, winds aloft, the flight briefing pack, a navlog, mass and balance) and manages alerts, useful for checking a call by eye. Questions: hello@bravoecho.ai. Help with an account, a key or an answer: support@bravoecho.ai. Higher limits and volume pricing: sales@bravoecho.ai. ## Base URL, auth, conventions - Base URL: `https://api.bravoecho.ai/v1` - Auth: `Authorization: Bearer $BRAVOECHO_KEY` on every request. - JSON in, JSON out. `Content-Type: application/json`. - Times: RFC 3339 in UTC with a trailing `Z`, everywhere. Never local time. - Units: latitude and longitude in decimal degrees (WGS-84), altitude in metres (the flight object also takes feet and flight levels, see Flight object), alert limits in nautical miles (`hal_nm`), protection levels in both metres and NM. Airport and navaid reference data keeps the operational units: elevations and runway dimensions in feet, COM frequencies in MHz, navaid frequencies in kHz. - Health: `GET https://api.bravoecho.ai/up` returns 200 when the API is up. Use it in a smoke test. - CORS: none. The API is for servers; see "Server-side only" above. - MCP: an agent that speaks the Model Context Protocol can use `https://api.bravoecho.ai/mcp` with the same key instead of writing a client. See MCP below. ## Errors Every error has one shape, a code and a sentence: ```json { "error": "validation", "message": "waypoint \"KSFO\": lat/lon out of range" } ``` Some add fields: `errors` (per field) on a request that fails validation, `unresolved` and `resolved_route` on `route_unresolved`, `almanac` on `almanac_untrusted`. - 400 `bad_request`: a required parameter is missing or malformed. `unknown_field`: the body has a field this endpoint does not take. - 401 `unauthenticated`: missing or invalid key. - 403 `forbidden`: the key cannot use this endpoint. `licence_required`: the organisation has used its free allowance of counted requests and has not accepted the licence agreement; an owner or admin accepts it in the console (app.bravoecho.ai/licence). Beta endpoints keep answering. - 404 `not_found`. - 409 `alert_limit`: the organisation already has 25 active alerts. - 422 `validation`: the message names the field. `route_unresolved`: a route string has a point we cannot place. - 429 `rate_limited`: back off for the number of seconds in the `Retry-After` header, then retry. `quota_exceeded`: the organisation's monthly cap of counted requests is reached; `Retry-After` runs to the start of next month. Beta endpoints keep answering. - 503 `almanac_untrusted`: the requested time is too far from the almanac to predict (more than 30 days). `upstream_unavailable`: a backing service is down. - Do not retry other 4xx responses. 5xx: retry once after a short delay, then surface the error. ## Limits and pricing - Rate limit per key: 60 requests a minute on Free, 600 (10 a second) on pay as you go. Ask for more; it costs nothing extra. - World maps (`POST /v1/raim/map`) are also limited to 10 a minute for each organisation, across its keys: one map is seconds of engine time. Past that, 429 `rate_limited` with `Retry-After`. - A request is one API call or one MCP tool call, whatever it returns. A predict call with 50 points is one request. `GET /v1/ping` is free. - The flight briefing pack counts one request for each part it runs, in that part's own service: a pack of RAIM, NOTAMs and weather is three requests (one RAIM, one NOTAMs, one weather). An alert counts the same way, only when a check finds a change and calls your webhook. Each request is counted or not as its service is. - Only Available services count. Beta requests are recorded (they show in the console's usage) but are not counted, never billed and never refused for quota. - Pricing per organisation (all its keys together), per calendar month, graduated (each band's price applies to the requests inside it): - 0 to 1,000 requests: free, every month. No card. - 1,001 to 100,000: EUR 0.010 per request. - 100,001 to 1,000,000: EUR 0.008 per request. - Over 1,000,000: enterprise agreement, write to sales@bravoecho.ai. - Plans: every organisation starts on Free, capped at the 1,000 free counted requests a month: over it, counted requests get 429 `quota_exceeded` (beta services keep answering). To go beyond, an owner or admin requests pay as you go on the console's Billing page (https://app.bravoecho.ai/billing) with the organisation's billing details; once Bravo Echo approves it there is no monthly cap, and the counted requests over the free 1,000 are invoiced at the end of each month at the prices above, paid by bank transfer. No card payments. Volume pricing: sales@bravoecho.ai. - Counted requests beyond the free allowance are covered by the licence agreement (https://bravoecho.ai/licence), accepted once per organisation in the console; until then they get 403 `licence_required`. ## RAIM prediction `POST /v1/raim/predict` Predicts GNSS RAIM availability at each point you give, for a window around each point's time, using the GPS (and optionally Galileo) almanac and the satellite outages in force (NANUs and NAGUs are ingested continuously). The computation is DO-229 fault detection (FD) and fault detection and exclusion (FDE) with a weighted, elevation and latitude dependent error model, checked against FAA RAIM SAPT daily plots from 2024 to 2026 and Eurocontrol AUGUR output. Past times: a window more than a week before the current almanac is computed against the archived GPS almanac that was in force then (CelesTrak SEM archive) and the NANU windows in force on that day, so a past day reproduces what a receiver saw. The response says so in `warnings` ("historical request: computed against the archived almanac ..."), `almanac.version` names it, and `degraded` stays false. Galileo has no archive and stays on the live almanac. The future is never archived: more than a week past the live almanac's toa the age warning applies, and more than 30 days is refused with 503. FD is available when the horizontal protection level (HPL) is at or under the alert limit (HAL) with at least 5 satellites; FDE additionally requires at least 6 and that every single-satellite-removed subset still detects. Request: ```json { "window_minutes": 60, "step_seconds": 60, "mask_deg": 5, "fde": true, "baro": false, "detail": false, "points": [ { "id": "EGLL", "lat": 51.4775, "lon": -0.4614, "alt_m": 25, "time": "2026-09-18T06:00:00Z", "phase": "enroute" }, { "id": "EHAM", "lat": 52.3086, "lon": 4.7639, "alt_m": 0, "time": "2026-09-18T07:15:00Z", "phase": "approach" }, { "id": "WPT1", "lat": 52.0, "lon": 2.5, "alt_m": 11000, "time": "2026-09-18T06:40:00Z", "hal_nm": 2.0 } ] } ``` Fields: - `points` (required, 1 to 500): each point needs `lat`, `lon`, `time`, and either `phase` or `hal_nm`. `id` is echoed back; use ICAO codes or your own waypoint names. `alt_m` defaults to 0. - `phase`: one of `oceanic` (HAL 4.0 NM), `enroute` (2.0 NM), `terminal` (1.0 NM), `approach` (0.3 NM; aliases `npa`, `lnav`), or an RNP value, `rnp4`, `rnp2`, `rnp1`, `rnp0.5`, `rnp0.3`, `rnp0.2`, `rnp0.15`, `rnp0.1`, whose HAL is the RNP value in NM (`RNP 0.3` and `rnp-0.3` work too). `hal_nm` (above 0, up to 20) instead of a phase sets the limit directly. `GET /v1/raim/phases` lists them. - `window_minutes` (default 60): each side of the point's `time`. `step_seconds` (default 60, min 30, max 3600). - `mask_deg` (default 5): elevation mask. `fde` (default true): false means FD only. `baro` (default false): add an altitude pseudo-measurement. - `detail` (default false): true adds per-epoch arrays to each point. - `outages` (optional): extra satellite outages to apply, `[{ "prn": 1, "from": "...", "to": "...", "source": "...", "system": "GPS" }]`. Current NANU (GPS) and NAGU (Galileo) outages are applied automatically and listed in `outages_applied`. - `profile` (optional): a named parameter set that reproduces a reference service. `do229` (default): the RTCA DO-229 baseline budget, FDE, every outage counts. `faa-c129`: matches the FAA RAIM SAPT / TSO-C129 prediction (FD only, 5 deg, 1 min step, error model calibrated on the FAA daily plot, outages under 5 continuous minutes ignored). `augur`: matches Eurocontrol AUGUR (FD, 5 deg, 1 min step, AUGUR's own error and baro models as read from its output, and its 5 minute rule, which measures an outage from its first to its last unavailable minute as AUGUR prints it). `faa-c129-sa`: the TSO-C129 SA-on receiver class. Any explicit field overrides the profile; the response `params` echoes the settings applied. `GET /v1/raim/profiles` lists them. - `constellations` (optional, default `["gps"]`): `["gps", "galileo"]` adds Galileo from the GSC almanac; the solution then carries the GPS-Galileo time offset as a fifth unknown, so FD needs 6 satellites and FDE 7. `["galileo"]` alone is a single-system solve. - `min_outage_minutes` (optional): outages shorter than this are ignored for the dispatch verdict; the FAA and AUGUR profiles set 5. - `sa` (optional, default false): the TSO-C129 Selective Availability receiver class, a much larger error budget; AUGUR offers the same switch. Unknown fields are rejected with 400 `unknown_field`. Response: ```json { "almanac": { "version": "2436-61440", "toa": "2026-09-13T17:03:42Z", "source": "navcen", "satellites": 32, "healthy": 31 }, "params": { "window_minutes": 60, "step_seconds": 60, "mask_deg": 5, "fde": true, "baro": false, "min_sats_fd": 5, "min_sats_fde": 6, "profile": "do229", "min_outage_minutes": 0, "constellations": ["gps"] }, "outages_applied": [ { "prn": 1, "from": "2026-09-18T03:00:00Z", "to": "2026-09-18T15:00:00Z", "source": "NANU 2026073 FCSTDV" } ], "dispatch": { "ok": false, "min_outage_minutes": 0, "longest_outage_s": 840, "short_outages": 0 }, "points": [ { "id": "EHAM", "hal_nm": 0.3, "hal_m": 555.6, "start": "2026-09-18T06:15:00Z", "end": "2026-09-18T08:15:00Z", "available_now": false, "epochs": 121, "epochs_available": 107, "availability_pct": 88.4, "outages": [ { "from": "2026-09-18T07:10:00Z", "to": "2026-09-18T07:24:00Z", "duration_s": 840, "reason": "hpl_exceeds_hal", "max_hpl_m": 612.0 } ], "dispatch": { "ok": false, "min_outage_minutes": 0, "longest_outage_s": 840, "short_outages": 0 }, "worst": { "hpl_nm": 0.33, "hpl_m": 612.0, "at": "2026-09-18T07:17:00Z", "sats": 6 }, "min_sats": 6 } ], "warnings": [], "degraded": false, "compute_ms": 4.3, "generated_at": "2026-09-18T06:02:11Z" } ``` Reading it: - `outages_applied` lists the outage windows (NANU, NAGU and your own) that overlap the computed window; `almanac.satellites` and `almanac.healthy` count the requested constellations. `generated_at` is when the answer was computed (UTC); every RAIM answer carries it. - `available_now` is availability at the epoch nearest the point's `time`. - `outages[]` are merged intervals where RAIM was not available. `reason` is `hpl_exceeds_hal` or `insufficient_satellites`. `to` is the last unavailable epoch plus one step. - `worst` is the largest HPL in the window and when it occurred. `hpl_m` is `null` when RAIM is impossible (too few satellites or degenerate geometry). - With `detail: true`, each point also carries `epochs_detail: { "t": [unix seconds], "hpl_m": [], "sats": [], "avail": [] }`. - `dispatch` is the verdict under the profile's outage rule: `ok` is true when no outage of `min_outage_minutes` or longer remains; `short_outages` counts the ones ignored. The top-level `dispatch` covers every point. - `degraded: true` (also the `X-Raim-Degraded: true` header) means a warning affects trust in the numbers: almanac more than 7 days from the requested time, or the NANU/NAGU set stale. Show `warnings` to the user; never hide them. Requests more than 30 days from the almanac are refused with 503 rather than guessed. Example call: ``` curl -s https://api.bravoecho.ai/v1/raim/predict \ -H "Authorization: Bearer $BRAVOECHO_KEY" \ -H "Content-Type: application/json" \ -d '{"points":[{"id":"EHAM","lat":52.3086,"lon":4.7639,"time":"2026-09-18T07:15:00Z","phase":"approach"}]}' ``` ## Flight object `POST /v1/raim/route`, `POST /v1/notams/route`, `POST /v1/weather/route` and `POST /v1/airspace/route` take the same flight object, so one object can go to all four. Each reads what it needs from it and ignores the rest of it. Beside it, each takes its own options, listed with the endpoint. A field that is neither is refused with 400 `unknown_field`. The flight as waypoints: ```jsonc { "waypoints": [ { "id": "KJFK", "lat": 40.6398, "lon": -73.7789, "alt_ft": 13, "codes": ["KJFK"], "phase": "terminal" }, { "id": "TOC", "lat": 41.3, "lon": -70.0, "fl": 350, "phase": "enroute" }, { "id": "4460N", "lat": 44.0, "lon": -60.0, "fl": 350, "phase": "oceanic" }, { "lat": 50.0, "lon": -40.0, "fl": 350 }, { "lat": 52.0, "lon": -20.0, "fl": 370 }, { "id": "TOD", "lat": 51.6, "lon": -3.5, "fl": 370, "phase": "enroute" }, { "id": "EGLL", "lat": 51.4706, "lon": -0.4619, "alt_ft": 83, "codes": ["EGLL"], "phase": "approach" } ], "departure": "2026-09-24T08:00:00Z", "speed_kt": 480 } ``` - `waypoints` (at least 2; the RAIM route check takes up to 500, the NOTAM and weather routes up to 200): each needs `lat` and `lon`. Optional: `id` (a name; `label` is read as `id`), `time` (RFC 3339), one of `alt_m`, `alt_ft` or `fl` (flight level: 350 for FL350), `codes` (aerodrome identifiers at the point), and the RAIM alert limit, `phase` or `hal_nm` (see RAIM prediction). - `departure`: the time of the first waypoint when it has none. `speed_kt` (up to 2000): ground speed for the waypoints without a time. The flight as filed: ```json { "route": "LDZA DCT 4448N01552E DCT LDSP", "departure": "2026-09-24T07:00:00Z", "speed_kt": 250, "cruise_fl": 150 } ``` - `route` (up to 2,000 characters), `departure`, `speed_kt`, the cruise level as one of `cruise_fl` (0 to 650) or `cruise_alt_m` (-500 to 20,000), and for RAIM `phase` (default `auto`) or `hal_nm`. - The route is resolved once, the way `GET /v1/routes/resolve` resolves it, with the AIRAC cycle in force at `departure` (the NOTAM route without a departure: at `from`, else now). Its airports are aerodromes, as if their code were in `codes`. Every answer to a route string adds `resolved_route`: `route`, `points[]`, `notes[]`, `nav_data`. A point that cannot be placed is 422 `route_unresolved` with `unresolved[]`, on all three. - `route` and `waypoints` together are 400 `bad_request` ("Give route or waypoints, not both."); neither is 400 `bad_request` too. `cruise_fl`, `cruise_alt_m`, `phase` and `hal_nm` at the top level go with `route` only: with waypoints they are 400 `unknown_field`, because each waypoint carries its own level and alert limit. What each endpoint reads: - RAIM route check: positions, times, `departure`, `speed_kt` and the alert limits. Levels are in metres, so `alt_ft` and `fl` are converted (FL x 100 ft x 0.3048: FL350 is 10,668 m). A route string: the departure and destination airports at field elevation, every other point at the cruise level (without one, at its own elevation), phases from its airports unless `phase` or `hal_nm` is given. - NOTAM route: positions, `id` and `codes`. Times only for the window (see `POST /v1/notams/route`); levels and alert limits are not used. - Weather route: positions, times, levels as given, `codes`, `departure` and `speed_kt`; the service times and levels the flight itself. A route string with a cruise level climbs from the departure airport's field elevation at 250 ft a NM to a top of climb (`TOC`), flies the cruise level, and descends on a 3 degree path (318 ft a NM) from a top of descent (`TOD`) to the destination's field elevation; `TOC` and `TOD` are added as points (they show among the fixes), and a route too short to level off gets one `TOC` where the climb meets the descent. A route string that starts or ends away from an airport is at the cruise level there. Without a cruise level no point has a level, so no hazard is left out by level (two field elevations alone would fly the whole route near the ground). Waypoints are flown as given: give your own top of climb and top of descent. The flight object is checked before anything is sent, the same way on all three. A field it does not document is 400 `unknown_field`, naming the waypoint and the field; a value that cannot be right is 422 `validation` with `errors` per field. Waypoints are counted from 0. Times are RFC 3339; numbers are JSON numbers (`"51.47"` is refused). ```json { "error": "unknown_field", "message": "Waypoint 1 (EGLL): unknown field eta. A waypoint takes id, lat, lon, time, alt_m, alt_ft, fl, codes, phase, hal_nm (label is read as id). See https://bravoecho.ai/llms-full.txt for the documented request." } ``` ```json { "error": "validation", "message": "waypoint 0 (KJFK): give one of alt_m, alt_ft and fl. (and 1 more error)", "errors": { "waypoints.0.alt_m": ["waypoint 0 (KJFK): give one of alt_m, alt_ft and fl."], "waypoints.1.lat": ["waypoint 1 (EGLL): lat must be a number from -90 to 90."] } } ``` One flight, three answers. The route string above sent to the three endpoints; each answer leads with `summary`: ``` for e in raim notams weather; do curl -s https://api.bravoecho.ai/v1/$e/route \ -H "Authorization: Bearer $BRAVOECHO_KEY" -H "Content-Type: application/json" \ -d @flight.json | jq -r .summary done ``` ``` RAIM is available along the whole route at the planned times. 1 NOTAM along the route, in force 24 Sep 07:00Z to 07:32Z: 1 crossing the corridor, 0 abeam, 0 at the route's aerodromes, 0 FIR-wide. 135 NM, 24 07:00Z to 24 07:32Z. No hazard in force meets the corridor at the aircraft's time and level. departure LDZA: METAR VFR, TAF worst VFR around 07:00Z. destination LDSP: METAR VFR, TAF worst VFR around 07:32Z. Collection has 1 gap(s) in the period: see meta.gaps. ``` ## RAIM route check `POST /v1/raim/route` A flight plan flown along great-circle legs, RAIM evaluated at the aircraft's position and time at every step. The flight is the flight object (see Flight object): waypoint times may be given, or computed from `departure` and `speed_kt`; a waypoint's level may be `alt_m`, `alt_ft` or `fl`, and is flown in metres; `label` is read as `id`, and `codes` are accepted and not used. `phase` or `hal_nm` applies from a waypoint onwards (the first must have one); a leg is flown against the smaller HAL of its two ends. Beside the flight: `window_minutes`, `detail` and the same tuning fields as predict (`profile`, `constellations`, `step_seconds`, `mask_deg`, `fde`, `baro`, `outages`, ...). ```json { "departure": "2026-09-18T08:00:00Z", "speed_kt": 450, "profile": "faa-c129", "waypoints": [ { "id": "LOWW", "lat": 48.1103, "lon": 16.5697, "alt_m": 183, "phase": "terminal" }, { "id": "TOC", "lat": 48.9, "lon": 12.5, "alt_m": 11000, "phase": "enroute" }, { "id": "EGLL", "lat": 51.4706, "lon": -0.4619, "alt_m": 25, "phase": "approach" } ] } ``` Or give the route as filed instead of `waypoints`: `route` (a string), with `phase` (default `auto`) or `hal_nm`, and the cruise level as `cruise_alt_m` or `cruise_fl` (optional; `cruise_fl: 350` flies at 10,668 m). `auto` takes the phases from the route's airports: terminal (1.0 NM) from the departure airport until the route first leaves 30 NM of it, and from the last time it comes within 30 NM of the destination; approach (0.3 NM) from the last time it comes within 7 NM of the destination; en route (2.0 NM) everywhere else. These zones exist only where the route starts or ends at an airport, and a later pass near either airport stays en route. Where a zone starts or ends inside a leg, a point is added there (`kind: "boundary"`, ids such as `KMSP+30NM`, `KSFO-30NM`, `KSFO-7NM`) carrying the looser phase, so each leg is flown against its own zone's limit; every point in `resolved_route.points` then has its `phase`. Any other `phase`, or `hal_nm`, holds for the whole route. Departure and destination airports sit at field elevation, every other point at the cruise level (without one, at its own elevation). Airports and navaids come from OurAirports (worldwide); named fixes, airways and SIDs/STARs from the FAA CIFP (US airspace, including Alaska, Hawaii and the US oceanic areas), using the AIRAC cycle in force at `departure`. An airway (`V170`, `J148`) is expanded into its fixes between the entry and the exit. `SID.TRANSITION` after the departure is flown on the SID's common route then the transition; `TRANSITION.STAR` before the destination on the transition then the common route; runway-specific legs are left out because the runway is not known, and legs without a fix (headings, vectors) are skipped. A retired revision (`MOD5`) is flown as the current one (`MOD9`) with a note. Coordinates (`5020N01000W`, `52N020W`, `4413N/09354W`, `N44.2/W93.9`, `44.2/-93.9`) are placed as given; `DCT` and speed/level groups (`N0450F350`) are dropped. An airway or procedure outside the US data is flown direct, with a note. An identifier shared by several places resolves to the one on the way. A fix we cannot place (`KOXER` outside the US) is a 422 `route_unresolved` with `unresolved[]`, never a guess. The response adds `resolved_route` with `route`, `points[]` (`token`, `id`, `kind` airport/navaid/fix/coordinate, or boundary with `phase: "auto"`, `type`, `name`, `country`, `lat`, `lon`, `elevation_ft`, `matches`, `via`: the airway or procedure a fix comes from), `notes[]`, one per simplification, and `nav_data` (`source`, `airac`, `effective_from`, `effective_to`). ```json { "route": "KMSP DWN6.ABR J158 RAP MBW MTU J148 OAL.MOD5 KSFO", "departure": "2026-09-23T18:00:00Z", "speed_kt": 450, "cruise_alt_m": 10700, "phase": "enroute", "profile": "faa-c129" } ``` Response: `dispatch` (the verdict), `summary`, `start`, `end`, `duration_s`, `distance_nm`, `availability_pct`, `outages[]` with `from`/`to` time and position, `leg`, `reason`, `max_hpl_m`, `hal_nm`; `worst` (HPL, time, position, leg); `waypoints[]` (each fix at its own time: `hal_nm`, `available`, `hpl_m`, `sats`); `legs[]` (`from`, `to`, `hal_nm`, `distance_nm`, `samples`, `epochs_available`, `outages`); plus `almanac`, `params`, `outages_applied`, `warnings`, `degraded`. `detail: true` adds `samples_detail` with `t`, `lat`, `lon`, `hpl_m`, `sats`, `avail`. `summary` is the verdict in one to three plain sentences, for example: "RAIM is available along the whole route for the planned departure 12:00Z. Every departure from 11:46Z to 12:26Z is clean (14 min early to 26 min late). Departing 11:17Z to 11:45Z loses RAIM between p1 and p6 (HAL 0.1 NM)." Show it to people as it is; use the fields for logic. ### Departure window A timed track, for example exported from a flight plan, with its own alert limit per point, checked for a departure that may slip by up to two hours either way: ```jsonc { "profile": "faa-c129", "window_minutes": 120, "waypoints": [ { "id": "KSFO", "lat": 37.6188, "lon": -122.3750, "alt_m": 4, "time": "2026-09-24T12:00:00Z", "hal_nm": 0.1 }, { "id": "CLB", "lat": 37.7400, "lon": -121.9800, "alt_m": 1800, "time": "2026-09-24T12:05:00Z", "hal_nm": 0.3 }, { "id": "TOC", "lat": 38.1500, "lon": -120.4000, "alt_m": 10700, "time": "2026-09-24T12:20:00Z", "hal_nm": 2.0 }, // ... en route points, up to 500 waypoints in all { "id": "TOD", "lat": 40.9000, "lon": -75.9000, "alt_m": 10700, "time": "2026-09-24T17:02:00Z", "hal_nm": 1.0 }, { "id": "FAF", "lat": 40.6700, "lon": -73.9000, "alt_m": 600, "time": "2026-09-24T17:17:00Z", "hal_nm": 0.1 }, { "id": "KJFK", "lat": 40.6398, "lon": -73.7789, "alt_m": 4, "time": "2026-09-24T17:20:00Z" } ] } ``` A leg takes the smaller HAL of its two ends, so put a point where a tighter limit starts (above, the leg from TOD to FAF is flown against 0.1 NM). `window_minutes` (0 to 720, default 0) is how far the departure may slip. The whole schedule is shifted by every `step_seconds` within that many minutes either side of the planned times, and each shifted flight is judged with the same outage rule as the planned one. The cost is departures times samples along the route, at most 250,000 per request: 152 points over 5 h 20 at ±120 min is 241 x 454, well inside it; a longer route or window gets a 422 saying by how much, and a larger `step_seconds` brings it back. With a window the response adds: - `window`: `planned_departure`, `planned_ok` (the same verdict as `dispatch.ok`), `minutes`, `step_s`, `departures_checked`, `clean_pct` (share of the checked departures that are clean), `slack_minutes` (`early`, `late`: how far the departure can move with every departure in between clean; equal to `minutes` when it stays clean to the edge of the window; `null` when the planned departure fails), `nearest_clean` (only when the planned departure fails: `earlier` and `later`, each `{ "departure", "offset_min" }` or `null`), `clean_departures[]` and `blocked_departures[]` (`from`, `to`, `from_offset_min`, `to_offset_min`, both ends inclusive and sampled every `step_s`; each blocked range lists `segments[]`, where those departures lose RAIM: `from_id`, `to_id`, `first_leg`, `last_leg`, the smallest `hal_nm` there, `reasons`, `max_hpl_m`, `longest_outage_s`) and `fixes_ok`. - `waypoints[].window`: that fix held at its position from its time minus the window to its time plus the window: `start`, `end`, `epochs`, `epochs_available`, `availability_pct`, `ok`, `outages[]`. This is what predict returns for the same point and window. It is stricter than the departure view, because the aircraft does not stay at a fix; `fixes_ok` is true when every fix passes. - With `detail: true`, `window_detail`: `offset_start_min`, `step_s`, `rows`, `row_ok[]` and `unavailable` runs `[row, first sample, last sample]` indexing `samples_detail`, enough to draw departure time against distance along the route. Reading it: `planned_ok` answers "is the planned flight covered", `slack_minutes` "how much delay or early departure it can take", `blocked_departures[].segments` "where the others lose RAIM", and `fixes_ok` the stricter per-point question. ## RAIM trajectory and world map `POST /v1/raim/trajectory`: one position over up to 72 h (the length of an AUGUR scenario) with satellite geometry (`passes[]` with azimuth, elevation and status per epoch) for plotting. `POST /v1/raim/map`: unavailability minutes on a lat/lon grid for a UTC day and HAL, the FAA "24-hour worldwide RAIM unavailability" product on demand (`start` at 00:00Z of the day, `hours` 24, `grid_deg` 2, `lat_min`, `lat_max`, `hal_nm`; response `outage_min[nlat*nlon]`, row-major from `lat0` northwards and `lon0` = -180 eastwards, and `coverage` with `pct_cells_100`, `max_outage_min` and `top_cells`). The map counts every unavailable minute per cell, as the FAA plot does; the profile's dispatch rule applies only when `min_outage_minutes` is sent. To match the FAA plot use `profile: "faa-c129"`, `step_seconds: 60`, `hal_nm: 0.1` or `0.3`; a past day picks up the almanac and NANUs of that day. Both take the same tuning fields as predict. Reference data: `GET /v1/raim/almanac` (GPS and Galileo almanac status, NANU and NAGU sets), `GET /v1/raim/nanu`, `GET /v1/raim/nagu`, `GET /v1/raim/phases`, `GET /v1/raim/profiles`. ## NOTAMs (beta) The NOTAM archive: every NOTAM the FAA NOTAM Management Service distributes (US domestic, FDC, military and the international feed, worldwide), collected every few minutes since 2026-08-12, kept as append-only versions. Answers describe a time and say so; nothing is hidden behind a 200. All times RFC 3339 UTC. ### `GET /v1/notams` At least one filter is required. Query parameters: - `location`: domestic or ICAO identifiers, comma separated (`KJFK,JFK,EGLL`). Either form matches. - `fir`: FIR identifiers (`EGTT,KZNY`). Matches NOTAMs affecting the FIR or addressed to it. - `bbox`: `west,south,east,north` in degrees. - `lat`, `lon`, `radius_nm`: within a radius (up to 1000 NM) of a point. - `classification`: `DOM`, `INTL`, `MIL`, `FDC`, `LMIL`, comma separated. - `category`: `runway`, `taxiway`, `aerodrome`, `lighting`, `obstacle`, `navaid`, `airspace`, `warning`, `procedure`, `service`, `other` (our grouping of the Q-code, with the FAA keyword as fallback). - `number`: exact NOTAM number (`A1045/26`, `06/048`). - `q`: substring over text, number and locations. - `from`, `to`: an in-force window. Returns NOTAMs effective at any point of it, reaching back into expired ones (bounded by when collection started). An estimated end never excludes a NOTAM. - `active`: default `true`, the FAA's live set (distributed, not necessarily in force yet; use a window for "in force now"). `false` includes cancelled and expired. - `as_of`: replay the archive as it was known at that instant. - `include`: `geometry` (the FAA's own geometry, GeoJSON), `area` (the affected area: FAA polygon, else the area read out of the text, else the radius circle; `area_source` says which, `area_km2` its size), `raw` (ICAO and domestic renderings as received). - `collapse`: default `true`. The same notice issued under two numbers (domestic `06/048` and international `A1045/26`) is one entry with `also_issued_as`. - `cursor`, `limit`: newest first, `limit` up to 500, default 100. `meta.next_cursor` continues. ``` curl -H "Authorization: Bearer $BRAVOECHO_KEY" \ "https://api.bravoecho.ai/v1/notams?location=EGLL&from=2026-09-20T06:00:00Z&to=2026-09-20T12:00:00Z&include=area" ``` Response: `notams[]` with `nms_id`, `number`, `classification`, `category`, `title` (our plain reading of the Q-code, `null` when there is nothing trustworthy to say), `unusable`, `type` (N new, R replaces, C cancels another NOTAM), `location`, `icao_location`, `affected_fir`, `is_active`, `issued_at`, `effective_start`, `effective_end`, `estimated`, `cancelled_at`, `schedule` (the raw D) field), `selection_code`, `traffic`, `purpose`, `scope`, `lower_limit`, `upper_limit`, `minimum_fl`, `maximum_fl`, `coordinates`, `radius_nm`, `text`, `faa_last_updated`, `observed_at`, `version_count`, and the included blocks. `meta`: `total`, `returned`, `truncated`, `next_cursor`, `duplicates_collapsed`, `window` (`semantic` is `active`, `in_force` or `all`), and for anything about the past `as_of` (`archive_start`, `before_archive`) and `gaps` (unrecovered collection holes before the period, with `from`, `to`, `status`). Read `meta.gaps` before trusting history: the upstream feed reaches back 24 hours, so an outage longer than that is a permanent hole and is listed. ### `GET /v1/notams/{nms_id}` Everything held about one NOTAM: the fields above plus `raw.icao`, `raw.domestic`, `raw.aixm` (as received, never reformatted), `schedule_decoded` (the D) field read into `kind`, `summary` and `entries`; display only, it never removes a NOTAM from a result), the text-derived geometry, and provenance (`source`, `first_seen_at`, `last_seen_at`). 404 when the id is not in the archive. ### `GET /v1/notams/{nms_id}/history` Every observed version, oldest first: `version_id`, `faa_last_updated`, `observed_at`, `source`, `content_hash`, the lifecycle fields, `text`, and `changed_from_previous` (which of `text`, `effective_start`, `effective_end`, `cancellation_date`, `type` changed). ### `POST /v1/notams/route` NOTAMs along a flight. The flight is the flight object (see Flight object): waypoints or a route string, with `departure` and `speed_kt`. The NOTAM service reads each point's position, name (`id`, or `label`) and `codes`. ```jsonc { "route": "LDZA DCT 4448N01552E DCT LDSP", "departure": "2026-09-24T07:00:00Z", "speed_kt": 250, "half_width_nm": 5, "terminal_radius_nm": 10, // defaults "fl_from": 100, "fl_to": 350, // optional; only a clean three-digit Q-line pair can exclude a NOTAM "classifications": [], "categories": [], "q": "", "include_area": false, "include_geometry": false, "include_raw": false, "collapse": true, "limit": 500 } ``` The window: `from` and `to` when either is given; a NOTAM in force at any point of it matches, and one of them alone is an instant. Without them, the flight's own window: from the first waypoint's time (or `departure`) to the last waypoint's time, or else to the last time known plus the rest of the route at `speed_kt` along the great circles. A flight that starts at a known time but cannot reach its arrival is 422 `validation` on `speed_kt`; one with times but no start, on `departure`. A flight with no time at all asks for the live set (distributed, not necessarily in force yet). `meta.window` says which window was used. The corridor is the great-circle line buffered sideways plus a circle around each waypoint. NOTAMs match by area, by aerodrome code (`codes` on a waypoint, and the airports of a route string), and through a FIR channel: FIR-wide notices addressed to the FIRs the corridor touched. Each NOTAM carries `placement`: `crosses` (`chainage_nm` where the area is entered, `span_end_nm` where it is left), `abeam` (`chainage_nm` of the nearest point, `offset_nm` off track), `aerodrome` (chainage of the fix), `fir`, or `unplaced`. `meta.route`: `length_nm`, `waypoint_chainage[]`, `firs[]`, `fir_section` (`matched_by_affected_fir` or `unavailable`: a route through airspace where nothing else matched still misses its FIR notices), `placements` counts. Same `meta.gaps` rule as search. When there are more matches than `limit`, the answer keeps the notices crossing the route first, then those at its aerodromes, then the ones abeam, nearest first, then FIR-wide ones, so the same request returns the same notices; `meta.total` counts every match. The answer is `summary`, then the service's `notams` and `meta` as they are, and `resolved_route` for a route string. `summary` is the answer in one sentence: how many NOTAMs, in force when, where they sit, and the two caveats when they apply (the corridor touched no FIR in the archive; the answer stopped at `limit` before every match, `meta.truncated`). It counts the notices returned, a notice issued under two numbers once. Live, the flight above: ```jsonc { "summary": "1 NOTAM along the route, in force 24 Sep 07:00Z to 07:32Z: 1 crossing the corridor, 0 abeam, 0 at the route's aerodromes, 0 FIR-wide.", "notams": [ { "number": "A3710/26", "icao_location": "LQSB", "category": "airspace", "title": "Danger area activated", "placement": "crosses", "chainage_nm": 55.76438613588303, "span_end_nm": 57.76362309611289, ... } ], "meta": { "window": { "from": "2026-09-24T07:00:00Z", "to": "2026-09-24T07:32:27Z", "semantic": "in_force" }, "route": { "length_nm": 135.2, "firs": ["LQSB"], "fir_section": "matched_by_affected_fir", ... }, ... }, "resolved_route": { "route": "LDZA 4448N01552E LDSP", "notes": [], ... } } ``` Other live summaries: `from` alone, "1 NOTAM along the route, in force at 24 Sep 12:00Z: ..."; waypoints without times, "2 NOTAMs along the route, in the live set: 2 crossing the corridor, 0 abeam, 0 at the route's aerodromes, 0 FIR-wide."; a route whose corridor met no FIR, "... 0 FIR-wide. The corridor touched no FIR in the archive, so FIR-wide notices for this route are missing." ### `GET /v1/locations/{id}` The FAA location reference row for a domestic or ICAO identifier: `location_id`, `icao_id`, `description`, `state_text`, `center_text` and the full `payload`. `GET /v1/notams/categories` lists the category slugs. ## Airports and navaids (beta) Reference data from OurAirports (public domain, community maintained, refreshed nightly): airports, runways, COM frequencies and navaids worldwide. Route strings also use the FAA CIFP (public domain, one AIRAC cycle every 28 days, checked daily; the current and the next cycle are kept) for fixes, airways, SIDs and STARs in US airspace. Every answer carries `meta.source`, `meta.licence` and `meta.as_of` (when the source file was last regenerated). Use it to turn codes into coordinates before a RAIM route check or a NOTAM route brief. ### `GET /v1/airports/{code}` One airport by ICAO, IATA, GPS or local code (resolved in that order; `VIE` and `LOWW` are the same row). Fields: `code` (ICAO where one exists), `ident`, `icao_code`, `iata_code`, `gps_code`, `local_code`, `name`, `type` (`large_airport`, `medium_airport`, `small_airport`, `heliport`, `seaplane_base`, `balloonport`, `closed`), `lat`, `lon`, `elevation_ft`, `country` (ISO 3166-1), `region` (ISO 3166-2), `municipality`, `scheduled_service`, `links`, `runways[]` (`length_ft`, `width_ft`, `surface`, `lighted`, `closed`, `low_end` and `high_end` with `ident`, `lat`, `lon`, `elevation_ft`, `heading_deg_true`, `displaced_threshold_ft`) and `frequencies[]` (`type`, `description`, `mhz`). 404 when the code is unknown. ### `GET /v1/airports` Search. At least one of `q` (code, name or municipality), `country`, `region`, or `lat` and `lon` (with `radius_nm`, default 50, max 500). Optional `type[]`, `scheduled` (true/false), `limit` (default 25, max 100). Returns `airports[]` in the same shape as above without runways and frequencies, sorted by size then name; a position search adds `distance_nm` and sorts by distance. ### `GET /v1/navaids` At least one of `ident`, `airport` (the associated aerodrome code), or `lat` and `lon` with `radius_nm`. Optional `type` (`VOR`, `VOR-DME`, `VORTAC`, `DME`, `NDB`, `NDB-DME`, `TACAN`), `limit`. Each navaid: `ident`, `name`, `type`, `frequency_khz`, `lat`, `lon`, `elevation_ft`, `country`, `dme` (`frequency_khz`, `channel`, `lat`, `lon`, `elevation_ft`, or null), `magnetic_variation_deg`, `slaved_variation_deg`, `usage` (`LO`, `HI`, `BOTH`, `TERM`, `RNAV`), `power`, `associated_airport`. ### `GET /v1/routes/resolve` `route` (query), optional `at` (RFC 3339, default now, picks the AIRAC cycle): the points a route string stands for, exactly as `POST /v1/raim/route` would fly it: `route` (normalised), `points[]`, `notes[]` and `nav_data` as in `resolved_route` above. 422 `route_unresolved` with `unresolved[]` when a point cannot be placed. `GET /v1/airports/status` lists each dataset with its row count and `synced_at`, and `nav_data[]`: the AIRAC cycles loaded that are in force today or later (`source`, `airac`, `effective_from`, `effective_to`, `in_force`), each shaped as a route answer's `nav_data`. ## Weather (beta) The weather archive: METAR, TAF, SIGMETs (international and US), G-AIRMETs and CWAs from aviationweather.gov, collected since 2026-09-24, METARs polled every minute and the rest every few minutes, kept as received and never deleted, plus aircraft reports (PIREPs and AIREPs) for the route check. Decoded fields are the source's, next to the raw text; ours are the ceiling, a TAF group's flight category (FAA thresholds), the altitude band a hazard is matched against, and plain titles. All times RFC 3339 UTC. Every answer carries `meta`, which says what the answer describes and how far to trust it: - `time`: the time the answer describes: `semantic` (`latest`, `latest_at`, `history`, `valid_at`, `valid_during`, `flight`) with `at`, or `from` and `to`. - `as_of`: what was known: `time` (default now), `archive_start`, `before_archive`. Nothing first seen after `as_of.time` is used, so a past briefing replays as it was. `as_of` in the future is 400. - `collection`: per product used, `last_successful_poll`, `age_s`, `fresh`. - `gaps`: holes in collection overlapping the period (`checked`, `total`, `recent[]` with `product`, `from`, `to`, `status`, `notes`). Read them before trusting an answer about the past. - `missing` (ids with nothing found), `resolved` (ids read as another station), `rules` (how hazards were matched), `notes`. ```json "meta": { "time": { "semantic": "latest", "at": "2026-09-24T06:59:33.941179938Z" }, "as_of": { "time": "2026-09-24T06:59:33.941179938Z", "archive_start": "2026-09-24T00:04:15.773259Z", "before_archive": false }, "collection": { "metar": { "last_successful_poll": "2026-09-24T06:59:16.632769Z", "age_s": 17, "fresh": true } }, "gaps": { "checked": { "from": "2026-09-24T04:59:33.941179938Z", "to": "2026-09-24T06:59:33.941179938Z" }, "total": 0, "recent": [] }, "resolved": { "JFK": "KJFK" } } ``` The service's own refusals come back as 400 `bad_request` with its message, for example `{"error": "bad_request", "message": "at most 50 ids"}`. ### `GET /v1/weather/metar` - `ids` (required, up to 50, comma separated): ICAO identifiers; IATA and FAA codes are read through the station list (`JFK` as `KJFK`, said in `meta.resolved`). - Then one of: nothing (the latest report now), `at` (the latest observed at or before it), or `from` and `to` (every report in the window, up to 31 days). `as_of`: only what was known then. Each report: `station`, `name`, `lat`, `lon`, `elevation_m`, `observed_at`, `report_type` (`METAR`, `SPECI`), `raw`, `flight_category` (the source's), `wind` (`direction_deg`, `variable`, `speed_kt`, `gust_kt`), `visibility` (`statute_mi`, `or_more`), `ceiling_ft_agl` (ours: the lowest broken or overcast layer, or the vertical visibility), `vertical_visibility_ft`, `sky[]` (`cover`, `base_ft_agl`, `cloud_type`), `temperature_c`, `dewpoint_c`, `altimeter_in_hg`, `altimeter_hpa`, `weather`, `corrected`, `auto`, `decoded` (the rest of the source's fields), `versions` (corrections of the same report), `first_seen_at`, and without `from`/`to` `age_minutes`. ```json { "station": "EGLL", "name": "London/Heathrow Intl", "lat": 51.477, "lon": -0.461, "elevation_m": 26, "observed_at": "2026-09-24T06:20:00Z", "report_type": "METAR", "raw": "METAR EGLL 240620Z AUTO 05001KT 9999 NCD 11/09 Q1026", "flight_category": "VFR", "wind": { "direction_deg": 50, "variable": false, "speed_kt": 1, "gust_kt": null }, "visibility": { "statute_mi": 6, "or_more": true }, "ceiling_ft_agl": null, "vertical_visibility_ft": null, "sky": [ { "cover": "CLR" } ], "temperature_c": 11, "dewpoint_c": 9, "altimeter_in_hg": 30.3, "altimeter_hpa": 1026.1, "weather": null, "corrected": false, "auto": true, "versions": 1, "first_seen_at": "2026-09-24T06:26:03.718225Z", "age_minutes": 22.2 } ``` ### `GET /v1/weather/taf` `ids` as for METARs, optional `at` and `as_of`. Without `at`: per station the newest TAF issued, even one valid only from a later hour; `covers_time` says whether it is in force now. With `at`: the TAF in force then, the newest issued by then whose validity covers it; for an `at` in the future, the TAF issued by now that covers it, and `meta.notes` says so. A station with none is in `meta.missing`. Each TAF: `station`, `name`, position, `issued_at`, `bulletin_at`, `valid_from`, `valid_to`, `covers_time`, `raw`, `amended`, `corrected`, `remarks`, `versions`, `first_seen_at`, and `periods[]`, the base forecast first, then the change groups by start: `from`, `to`, `change` (null for the base forecast, `FM`, `BECMG`, `TEMPO`, `PROB`), `probability`, `becoming_at`, `wind`, `wind_shear`, `visibility`, `sky`, `vertical_visibility_ft`, `weather`, `turbulence`, `icing`, `temperatures`, `not_decoded`, and ours: `ceiling_ft_agl`, `flight_category`, `outside_validity` (a group whose times are a typo at source). ```jsonc // trimmed: three of the seven periods { "station": "EGLL", "issued_at": "2026-09-24T04:56:00Z", "valid_from": "2026-09-24T06:00:00Z", "valid_to": "2026-09-25T12:00:00Z", "covers_time": true, "raw": "TAF EGLL 240456Z 2406/2512 08005KT 9999 FEW030 PROB30 2406/2408 7000 BECMG 2411/2414 19010KT BECMG 2419/2422 VRB03KT TEMPO 2500/2506 7000 PROB30 TEMPO 2503/2508 3000 BR BKN004 TEMPO 2506/2510 7000 BKN006", "periods": [ { "from": "2026-09-24T06:00:00Z", "to": "2026-09-24T11:00:00Z", "change": null, "wind": { "direction_deg": 80, "variable": false, "speed_kt": 5, "gust_kt": null }, "visibility": { "statute_mi": 6, "or_more": true }, "sky": [ { "cover": "FEW", "base_ft_agl": 3000 } ], "ceiling_ft_agl": null, "flight_category": "VFR" }, { "from": "2026-09-24T11:00:00Z", "to": "2026-09-24T19:00:00Z", "change": "BECMG", "becoming_at": "2026-09-24T14:00:00Z", "wind": { "direction_deg": 190, "variable": false, "speed_kt": 10, "gust_kt": null }, "visibility": { "statute_mi": 6, "or_more": true }, "sky": [ { "cover": "FEW", "base_ft_agl": 3000 } ], "ceiling_ft_agl": null, "flight_category": "VFR" }, { "from": "2026-09-25T03:00:00Z", "to": "2026-09-25T08:00:00Z", "change": "TEMPO", "probability": 30, "visibility": { "statute_mi": 1.86, "or_more": false }, "sky": [ { "cover": "BKN", "base_ft_agl": 400 } ], "weather": "BR", "ceiling_ft_agl": 400, "flight_category": "LIFR" } ] } ``` ### `GET /v1/weather/hazards` SIGMETs, G-AIRMETs and CWAs in force. Without an area the answer is worldwide. - `bbox`: `west,south,east,north` in degrees; west greater than east crosses the antimeridian. - `lat`, `lon`, `radius_nm`: within a radius, up to 2,000 NM. - `at` (default now): in force at an instant. `from`, `to`: in force at any point of a window, up to 72 hours. `as_of`: what was known then. - `fl_from`, `fl_to`: a flight level band; only a stated limit keeps a hazard out. - `types`: `sigmet_intl`, `sigmet_us`, `gairmet`, `cwa`, or `sigmet` for both SIGMET feeds. - `hazard`: source codes, comma separated (`TS`, `TC`, `TURB-HI`, `ICE`, `IFR`, `LLWS`, ...). `FZLVL` returns the G-AIRMET freezing-level contours, otherwise left out. - `include`: `geometry` (GeoJSON), `raw` (the bulletin). - `limit`: default 500; `meta.truncated` says when there were more. Each hazard: `id`, `product`, `ident` (`G-AIRMET TANGO 1W TURB-HI +3h`, `KZMA SIGMET FOXTROT 4`, `NFFF SIGMET 05`), `hazard`, `title`, `qualifier`, `severity`, `issuer`, `fir_id`, `fir_name`, `series`, `issued_at`, `valid_from`, `valid_to`, `snapshot_at` and `package_expires_at` (G-AIRMET), `status`, `band` (`base_ft`, `base`, `top_ft`, `top`, `text`, and `matched_from_ft`, `matched_to_ft`: the band a level is compared with), `movement`, `change`, `due_to`, `geometry_kind`, `geometry_note` (a repair of a broken shape at source), `listed[]` (when the feed carried it: `from`, `last_seen`, `until`), `first_seen_at`. ```jsonc // trimmed: listed[] left out { "id": 29, "product": "gairmet", "ident": "G-AIRMET TANGO 1W TURB-HI +3h", "hazard": "TURB-HI", "title": "Moderate turbulence (high level)", "severity": "MOD", "issuer": "KKCI", "series": "TANGO", "issued_at": "2026-09-24T02:45:00Z", "valid_from": "2026-09-24T04:30:00Z", "valid_to": "2026-09-24T07:30:00Z", "snapshot_at": "2026-09-24T06:00:00Z", "package_expires_at": "2026-09-24T09:00:00Z", "status": "NRML", "band": { "base_ft": 17000, "base": "given", "top_ft": 33000, "top": "given", "text": "17000 ft to FL330", "matched_from_ft": 17000, "matched_to_ft": 33000 }, "geometry_kind": "area", "first_seen_at": "2026-09-24T06:06:29.198579Z" } ``` Matching rules, in `meta.rules` on every hazard and route answer: - `in_force`: valid at the time asked about and listed in the source's feed at that time, or at `as_of` if that is earlier; a hazard dropped from the feed before it expired (cancelled or superseded) stops counting from then. - `vertical`: only a stated limit keeps a hazard off a level: an unstated base is the surface, an unstated top is unbounded; heights are compared in feet without correcting flight levels for pressure. - `gairmet_snapshot_window`: a G-AIRMET snapshot is taken to apply 90 minutes either side of its valid time. - `surface_based_gairmets_top`: IFR, mountain obscuration, low-level wind shear and surface wind G-AIRMETs have no issued top; they are matched up to 15,000 ft (`top: "assumed"`). - `freezing_level_contours`: stored, returned only with `hazard=FZLVL`. ### `POST /v1/weather/route` The weather along a flight in four dimensions: a hazard counts where the corridor meets it, while it is in force, and where the aircraft is within its band. The flight is the flight object (see Flight object). The service times it (each waypoint's `time_source`: `given`, `departure`, `speed`, `interpolated`) and levels it (`level_source`: `given`, `interpolated` by distance between waypoints that have a level, `unknown` outside them); an unknown level never keeps a hazard off. Give top of climb and top of descent as waypoints for a realistic profile. Options beside the flight: - `alternates` (up to 10): METAR and TAF at the destination ETA. - `half_width_nm` (1 to 100, default 20: the FAA's distance to keep from a thunderstorm). - `as_of`: replay with only what was known then. - `types`, `hazards`: as `types` and `hazard` on `GET /v1/weather/hazards`. - `pireps`: `significant` (default), `all` (routine wind and temperature AIREPs too, otherwise counted in `meta.notes`) or `none`; `pirep_radius_nm` (1 to 200, default 50), `pirep_lookback_minutes` (1 to 720, default 90), `pirep_level_band_ft` (default 4,000: a report further from the aircraft's level there is left out). - `aerodrome_window_minutes` (0 to 360, default 60): TAF groups within this of the aircraft's time are marked `in_window`. - `include_geometry` (default false), `include_raw` (default true: the hazard bulletins). Limits: 2 to 200 waypoints, each leg under 9,000 NM, the route at most 13,000 NM and 36 hours. The answer: - `summary`: the answer in one paragraph. Show it to people as it is. - `flight`: `length_nm`, `departure`, `arrival`, `duration_min`, `half_width_nm`, `sample_step_nm`, `waypoints[]` (`index`, `id`, `lat`, `lon`, `chainage_nm`, `time`, `time_source`, `alt_ft`, `level_source`, `codes`). - `hazards[]`: in the corridor while in force and within the band, in order along the track. The hazard fields above, plus `placement` (`crosses`: the track enters the area; `abeam`: within the half width), `affects`, `chainage_nm` (NM from the first waypoint), `offset_nm`, `track[]` (where the aircraft is in it: `from_nm`, `to_nm`, `from_time`, `to_time`, `level_from_ft`, `level_to_ft`, `min_offset_nm`, `track_inside`) and `spatial[]` (where the corridor meets it at all). - `hazards_clear[]`: in the corridor, but not at the aircraft's time or level: `clear_by` (for example `before_valid`, `after_expiry`, `above`) and `clear_reason`. - `unplaced[]`: in force during the flight, without usable geometry. - `aerodromes[]`: `role` (`departure`: the first waypoint's `codes`, or its `id` when the station list knows it; `destination`: the last waypoint's; `alternate`; `enroute`: `codes` on other waypoints), `code`, `station`, `name`, `time` (when the aircraft is there), `reference` (the instant the reports are chosen for: that time, or `as_of`, default now, when that is earlier), `window`, `metar`, `taf` (with `covers_time`, `in_window` on its groups, `worst_category_in_window`), `missing`. - `pireps[]`: `observed_at`, `report_type`, `aircraft`, `raw`, `lat`, `lon`, `altitude_ft`, `chainage_nm`, `offset_nm`, `aircraft_passes_at`, `aircraft_level_ft`, `level_diff_ft`, `significant`, and when reported `turbulence`, `icing`, `weather`, `temperature_c`. - `meta` (with `rules` and `notes`), and `resolved_route` for a route string. ``` curl -s https://api.bravoecho.ai/v1/weather/route \ -H "Authorization: Bearer $BRAVOECHO_KEY" -H "Content-Type: application/json" \ -d '{"route":"KMSP DWN6.ABR J158 RAP MBW MTU J148 OAL.MOD5 KSFO","departure":"2026-09-24T08:00:00Z","speed_kt":450,"cruise_fl":350,"alternates":["KOAK"]}' ``` Live (that call, run at 06:42Z): the aircraft climbs into a high-level turbulence G-AIRMET at 222 NM and stays in it to 953 NM; a surface-based IFR G-AIRMET it crosses in the climb is clear because the aircraft is above its assumed top by then. ```jsonc { "summary": "1436 NM, 24 08:00Z to 24 11:11Z. 2 hazard(s) affect the flight: Moderate turbulence (high level) (G-AIRMET TANGO 3W TURB-HI +6h) crosses from 222 NM, 08:29Z at FL350; IFR conditions (G-AIRMET SIERRA 2E IFR +9h) crosses from 1428 NM, 11:10Z at 15000 ft. 5 more meet the corridor but not the aircraft's time or level. departure KMSP: METAR VFR, TAF worst VFR around 08:00Z. destination KSFO: METAR VFR, TAF worst MVFR around 11:11Z. Collection has 1 gap(s) in the period: see meta.gaps.", "flight": { "length_nm": 1436.4, "departure": "2026-09-24T08:00:00Z", "arrival": "2026-09-24T11:11:31Z", "duration_min": 191.5, "half_width_nm": 20, "sample_step_nm": 1, "waypoints": [ { "index": 0, "id": "KMSP", "lat": 44.880081, "lon": -93.221741, "chainage_nm": 0, "time": "2026-09-24T08:00:00Z", "time_source": "departure", "alt_ft": 841, "level_source": "given", "codes": ["KMSP"] }, { "index": 1, "id": "ABR", "lat": 45.4174, "lon": -98.368698, "chainage_nm": 220.3, "time": "2026-09-24T08:29:22Z", "time_source": "speed", "alt_ft": 35000, "level_source": "given" }, ... ] }, "hazards": [ { "ident": "G-AIRMET TANGO 3W TURB-HI +6h", "hazard": "TURB-HI", "title": "Moderate turbulence (high level)", "severity": "MOD", "valid_from": "2026-09-24T07:30:00Z", "valid_to": "2026-09-24T10:30:00Z", "band": { "text": "FL250 to FL420", "matched_from_ft": 25000, "matched_to_ft": 42000, ... }, "placement": "crosses", "affects": true, "chainage_nm": 222.2, "offset_nm": 0, "track": [ { "from_nm": 222.2, "to_nm": 952.9, "from_time": "2026-09-24T08:29:37Z", "to_time": "2026-09-24T10:07:02Z", "level_from_ft": 35000, "level_to_ft": 35000, "min_offset_nm": 0, "track_inside": true } ], ... }, ... ], "hazards_clear": [ ..., { "ident": "G-AIRMET SIERRA 5E IFR +6h", "placement": "crosses", "valid_from": "2026-09-24T07:30:00Z", "valid_to": "2026-09-24T10:30:00Z", "band": { "text": "SFC to 15000 ft (top assumed)", ... }, "chainage_nm": 162.9, "spatial": [ { "from_nm": 162.9, "to_nm": 370.9, "from_time": "2026-09-24T08:21:43Z", "to_time": "2026-09-24T08:49:27Z", "level_from_ft": 26106, "level_to_ft": 35000, ... } ], "clear_by": "above", "clear_reason": "the aircraft is above the hazard's top" }, ... ], "aerodromes": [ ..., { "role": "destination", "code": "KSFO", "station": "KSFO", "time": "2026-09-24T11:11:31Z", "window": { "from": "2026-09-24T10:11:31Z", "to": "2026-09-24T12:11:31Z" }, "metar": { "observed_at": "2026-09-24T05:56:00Z", "flight_category": "VFR", "raw": "METAR KSFO 240556Z 29008KT 10SM CLR 17/12 A2986 RMK AO2 SLP113 T01720122 10250 20172 51005", ... }, "taf": { "valid_from": "2026-09-24T06:00:00Z", "valid_to": "2026-09-25T12:00:00Z", "covers_time": true, "worst_category_in_window": "MVFR", ... } } ], "resolved_route": { "route": "KMSP DWN6.ABR J158 RAP MBW MTU J148 OAL.MOD5 KSFO", "notes": [ "Procedure DWN6 is not in AIRAC 2609 (US procedures only): flown direct to its transition ABR.", "MTU: 2 places share this identifier; took Myton VORTAC (US), the one on the way.", "MOD5 is not published at KSFO in AIRAC 2609; flown as MOD9, its current revision." ], ... } } ``` For a flight ahead, `meta.notes` says so: "the flight is in the future: hazards are the forecasts listed now, aerodrome reports are the latest now and TAF groups are marked for the times of the flight". ## Winds aloft (beta) `POST /v1/weather/winds/route`, `GET /v1/weather/winds` The wind and temperature a flight meets at its levels and times, from the official winds and temperatures aloft forecasts (FB) of aviationweather.gov, the forecast a standard briefing gives: the United States, Alaska, Hawaii and the Pacific, 3,000 to 53,000 ft, issued four times a day for 6, 12 and 24 hours ahead, every issue kept so a past briefing replays with what was issued by then. FB forecasts are point forecasts for about 220 stations: each value here is interpolated between the stations nearest the point (inverse-distance weighting of up to four within 400 NM, the wind as components) and between the levels either side, from the forecast whose period of use holds the time. It is not a gridded model: what happens between stations, such as a jet stream core or mountain waves, is not resolved, and the caveats say so. Request: the flight object (see Flight object) and: - `step_nm` (default 25, 5 to 200): distance between samples along the track; every fix is sampled too. - `tas_kt` (50 to 650): true airspeed. Adds the ground speed at each sample and the flight's time with the wind against still air. The flight's times still come from its waypoints, `departure` and `speed_kt`. - `as_of`: replay: only the forecasts issued by then. ```json { "waypoints": [ { "id": "KLAS", "lat": 36.08, "lon": -115.1522, "alt_ft": 2181 }, { "id": "TOC", "lat": 35.75, "lon": -115.75, "fl": 250 }, { "id": "TOD", "lat": 34.35, "lon": -117.6, "fl": 250 }, { "id": "KLAX", "lat": 33.9425, "lon": -118.4081, "alt_ft": 128 } ], "departure": "2026-10-14T03:00:00Z", "speed_kt": 380, "tas_kt": 440, "step_nm": 50 } ``` Response (shortened; live on 14 Oct): ```json { "summary": "Over 206 NM at its levels the flight meets an average tailwind component of 15 kt; the strongest wind is 338° 61 kt at 35 NM (FL250); temperatures average ISA +8 °C; at 440 kt TAS it takes 27 min with the wind against 28 min in still air.", "points": [ { "chainage_nm": 35.2, "fix": "TOC", "time": "2026-10-14T03:05:33Z", "level_ft": 25000, "track_deg": 228, "wind_dir_deg": 338, "wind_kt": 61, "temp_c": -28, "isa_dev_c": 6.5, "headwind_kt": -21, "crosswind_kt": 57, "ground_speed_kt": 457, "light_and_variable": false, "available": true, "reason": null, "notes": [] } ], "legs": [ { "from": "TOC", "to": "TOD", "from_nm": 35.2, "to_nm": 159, "avg_headwind_kt": -15.6, "max_wind_kt": 61, "avg_temp_c": -26, "avg_isa_dev_c": 8.5 } ], "totals": {"avg_headwind_kt": -14.6, "max_wind": {"kt": 61, "dir_deg": 338, "chainage_nm": 35.2, "level_ft": 25000}, "avg_isa_dev_c": 7.7, "time_minutes": 27.3, "still_air_minutes": 28.1, "available_pct": 100}, "flight": { "length_nm": 206, "departure": "2026-10-14T03:00:00Z", "arrival": "2026-10-14T03:32:32Z", "sample_step_nm": 50, "tas_kt": 440, "fixes": [] }, "meta": { "as_of": { "time": "..." }, "forecasts": [ { "region": "us", "level_set": "low", "based_on": "...", "valid": "2026-10-14T06:00:00Z", "for_use_from": "2026-10-14T03:00:00Z", "for_use_to": "2026-10-14T12:00:00Z" } ], "sources": { "windtemp": { "last_poll": "...", "stale": false } }, "caveats": [] } } ``` - `points[]`: each sample with its distance along the track, time and level, the true course of the great circle there (`track_deg`), the wind (`wind_dir_deg` true, where it blows from, 1 to 360, null when light and variable; `wind_kt`), the temperature, ISA and the deviation from ISA, `headwind_kt` (positive a headwind, negative a tailwind) and `crosswind_kt` (positive from the right), and with `tas_kt` the ground speed. A point with no forecast within reach has `available: false` and a `reason`. `notes` say when a value was held or carried down below a station's lowest forecast level. - `legs[]`: from fix to fix, the distance-weighted average headwind, the strongest wind, the average temperature and ISA deviation. `totals`: the same over the flight, the strongest wind with where and at what level, the share of the flight with a forecast, and with `tas_kt` the minutes with the wind and in still air. - `meta.forecasts`: the bulletins the answer used, with their period of use. `meta.sources.windtemp`: when the forecasts were last polled (`stale` when late) and the newest issue. `meta.caveats`: read them; they say how the values were made. - Levels are compared in feet as published: flight levels as pressure altitude, FB levels as true altitude up to 12,000 ft and pressure altitude from 18,000 ft. FB leaves the levels near a station's ground blank and 3,000 ft has no temperature, so below a station's lowest forecast its lowest wind is held and its deviation from ISA is carried down. `GET /v1/weather/winds?lat=36.08&lon=-115.15&fl=250&at=2026-10-14T03:00:00Z` answers one point the same way (`alt_ft` instead of `fl` for an altitude), with the stations used: id, name, position, distance, weights and their published values at the levels either side. ## Airspace (beta) `POST /v1/airspace/route` The airspace a flight enters, where, when and at what level: special use airspace (prohibited, restricted, warning and alert areas, military operations areas, and the danger areas the FAA publishes for the Bahamas), Class B, C and D airspace, and TFRs. United States only, from FAA data: special use and class airspace from the FAA's airspace datasets, checked every day and imported again when the FAA publishes a change, and TFRs from the FAA's TFR service, polled every 5 minutes with every version kept, so a past briefing replays with what was known then. Airspace is placed along the flight as the weather route check places hazards: an airspace is entered where the track itself is inside its area while it is in force and the aircraft is within its vertical limits there. Request: the flight object (see Flight object) and: - `half_width_nm` (default 5, 1 to 50): airspace within this distance of the track that the flight does not enter is listed as `near`. - `kinds`: only these kinds: `prohibited`, `restricted`, `warning`, `alert`, `moa`, `danger`, `class_b`, `class_c`, `class_d`, `tfr`. Default all. - `as_of`: replay: only the TFRs known at that time. - `include_geometry` (default false): each airspace's area as GeoJSON. Levels come from the flight object as for the weather route check: waypoints carry `alt_ft`, `fl` or `alt_m`; a route string with a cruise level climbs to a top of climb and descends from a top of descent added as points (see Flight object). Without any level nothing is left out by level. With waypoints, give your own top of climb and top of descent for a realistic profile: between points the level is interpolated by distance. ```json { "waypoints": [ { "id": "KLAS", "lat": 36.08, "lon": -115.1522, "alt_ft": 2181 }, { "id": "TOC", "lat": 35.75, "lon": -115.75, "fl": 250 }, { "id": "TOD", "lat": 34.35, "lon": -117.6, "fl": 250 }, { "id": "KLAX", "lat": 33.9425, "lon": -118.4081, "alt_ft": 128 } ], "departure": "2026-10-14T18:00:00Z", "speed_kt": 300 } ``` Response (shortened; waypoints KLAS, top of climb, top of descent, KLAX at FL250): ```json { "summary": "The flight enters 11 airspaces (2 restricted areas, 6 class B areas, 3 TFRs), first LAS VEGAS CLASS B at 0 NM (18:00Z, 2181 ft); 20 more are near; 12 in force during the flight cannot be placed (see unplaced).", "airspaces": [ { "id": "faa_sua:10FDAEFA-40E3-4810-B1D9-A71CB0F97F74", "source": "faa_sua", "kind": "restricted", "name": "R-2508", "lower": {"ft": 20000, "ref": "MSL", "text": "20000 ft MSL", "inclusive": true, "matched_ft": 20000}, "upper": {"ft": null, "ref": "UNL", "text": "unlimited", "inclusive": true, "matched_ft": null}, "times_of_use": "CONTINUOUS", "controlling": "FAA, JOSHUA CONTROL FACILITY, EDWARDS AFB, CA", "state": "CA", "placement": "enters", "chainage_nm": 72.4, "entries": [{ "from_nm": 72.4, "to_nm": 91.5, "from_time": "2026-10-14T18:14:29Z", "to_time": "2026-10-14T18:18:18Z", "level_from_ft": 25000, "level_to_ft": 25000, "track_inside": true }], "min_offset_nm": 0, "clear_by": null, "notes": [] } ], "unplaced": [{ "source": "faa_tfr", "id": "faa_tfr:0/0367", "notam": "0/0367", "type": "SECURITY", "text": "!FDC 0/0367 FDC PART 1 OF 4 SECURITY SPECIAL SECURITY INSTRUCTIONS (SSI) FOR SELECT SPORTING EVENTS ...", "why": "no shape published; a national notice (FDC), listed for every flight in or near US airspace", "location": "FDC", "scope": "national" }], "flight": { "length_nm": 206, "departure": "2026-10-14T18:00:00Z", "arrival": "2026-10-14T18:41:12Z", "half_width_nm": 5, "fixes": [] }, "meta": { "as_of": { "time": "..." }, "sources": { "faa_sua": { "edited_at": "2026-09-28T13:54:26.108Z", "imported_at": "...", "features": 1527 }, "faa_class": {}, "faa_tfr": { "last_poll": "...", "listed": 76, "stale": false } }, "truncated": false, "caveats": [] } } ``` - `airspaces[]`: the entered ones in order along the track, then the near ones. Where Class B airspace is built of shelves, each shelf is its own airspace. Each has `kind`, `name`, `ident`, `chainage_nm` (the first entry, or where the corridor first meets it), `lower` and `upper`, `times_of_use` and `controlling` for special use and class airspace, and `placement`: `enters` with its `entries` (the stretches where the aircraft is inside: distance along the track in NM, times and the aircraft's levels), or `near` with `min_offset_nm` and `clear_by`: `level` (the aircraft is above or below it there), `time` (not in force then) or `distance` (the track passes outside it). - `lower` and `upper`: `ft` as published in its own reference (18000 for FL180, 3000 for 3000 ft AGL, 0 for the surface, null for unlimited), `ref` (`SFC`, `MSL`, `AGL`, `FL` or `UNL`), `text` as published, `inclusive` (false for "to but not including" and "above but not including": the limit level itself is outside) and `matched_ft`, the level the placement matched in feet above sea level, flight levels taken as pressure altitude. There is no terrain model: a limit given above ground level is widened by the highest terrain in its state, so a flight is never cleared by a guess about the ground, and `notes` says so. - A TFR adds `tfr`: `notam`, `type`, `rule`, `effective_from`, `effective_to` (null for until further notice), `schedule`, `text`, `area` and `shape` (`published`: drawn from the area's own circles, arcs and points; `merged`: the FAA's outline of the whole TFR). A TFR in force during the flight with no published shape cannot be placed: it is in `unplaced` with its text, and the summary counts it. Each carries `location`, the NOTAM's location, and `scope`. A notice issued for an ARTCC (such as the Washington DC instructions, location ZDC) is listed only when the route passes through that centre, with `scope: "artcc"` and the centre in `artcc`. A national notice (location FDC or ZZZ: the special security instructions for sporting events, unmanned aircraft and national security) is listed for every flight in or near US airspace, with `scope: "national"`. `meta.unplaced_filter` says what was picked: `route_artccs` (the centres the track enters), `near_artccs`, `us_airspace` (`in`, `near`, `outside` or `unknown`), and the notices left out (`left_out_artcc`, `left_out_by_artcc`, `left_out_national`). The ARTCC boundaries come from the FAA's Boundary_Airspace layer (`meta.sources.faa_artcc`); until they are loaded every notice is listed, with `applied: false`. - Special use airspace counts as in force at all times. Its published times of use are given, but whether it is active is not known here: check NOTAMs or the controlling agency. A TFR with a daily schedule counts as in force over its whole effective window; its hours are in `schedule`. - `meta.sources`: when each FAA dataset was edited and imported, and when the TFR list was last polled (`stale` when it is late). `meta.truncated`: more airspace met the corridor than one answer lists; narrow `half_width_nm` or `kinds`. `meta.caveats`: what the answer cannot know. Read them before relying on an answer. ## Mass and balance (beta) `POST /v1/planning/mass-balance` Weight and centre of gravity at ramp, takeoff, landing and zero fuel for one loading, checked against the aircraft's weight limits and centre of gravity envelopes, with the centre of gravity path as the trip fuel burns. It computes on the aircraft data in the request: BravoEcho holds no aircraft data, so the answer is only as right as that data. Check it against the aircraft flight manual. Request: - `units`: `imperial` (lb, in, US gallons; the default) or `metric` (kg, m, litres). - `aircraft`: `empty` (`weight`, and `arm` or `moment`, from the weight and balance report); `stations` (`id`, `arm`, optional `max_weight`): seats, baggage areas, cargo; `fuel_tanks` (`id`, `arm`, optional `capacity` in volume, and `fuel_type` `avgas`, `jet_a` or `mogas`, or your own `unit_weight`: avgas and mogas weigh 6.0 lb a gallon or 0.72 kg a litre, jet_a 6.7 lb or 0.80 kg); `limits` (`max_ramp`, `max_takeoff`, `max_landing`, `max_zero_fuel`); `envelopes` (`name` and `points`, the polygon as `[arm, weight]` pairs); optional `mac` (`leading_edge_arm`, `length`) for the centre of gravity in % MAC. Arms are from the datum, positive aft. - `load`: `stations` (weight by station id) and `fuel` (volume by tank id). - `fuel_burn`: `taxi` and `trip` fuel in volume, burned from the tanks in proportion to what each holds, unless `burn_order` names the tanks to empty in turn. Anything else is 400 `unknown_field`. A load naming a station or tank the aircraft does not have, or more fuel burned than loaded, is 422. ```json { "aircraft": { "empty": { "weight": 1663, "arm": 39.45 }, "stations": [{ "id": "front_seats", "arm": 37 }, { "id": "rear_seats", "arm": 73 }, { "id": "baggage_1", "arm": 95, "max_weight": 120 }], "fuel_tanks": [{ "id": "main", "arm": 48, "capacity": 53, "fuel_type": "avgas" }], "limits": { "max_ramp": 2558, "max_takeoff": 2550, "max_landing": 2550 }, "envelopes": [{ "name": "normal", "points": [[35, 1500], [35, 1950], [39.5, 2550], [47.3, 2550], [47.3, 1500]] }] }, "load": { "stations": { "front_seats": 340, "rear_seats": 0, "baggage_1": 30 }, "fuel": { "main": 48 } }, "fuel_burn": { "taxi": 1.4, "trip": 20 } } ``` Response (shortened): ```json { "summary": "Takeoff 2,312.6 lb at 40.84 in, landing 2,192.6 lb at 40.45 in. The centre of gravity stays within the normal envelope from takeoff to landing.", "ok": true, "units": { "system": "imperial", "weight": "lb", "arm": "in", "fuel": "gal" }, "phases": { "ramp": { "weight": 2321, "moment": 94859.35, "cg_arm": 40.87, "cg_mac_pct": null, "fuel": 48, "envelopes": [{ "name": "normal", "within": true }] }, "takeoff": { "weight": 2312.6, "moment": 94456.15, "cg_arm": 40.844, "cg_mac_pct": null, "fuel": 46.6, "envelopes": [{ "name": "normal", "within": true }] }, "landing": { "weight": 2192.6, "moment": 88696.15, "cg_arm": 40.452, "cg_mac_pct": null, "fuel": 26.6, "envelopes": [{ "name": "normal", "within": true }] }, "zero_fuel": { "weight": 2033, "moment": 81035.35, "cg_arm": 39.86, "cg_mac_pct": null, "fuel": 0, "envelopes": [{ "name": "normal", "within": true }] } }, "limits": [{ "what": "takeoff weight", "value": 2312.6, "max": 2550, "margin": 237.4, "ok": true }], "cg_path": [{ "name": "normal", "within": true, "leaves_after_burning": null }], "stations": [{ "id": "front_seats", "weight": 340, "arm": 37, "moment": 12580 }], "notes": ["BravoEcho holds no aircraft data: every figure comes from the request, and the answer is only as right as it is. Check it against the aircraft flight manual.", "Lateral balance is not computed."] } ``` - `ok`: every limit met, and when envelopes are given, the centre of gravity within at least one of them all the way from takeoff to landing. - `phases`: weight, moment, centre of gravity (`cg_arm`, and `cg_mac_pct` with `mac`), the fuel left and each envelope's verdict. A point on an envelope's edge is within it. - `limits`: each station, tank and phase weight against its maximum, with the margin. - `cg_path`: each envelope against the centre of gravity as the trip fuel burns; `leaves_after_burning` is the fuel burned after takeoff when it leaves. ## Navlog (beta) `POST /v1/planning/navlog` A navigation log for a flight: each leg with its course and heading, true and magnetic, wind, ground speed, time, fuel and ETA, and the fuel plan, from the aircraft performance in the request and the winds aloft along the flight (see Winds aloft; United States, still air elsewhere). BravoEcho holds no aircraft performance models: the times and fuel are only as right as the figures you send. Check them against the aircraft flight manual. Request: the flight object (see Flight object) and: - `performance` (required): `cruise` (`tas_kt`, `fuel_per_hour`) and, when they differ, `climb` and `descent` (`tas_kt`, `fuel_per_hour`, `rate_fpm`). A route string climbs and descends at the aircraft's own rates: its top of climb and top of descent are placed from `rate_fpm` and `tas_kt` (otherwise at 250 ft a NM up and a 3 degree path down). Waypoints are flown as given: a leg that climbs is flown at the climb figures, one that descends at the descent figures, the rest at the cruise figures. - `taxi_fuel`, `reserve` (`minutes` at the cruise fuel flow, or `fuel`), `fuel_on_board`, and `fuel_unit` (`gal`, `L`, `lb` or `kg`: a label for your figures). - `step_nm` (default 10, 5 to 50): how often the wind is sampled along the track. - `departure` picks the wind forecasts and gives the ETAs; the legs' times come from the performance, not from `speed_kt`. ```json { "route": "KMSP DCT KMKE", "departure": "2026-10-14T15:00:00Z", "cruise_fl": 65, "performance": { "climb": { "tas_kt": 80, "fuel_per_hour": 12, "rate_fpm": 700 }, "cruise": { "tas_kt": 120, "fuel_per_hour": 9 }, "descent": { "tas_kt": 130, "fuel_per_hour": 7, "rate_fpm": 500 } }, "taxi_fuel": 1.4, "reserve": { "minutes": 45 }, "fuel_on_board": 48, "fuel_unit": "gal" } ``` Each leg: `from`, `to`, `phase` (`climb`, `cruise`, `descent`), `distance_nm`, `level_from_ft` and `level_to_ft`, `true_course_deg`, `magnetic_variation_deg` (east positive) and `magnetic_course_deg`, the average `wind` (`dir_deg`, `kt`) and `headwind_kt` (positive a headwind), `wind_correction_deg` (positive to the right), `true_heading_deg` and `magnetic_heading_deg`, `tas_kt`, `ground_speed_kt`, `time_min`, `fuel`, the running `elapsed_min` and `fuel_used`, `eta`, and `still_air_nm` where no forecast reached. Ground speed holds the heading that makes good the track: the square root of the true airspeed squared less the crosswind squared, less the headwind; a wind that leaves no ground speed is 422. The magnetic variation is the declination of the World Magnetic Model 2025 (WMM2025, developed by NOAA and the British Geological Survey) at the leg's great-circle midpoint and mid time, to a tenth of a degree. The magnetic course and heading are the true ones as shown minus the variation, rounded, with 360 for north; compass deviation is not applied. WMM2025 is valid from 2025.0 to 2030.0 (1 January 2025 to 31 December 2029), and a leg flown outside that has no magnetic fields. Near the magnetic poles the horizontal field is weak and a compass unreliable: a leg in the blackout zone (horizontal field under 2000 nT) has no magnetic fields, and one in the caution zone (2000 to 6000 nT) keeps them with a caution. `meta.notes` names the legs in each case. The model holds the field of the Earth's core only, not local anomalies. `totals`: `distance_nm`, `time_min`, `arrival`, `trip_fuel`, `taxi_fuel`, `reserve_fuel`, `required_fuel` and, with `fuel_on_board`, `fuel_at_destination` and `reserve_met`; `avg_headwind_kt`. `summary` says it in one sentence. Live for the request above on 14 Oct: "KMSP to KMKE: 258 NM in 1 h 55 min, trip fuel 17.3 gal (taxi 1.4, reserve 6.8, required 25.4). With 48.0 on board, 29.3 left at the destination: reserve met. Average tailwind 17 kt." Its cruise leg: {"from": "TOC", "to": "TOD", "phase": "cruise", "distance_nm": 222, "true_course_deg": 115, "magnetic_variation_deg": -2.1, "magnetic_course_deg": 117, "wind": {"dir_deg": 307, "kt": 18}, "headwind_kt": -17.3, "wind_correction_deg": -1.5, "true_heading_deg": 113, "magnetic_heading_deg": 115, "tas_kt": 120, "ground_speed_kt": 137, "time_min": 97.1, "fuel": 14.6}. `meta.winds` names the forecasts used; `meta.notes`: courses and headings are given true and magnetic, the legs without magnetic fields and why, and where the winds come from. ## Flight briefing pack `POST /v1/briefing` RAIM, NOTAMs, weather and airspace for one flight in one call. The same flight object goes to the RAIM route check, the NOTAMs along the route, the weather along the route and the airspace the flight enters (see Airspace), or to the parts you name in `parts`, and the answer leads with one summary built from them. Each part counts as one request of its own service, recorded as `POST /v1/briefing (raim)`, `(notams)`, `(weather)` and `(airspace)`: today the RAIM part is counted like `POST /v1/raim/route`, and the NOTAM, weather and airspace parts are Beta and not counted. With RAIM in it the pack stands on its RAIM route check: when that check fails, the pack answers with its error. When any other part fails, its part says so and the rest of the pack stands. Request: the flight object (see Flight object) and these options. Anything else is refused with 400 `unknown_field`; for finer control call the route endpoints. - `parts`: the parts to run, any of `raim`, `notams`, `weather` and `airspace`. Default: all four. - `window_minutes` (default 0): for the RAIM part, how far the departure may slip each side, as on `POST /v1/raim/route`. - `profile`: the RAIM profile, as on `POST /v1/raim/route`. - `alternates`: for the weather part, alternate aerodromes (ICAO codes), up to 10, with METAR and TAF at the destination ETA. - `format`: `full` (default) or `compact`. Compact keeps what a crew acts on: each part's summary, the RAIM dispatch verdict, outages and departure window, every NOTAM with its full text and its place on the route (without geometry, area or the raw message), the weather hazards without their tracks, and the airspace entered with its limits and where the aircraft is inside (the near ones counted, the TFRs without a shape named). It is made for poor connections, such as a tablet on a satellite link over an ocean. - `known`: the etags of the parts you already hold, `{"raim": "...", "notams": "...", "weather": "...", "airspace": "..."}`. A part whose etag still matches comes back as `{"etag": "...", "unchanged": true}`, so a refresh en route carries only what changed. The etag ignores when an answer was computed. ```json { "route": "KMSP DWN6.ABR J158 RAP MBW KSFO", "departure": "2026-10-14T23:00:00Z", "speed_kt": 450, "cruise_fl": 370, "window_minutes": 60, "format": "compact", "known": { "notams": "9f2c1d0e4b7a6c35" } } ``` Response (a refresh where the NOTAMs had not changed): ```json { "summary": "RAIM: RAIM is available along the whole route for the planned departure 23:00Z and for every departure from 22:00Z to 15 Oct 00:00Z. NOTAMs: 82 NOTAMs along the route, in force 14 Oct 23:00Z to 15 Oct 02:11Z: 19 crossing the corridor, 57 abeam, 6 at the route's aerodromes, 0 FIR-wide. Weather: 1436 NM, 14 23:00Z to 15 02:11Z. 1 hazard(s) affect the flight.", "raim": { "etag": "3fa1c2d9e07b6a51", "summary": "RAIM is available along the whole route ...", "dispatch": { "ok": true }, "availability_pct": 100 }, "notams": { "etag": "9f2c1d0e4b7a6c35", "unchanged": true }, "weather": { "etag": "77b0e2a19c4d5f08", "summary": "1436 NM, 14 23:00Z to 15 02:11Z ...", "hazards": [], "aerodromes": [] }, "generated_at": "2026-10-14T21:05:12Z" } ``` Each part is its route endpoint's answer (in full, or in the compact form) with `etag` added. For a route string, `resolved_route` comes once at the top, not in each part. A part that failed is `{"etag": "...", "status": 503, "error": "...", "message": "..."}`, and the summary says it is not available right now. ## Alerts (beta) `POST /v1/alerts`, `GET /v1/alerts`, `GET /v1/alerts/{id}`, `DELETE /v1/alerts/{id}` A watch on one flight that calls your webhook when what a crew acts on changes. Every 15 minutes we run the briefing pack for the flight (see Flight briefing pack) and compare it with the last check: the RAIM dispatch verdict, its outages and, with `window_minutes`, the departure window; the NOTAMs along the route (a notice added, withdrawn, cancelled, or changed in its text or validity); the weather hazards along the route (one added or gone); the airspace the flight enters (a TFR issued on the route, or one lifted, or a TFR without a shape added or gone). A watch without `raim` never runs the RAIM check, so a RAIM failure cannot hold its NOTAMs or weather back. A new almanac or a fresh METAR that changes none of that does not call. Beta. Alerts count only when something changed. Making, listing and stopping watches and the checks that find nothing new count nothing. A check that calls your webhook with a situation it has not had (the first call, or a change) counts as the briefing pack of the watched parts: one request per watched part, in its own service, on the key that made the watch, recorded as `Alert change (raim)` and so on. Today only the RAIM part is counted. A change counts once, however many times its call is tried. Once the organisation has used its allowance, a change that would count waits: `last_error` says `quota_exceeded`, and the call goes out once the allowance allows. A watch with `raim` cannot be made while the allowance is used up (429 `quota_exceeded`). A watch stops when the key that made it is revoked or expires (`last_error`: `key_revoked`). Request: the flight object (see Flight object) and: - `webhook_url` (required): an https URL on the standard port (443) whose host resolves only to public addresses. Redirects are not followed. - `events`: the parts to watch, any of `raim`, `notams`, `weather` and `airspace`. Default `["raim", "notams"]`. - `expires_at`: when the watch ends (RFC 3339), within the next 7 days. Default: two hours after the flight's last timed point, or one day for a flight without times. A flight that has already arrived is 422. - `window_minutes` (up to 120) and `profile`: for the RAIM part, as on `POST /v1/raim/route`. Anything else is 400 `unknown_field`. The watch is checked once when it is made: a flight a service refuses (for RAIM, a flight without times, an unknown profile, a window too long for the route) is refused with that service's error, and nothing is kept. An organisation may have 25 active watches at once and make 100 a day; past either, 409 `alert_limit`. ```json { "route": "KMSP DWN6.ABR J158 RAP MBW KSFO", "departure": "2026-10-14T23:00:00Z", "speed_kt": 450, "cruise_fl": 370, "window_minutes": 30, "webhook_url": "https://ops.example.com/hooks/bravoecho", "events": ["raim", "notams"] } ``` Response, 201. The `secret` signs every call to the webhook and is shown this once; keep it like a password. Lost it? Stop the watch and make a new one. ```json { "id": "alr_01k7f3x9q2m4n8p6r0s2t4v6w8", "status": "active", "events": ["raim", "notams"], "webhook_url": "https://ops.example.com/hooks/bravoecho", "expires_at": "2026-10-15T04:11:00Z", "flight": { "route": "KMSP DWN6.ABR J158 RAP MBW KSFO", "departure": "2026-10-14T23:00:00Z", "speed_kt": 450, "cruise_fl": 370 }, "options": { "window_minutes": 30 }, "created_at": "2026-10-14T20:41:07Z", "last_checked_at": null, "last_change_at": null, "last_error": null, "last_delivery": null, "deliveries": 0, "secret": "whsec_..." } ``` The first check runs within a minute and calls with `alert.started`, the situation as it stands, so the receiver can check the signature straight away. After that a call comes only when a watched part changes, as `alert.changed`. Each call is a POST with a JSON body and these headers: - `BravoEcho-Event`: `alert.started` or `alert.changed`. - `BravoEcho-Signature`: `t=,v1=`, where the hex is the HMAC-SHA256 of `.` with the watch's secret. Compute it over the raw bytes you received, compare in constant time, and refuse a `t` more than five minutes old. - `User-Agent`: `bravoecho-alerts/1 (+https://bravoecho.ai/docs/alerts)`. ```json { "id": "evt_01k7f9b2c4d6e8g0h2j4k6m8n0", "type": "alert.changed", "alert": "alr_01k7f3x9q2m4n8p6r0s2t4v6w8", "created": "2026-10-14T22:15:31Z", "changed": ["notams"], "summary": "RAIM: RAIM is available along the whole route ... NOTAMs: 83 NOTAMs along the route ...", "expires_at": "2026-10-15T04:11:00Z", "raim": { "summary": "RAIM is available along the whole route ...", "dispatch": { "ok": true }, "outages": [] }, "notams": { "summary": "83 NOTAMs along the route ...", "notams": [ { "number": "A2817/26", "icao_location": "KSFO", "category": "runway", "placement": "aerodrome", "effective_start": "2026-10-14T22:00:00Z", "text": "RWY 10L/28R CLSD" } ], "meta": { "total": 83 } } } ``` `id` is unique to each call; a call made again after a failure is a new call with its own `id` and the situation then. `changed` names the parts that changed (empty for `alert.started`); each watched part is the briefing pack's compact form of it. Checking a signature in Python: ```python import hashlib, hmac, time def verified(secret: str, header: str, raw_body: bytes) -> bool: fields = dict(part.split("=", 1) for part in header.split(",")) expected = hmac.new(secret.encode(), fields["t"].encode() + b"." + raw_body, hashlib.sha256).hexdigest() return hmac.compare_digest(expected, fields["v1"]) and abs(time.time() - int(fields["t"])) < 300 ``` Answer 2xx within 10 seconds. A call that fails (another status, a timeout, no connection) is made again at the next check with the situation then, so no change is lost while the watch is active. After 10 failures in a row the watch's status becomes `failing` and its checks stop; make a new watch once the receiver is back. A watch is `active`, `stopped` (you stopped it, or its key was revoked), `expired` (past `expires_at`) or `failing`. `GET /v1/alerts` lists the organisation's watches, newest first, at most 100. `GET /v1/alerts/{id}` is one, with `last_checked_at`, `last_change_at` (when a change was last delivered), `last_error` (why the last check could not run or a part failed: `at`, `part`, `status`, `error`, `message`; null once a check runs clean), `last_delivery` (`at` and the HTTP `status` your webhook answered, 0 when it could not be reached) and `deliveries`. `DELETE /v1/alerts/{id}` stops it and answers with the watch. The secret is never shown again. Another organisation's watch is 404. ## MCP A hosted MCP server for agents that speak the Model Context Protocol (Claude Code, Cursor, Claude Desktop and others): thirteen tools shaped around the questions dispatch and flight planning ask, over the same data, rules and keys as the REST API. - URL: `https://api.bravoecho.ai/mcp`. Streamable HTTP, stateless: each POST gets one JSON answer; there is no session and no server stream (GET is 405). - Auth: the same organisation key as `/v1`, as `Authorization: Bearer $BRAVOECHO_KEY`. A missing or invalid key is a 401 with `WWW-Authenticate: Bearer`. - What counts: each `tools/call` that reaches a tool is one request, like one `/v1` call, whatever the tool answers, and shows in the console's usage as `MCP `. Connecting (`initialize`), `tools/list`, `ping` and notifications are free and never refused for quota; so is a call naming a tool that does not exist. Only the RAIM tools (`raim_route_check`, `raim_predict`) and the RAIM part of `flight_briefing` are counted; the others are beta, recorded and free. `flight_briefing` counts one request per part it runs, as `POST /v1/briefing` does, shown as `MCP flight_briefing (raim)` and so on. At the monthly cap a RAIM tool call is refused with HTTP 429 `quota_exceeded`, as on `/v1`. The per-minute rate limit covers every request. - Flight: the route tools (and `flight_briefing`) take the whole flight object (see Flight object) exactly as `POST /v1/raim/route`, `/v1/notams/route` and `/v1/weather/route` do, so one object goes to all three unchanged: each uses what it needs, accepts the rest and refuses what the endpoint refuses, with the same error. A waypoint may also give an airport code in `id` instead of `lat` and `lon`. - Answers: a lead sentence, then compact JSON, typically 1 to 12 KB. For the route tools the lead is the endpoint's `summary`, for points a one-line verdict, for airport weather one line per airport; show it to people as it is. Times are UTC. A failure is a tool error (`isError: true`) whose text is the API's error, `{"error": code, "message": text}`. Tools: - `raim_route_check`: RAIM along a flight. `route` as filed or `waypoints`, `departure`, `speed_kt`, `window_minutes` (default 60 here: how far the departure may slip each side), `step_seconds`, `profile`, and for a route string `cruise_fl` or `cruise_alt_m` (`cruise_fl: 350` flies at 10,668 m) and `phase` or `hal_nm`; waypoints carry their level as `alt_m`, `alt_ft` or `fl`, flown in metres, and `codes` are accepted and not used. Returns the verdict, the planned flight's outages with time and place, the departure window with its clean and blocked ranges, the fixes with their times and the per-fix window failures. As `POST /v1/raim/route`. - `flight_briefing`: RAIM, NOTAMs, weather and airspace for one flight in one call, as `POST /v1/briefing` in its compact form: the same flight object as the route tools, `parts` (default all four), `window_minutes` (default 60 here) and `profile` for the RAIM part, `alternates` for the weather part. The lead is the four summaries, RAIM first; then the RAIM dispatch verdict, outages and departure window, the first 30 NOTAMs along the route with their full text and place on the route (`notams_omitted` counts the rest; `notams_along_route` filters and lists more), the weather hazards and aerodromes, and the airspace entered (the first 25; `airspace_along_route` lists more). Each part counts as one request of its service; today only the RAIM part is counted. - `raim_predict`: RAIM at 1 to 100 points, `window_minutes` (default 60) each side of each point's time. A point may give an airport code in `id` instead of `lat` and `lon`. As `POST /v1/raim/predict`. - `notams_along_route`: NOTAMs along a route string or waypoints, in force between `from` and `to`, or from `departure` to the arrival at `speed_kt`, with an optional flight level band (`fl_from`, `fl_to`), `categories` and `q`. Without `from` and `to`, waypoint times or `departure` and `speed_kt` give the window. Levels (`alt_m`, `alt_ft`, `fl`, `cruise_fl`, `cruise_alt_m`) and RAIM's `phase` and `hal_nm` are accepted and not used. The lead is the API's `summary`, then what the list shows. Lists 25 by default (`limit` up to 100): the ones placed on the track and airspace, hazard and runway notices first, in order along the track with placement and chainage, text cut to its start; the counts cover every match. As `POST /v1/notams/route`. - `notams_search`: NOTAMs by `location`, `fir`, `bbox`, `lat`/`lon`/`radius_nm`, `number`, `q` or `categories`, in force between `from` and `to` or the live set, newest first. A search by `number` returns the whole text. As `GET /v1/notams`. - `weather_along_route`: weather along a flight: `route` as filed or `waypoints`, `departure`, `speed_kt`, the cruise level (`cruise_fl` or `cruise_alt_m`) for a route string, `alternates`, and optionally `half_width_nm`, `pireps`, `as_of`. RAIM's `phase` and `hal_nm`, at the top level or on waypoints, are accepted and not used. A waypoint may give an airport code in `id` instead of `lat` and `lon`: it is that aerodrome, and as the first or last point it sits at field elevation when another point has a level. The lead is the API's `summary`; then the hazards that affect the flight with their spans in NM, time and level, the ones in the corridor that are clear and why, the count of unplaced ones, the aerodromes (METAR and TAF text and categories for the time the aircraft is there), pilot reports, the flight as placed, the route's notes and the archive's caveats. Long lists are cut with a count of what was left out. As `POST /v1/weather/route`. - `weather`: METAR and TAF for 1 to 20 airports (ICAO, IATA or FAA codes), the latest or at a time (`at`: the latest METAR observed at or before it and the TAF in force then). One line per airport, then per airport the decoded METAR and TAF (with its worst group, over its validity or at `at`) and their text, the codes with nothing found or read as another station, and the archive's caveats. As `GET /v1/weather/metar` and `GET /v1/weather/taf`, in one call and one request. - `winds_along_route`: the wind and temperature at the flight's levels and times (United States, Alaska, Hawaii, the Pacific; FB forecasts): the same flight object, `step_nm` (default 50 here), `tas_kt` and `as_of`. The lead is the API's `summary`; then the totals (average headwind component, strongest wind, ISA deviation, with tas_kt the times), the legs from fix to fix, the samples along the track (up to 60), the forecasts used and the caveats. As `POST /v1/weather/winds/route`. - `airspace_along_route`: the special use airspace, Class B, C and D airspace and TFRs a flight enters (United States): the same flight object as the other route tools, `half_width_nm` (default 5) and `kinds`. The lead is the API's `summary`; then the airspaces entered in order along the track with their limits as published and the stretches where the aircraft is inside (NM, times, levels), times of use and, for a TFR, its NOTAM number, rule, effective times and text; the ones near the track with why the flight stays out (`clear_by`); TFRs with no published shape, each with its `scope` (an ARTCC the route passes through, or national); the flight as placed; the sources and caveats. As `POST /v1/airspace/route`. - `mass_and_balance`: weight and centre of gravity at ramp, takeoff, landing and zero fuel from the aircraft data you give, checked against its limits and envelopes. As `POST /v1/planning/mass-balance`, the same request; the lead is its summary. - `navlog`: the legs of a flight with course and heading, true and magnetic, wind, ground speed, time, fuel and ETA, and the fuel plan, from the aircraft performance you give and the winds aloft. As `POST /v1/planning/navlog`, the same request; the lead is its summary. - `airport`: one airport by ICAO, IATA, GPS or local code, with runways and frequencies. As `GET /v1/airports/{code}`. - `resolve_route`: the points a route string stands for, with its notes and the AIRAC cycle used. As `GET /v1/routes/resolve`. Claude Code: ``` claude mcp add --transport http bravoecho https://api.bravoecho.ai/mcp --header "Authorization: Bearer $BRAVOECHO_KEY" ``` Cursor (`~/.cursor/mcp.json`) and other clients whose configuration takes a URL and headers: ```json { "mcpServers": { "bravoecho": { "url": "https://api.bravoecho.ai/mcp", "headers": { "Authorization": "Bearer " } } } } ``` Claude Desktop (`claude_desktop_config.json`), through the `mcp-remote` bridge (needs Node.js): ```json { "mcpServers": { "bravoecho": { "command": "npx", "args": ["-y", "mcp-remote", "https://api.bravoecho.ai/mcp", "--header", "Authorization:${AUTH_HEADER}"], "env": { "AUTH_HEADER": "Bearer " } } } } ``` Keep the key in the client's configuration or environment, never in a repository. ## What to build 1. A small client module in the project's language, in server-side code: one function per endpoint, key from the environment or the platform's secrets, base URL configurable, timeouts set, 429 handled with `Retry-After`. Never call the API from a browser. 2. A smoke test that calls `/up`, one real predict call with a single point, one `GET /v1/airports/KJFK`, one `GET /v1/notams?location=KJFK` and one `GET /v1/weather/metar?ids=KJFK`. 3. Show the human a working call before wiring it into their product. 4. Do not invent fields that are not in this file. If something is missing, tell the human to request it at https://bravoecho.ai/request-a-service or email hello@bravoecho.ai. ## Planned (not callable) Not built yet: no endpoint, no MCP tool, no field. Do not write code against them, and do not tell people they exist. - More weather hazard types, starting with space weather advisories, and winds aloft from a gridded model outside the United States. ## Links - Site: https://bravoecho.ai - Index: https://bravoecho.ai/llms.txt - Status: https://uptime.bravoecho.ai/status/all - OpenAPI: https://bravoecho.ai/openapi.json - RAIM world map: https://bravoecho.ai/raim-map (public, no key: yesterday, today and tomorrow at 0.1 and 0.3 NM, updated at 00, 06, 12 and 18 UTC, built on `POST /v1/raim/map`); as JSON: https://bravoecho.ai/raim-map/maps.json; the current image: https://bravoecho.ai/raim-map/image/today/0.1.png (or yesterday, tomorrow, 0.3) - Contact: hello@bravoecho.ai; support: support@bravoecho.ai; sales: sales@bravoecho.ai - Request a service: https://bravoecho.ai/request-a-service