The Notamify Python SDK is an open-source (MIT) library that streamlines NOTAM data fetching, listener management, and real-time webhook handling for the Notamify API v2 and Watcher API.
The Notamify Python SDK is an open-source, MIT-licensed Python client that wraps both the Notamify API v2 and the Notamify Watcher API into a single, typed interface. It is designed to streamline NOTAM data fetching, eliminate boilerplate HTTP code, and make the Watcher API accessible with just a few lines of Python.
Key highlights:
Single NotamifyClient for both NOTAM queries and Watcher listener management
Fully typed with Pydantic v2 models — autocomplete and validation out of the box
Auto-paginating iterators for NOTAM endpoints
Built-in webhook signature verification
Embedded webhook receiver for local development and testing
Cloudflare tunnel integration for exposing local endpoints
Zero external HTTP dependencies (uses only urllib.request)
The SDK supports multiple ways to provide your API token:
Fetching NOTAMs
The SDK provides typed query models for each endpoint and two ways to fetch data: single-page methods and auto-paginating iterators.
Active NOTAMs
Nearby NOTAMs
Raw NOTAMs
Raw NOTAMs are returned without AI interpretation — useful for prefetching before pulling interpretations:
Historical NOTAMs
Async Briefings
The SDK wraps the two-step async briefing workflow:
Watcher API: Real-Time NOTAM Monitoring
The Watcher API is the push-based counterpart to the query endpoints. Instead of polling for NOTAMs, you register listeners with filters, and Notamify delivers matching NOTAMs to your webhook URL in real time.
The SDK makes this entire workflow — creating listeners, receiving webhooks, verifying signatures — straightforward.
Managing Listeners
Listener Filters
Filters control which NOTAMs are delivered to your webhook. Filters use OR logic within a field and AND logic across fields:
Lifecycle Events
Enable lifecycle tracking to receive notifications when NOTAMs you've already been delivered are cancelled or replaced:
When a previously delivered NOTAM is replaced or cancelled, Watcher sends a lifecycle event with a change object pointing to the original NOTAM. Lifecycle events are free — they do not consume credits.
Sandbox Testing
Test your webhook integration without waiting for real NOTAMs:
Webhook Secret Management
Receiving Webhooks
The SDK includes two tools for handling incoming webhook deliveries: signature verification and an embedded receiver for local development.
Signature Verification
Every webhook request from Notamify includes an X-Notamify-Signature header. Use the SDK to verify it:
Parsing Webhook Events
Local Development with Cloudflare Tunnels
To receive webhooks on your local machine, the SDK integrates with cloudflared to create a public tunnel:
from notamify_sdk import NotamifyClient, ConfigStore
# Option 1: Pass the token directly
client = NotamifyClient(token="YOUR_API_KEY")
# Option 2: Load from environment variable or config file
cfg = ConfigStore().load()
client = NotamifyClient(token=cfg.token)
from datetime import datetime, timedelta
from notamify_sdk import NotamifyClient, ActiveNotamsQuery
client = NotamifyClient(token="YOUR_API_KEY")
query = ActiveNotamsQuery(
location=["KJFK", "KLAX"],
starts_at=datetime.now(),
ends_at=datetime.now() + timedelta(days=1)
)
# Auto-paginated — iterates all pages automatically
for notam in client.notams.active(query):
print(f"{notam.notam_number} [{notam.interpretation.excerpt}]")
from notamify_sdk import NotamifyClient, NearbyNotamsQuery
client = NotamifyClient(token="YOUR_API_KEY")
query = NearbyNotamsQuery(lat=51.4775, lon=-0.4614, radius_nm=15.0)
for notam in client.notams.nearby(query):
print(f"{notam.notam_number}: {notam.interpretation.excerpt}")
from notamify_sdk import NotamifyClient, ActiveNotamsQuery
client = NotamifyClient(token="YOUR_API_KEY")
result = client.get_raw_notams(ActiveNotamsQuery(location=["EDDF"]))
for notam in result.notams:
print(notam.icao_message)
from datetime import date
from notamify_sdk import NotamifyClient, HistoricalNotamsQuery
client = NotamifyClient(token="YOUR_API_KEY")
query = HistoricalNotamsQuery(
location=["EDDM"],
valid_at=date(2024, 9, 20),
)
for notam in client.notams.historical(query):
print(f"{notam.notam_number}: {notam.message}")
from notamify_sdk import NotamifyClient
client = NotamifyClient(token="YOUR_API_KEY")
# List existing listeners
for listener in client.list_listeners():
print(f"{listener.name}: {listener.id} (active={listener.active})")
# Create a new listener
listener = client.create_listener(
name="European Hub Monitor",
webhook_url="https://your-server.com/webhooks/notamify",
filters={
"notam_icao": ["EDDF", "EDDM", "EGLL", "LFPG"],
"category": ["RUNWAY", "OBSTACLES", "AIRSPACE"],
},
lifecycle_enabled=True,
mode="prod",
)
print(f"Created: {listener.id}")
# Update filters
client.update_listener(
listener.id,
filters={
"notam_icao": ["EDDF", "EDDM", "EGLL", "LFPG", "EHAM"],
"category": ["all"],
},
)
# Pause a listener
client.update_listener(listener.id, active=False)
# Delete a listener
client.delete_listener(listener.id)
from notamify_sdk import ListenerFilters
filters = ListenerFilters(
notam_icao=["KJFK", "KEWR", "KLGA"], # OR — any of these airports
category=["OBSTACLES"], # OR — either category
subcategory=["CRANE_OPERATIONS"], # AND with category
affected_element=[
{"type": "RUNWAY", "effect": "RESTRICTED"} # Match restricted Runway
],
)
listener = client.create_listener(
name="NYC Area Runway & Obstacles",
webhook_url="https://your-server.com/webhooks",
filters=filters,
mode="prod",
)
# Switch a listener to sandbox mode
sandbox_listener = client.create_listener(
name="Test Listener",
webhook_url="https://your-server.com/webhooks",
filters={"notam_icao": ["KJFK"]},
mode="sandbox",
)
# Trigger a sandbox delivery with a specific NOTAM ID
result = client.send_sandbox_message(
sandbox_listener.id,
notam_id="some-notam-uuid",
)
print(f"Sandbox delivery: {result}")
# View your current (masked) webhook secret
masked = client.get_webhook_secret_masked()
print(f"Current secret: {masked}")
# Rotate the secret — previous key stays active for 3 hours
new_secret = client.rotate_webhook_secret()
print(f"New secret: {new_secret}")
from notamify_sdk import WatcherWebhookEvent
event = WatcherWebhookEvent.model_validate_json(raw_body)
if event.kind == "interpretation":
notam = event.notam
print(f"New NOTAM: {notam.notam_number}")
print(f" Airport: {notam.icao_code}")
print(f" Category: {notam.interpretation.category}")
print(f" Description: {notam.interpretation.description}")
# Access map elements for GeoJSON geometry
for element in (notam.interpretation.map_elements or []):
print(f" Map element: {element.element_type}")
if element.geojson:
print(f" GeoJSON: {element.geojson}")
elif event.kind == "lifecycle":
print(f"Lifecycle event for NOTAM: {event.notam.notam_number}")
print(f" Change type: {event.change.notam_type}") # "C" or "R"
print(f" Original NOTAM ID: {event.change.changed_notam_id}")
# Access airport context metadata
if event.context:
loc = event.context.location
print(f" Location: {loc.name} ({loc.icao}), {loc.iso_country_name}")
print(f" Coordinates: {loc.coordinates.lat}, {loc.coordinates.lon}")
import time
from notamify_sdk import (
APIError,
CloudflaredManager,
NotamifyClient,
ReceiverConfig,
WebhookReceiver,
)
receiver = WebhookReceiver(
ReceiverConfig(
host="127.0.0.1",
port=8080,
path="/webhooks/notamify",
allow_unsigned_dev=True,
),
on_event=lambda e: print(e.parse_webhook_event()),
)
receiver.start()
tunnel = CloudflaredManager(local_url="http://127.0.0.1:8080")
info = tunnel.start(timeout_seconds=20)
print(f"Public URL: {info.public_url}")
client = NotamifyClient(token="YOUR_API_KEY")
listener = client.create_listener(
name="Local Dev",
webhook_url=f"{info.public_url}/webhooks/notamify",
filters={"notam_icao": ["KJFK"]},
mode="sandbox",
lifecycle={"enabled": False},
)
try:
# Fresh trycloudflare.com quick tunnels can need a short DNS propagation delay.
for attempt in range(15):
try:
client.send_sandbox_message(listener.id)
break
except APIError as exc:
msg = (exc.message or "").lower()
retryable_dns_error = (
exc.status >= 500
and (
"no such host" in msg
or "temporary failure in name resolution" in msg
or ("dial tcp" in msg and "lookup" in msg)
)
)
if not retryable_dns_error or attempt == 14:
raise
time.sleep(4)
finally:
client.delete_listener(listener.id)
tunnel.stop()
receiver.stop()
from notamify_sdk import NotamifyClient, APIError
client = NotamifyClient(token="YOUR_API_KEY")
try:
result = client.get_active_notams({"location": ["KJFK"]})
except APIError as e:
print(f"HTTP {e.status}: {e.message}")
# e.status — HTTP status code (0 for connection errors)
# e.message — error description
# e.payload — raw error payload from the API
from notamify_sdk import (
NotamifyClient,
WebhookReceiver,
ReceiverConfig,
WatcherWebhookEvent,
ConfigStore,
)
# Load config from environment
cfg = ConfigStore().load()
client = NotamifyClient(token=cfg.token)
def handle_event(raw_event):
event = raw_event.parse_webhook_event()
if event.kind == "interpretation":
notam = event.notam
interp = notam.interpretation
print(f"[NEW] {notam.notam_number} at {notam.icao_code}")
print(f" Category: {interp.category}/{interp.subcategory}")
print(f" {interp.description}")
for elem in interp.affected_elements:
print(f" Affected: {elem.type} {elem.identifier} ({elem.effect})")
elif event.kind == "lifecycle":
change_type = "REPLACED" if event.change.notam_type == "R" else "CANCELLED"
print(f"[{change_type}] {event.notam.notam_number}")
print(f" Original NOTAM: {event.change.changed_notam_id}")
# Ensure listener exists
listeners = client.list_listeners()
if not any(l.name == "Production Service" for l in listeners):
client.create_listener(
name="Production Service",
webhook_url="https://your-server.com/webhooks/notamify",
filters={"notam_icao": ["KJFK", "KLAX", "EGLL"], "category": ["all"]},
lifecycle_enabled=True,
mode="prod",
)
# Start receiver
receiver = WebhookReceiver(
ReceiverConfig(
host="0.0.0.0",
port=8080,
path="/webhooks/notamify",
secret=cfg.webhook_secret,
require_signature=True,
),
on_event=handle_event,
)
receiver.start()
print("Watcher service running on :8080")
# Keep alive
import signal
signal.pause()