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 ofraim,notams,weatherandairspace. 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) andprofile: for the RAIM part, as onPOST /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.
{
"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.
{
"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.startedoralert.changed.BravoEcho-Signature:t=<unix time>,v1=<hex>, where the hex is the HMAC-SHA256 of<t>.<raw body>with the watch's secret. Compute it over the raw bytes you received, compare in constant time, and refuse atmore than five minutes old.User-Agent:bravoecho-alerts/1 (+https://bravoecho.ai/docs/alerts).
{
"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:
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.