NATS NOTAM API Documentation¶
Overview¶
The drone operations management system integrates with the NATS Public Information Bulletin (PIB) XML feed to automatically fetch and filter NOTAMs (Notices to Airmen) relevant to unmanned aircraft operations. This provides critical airspace activity information for pre-flight planning and regulatory compliance.
Purpose in Drone Operations: - Pre-flight airspace awareness and hazard identification - Identification of temporary flight restrictions and controlled airspace - UAS-specific NOTAM filtering (Code23=WU or "UAS" mentions) - Regulatory compliance documentation for flight planning - Risk analysis input for operational safety assessment
Integration Architecture: - Data Source: NATS PIB XML feed (public, no API key required) - Caching Strategy: 24-hour cache stored in database (Project.notam_data JSON column) - Filtering: Geographic (Haversine distance), UAS relevance, validity date - Coordinate Systems: DDMMSS → WGS84 decimal degrees conversion - Update Frequency: XML file updated continuously by NATS
Service Overview¶
Provider Information¶
- Provider: NATS (National Air Traffic Services) - UK
- Service: Public Information Bulletin (PIB)
- Cost: Free, public access, no API key required
- Data Format: XML
- Update Frequency: Real-time (XML regenerated continuously)
- Coverage: UK and adjacent airspace
- Reliability: High (national ATC provider)
Data Source¶
https://pibs.nats.co.uk/operational/pibs/PIB.xml
XML File Characteristics: - Size: ~2-5 MB (varies with active NOTAM count) - Encoding: UTF-8 - Structure: Nested XML (FIRSection → ADSection → NotamList → Notam) - Update Cadence: Continuous (fetched on-demand by application)
Rate Limits¶
- No formal rate limits documented
- Best Practice: Use 24-hour caching to minimise load
- Timeout: 30-second request timeout (large XML file)
API Integration¶
Endpoint¶
GET https://pibs.nats.co.uk/operational/pibs/PIB.xml
Request Method¶
- HTTP Method: GET
- Authentication: None required (public endpoint)
- Headers: None required
- Parameters: None (entire XML file is downloaded)
Request Example¶
import requests
url = "https://pibs.nats.co.uk/operational/pibs/PIB.xml"
timeout = 30 # seconds
response = requests.get(url, timeout=timeout)
response.raise_for_status()
xml_content = response.content
Python Integration (Using NOTAMAPI Class)¶
from app.utils.notam_api import NOTAMAPI
# Fetch NOTAMs for Edinburgh
latitude = 55.9533
longitude = -3.1883
flight_date = '2026-01-10'
result = NOTAMAPI.fetch_notams(latitude, longitude, flight_date)
if result['success']:
notam_data = result['notam_data']
metadata = result['metadata']
print(f"Found {notam_data['count']} relevant NOTAMs")
print(f"Total NOTAMs parsed: {metadata['total_notams_parsed']}")
print(f"Sources filtered: {metadata['filtered_count']}")
for notam in notam_data['notams']:
print(f"\nNOTAM {notam['nof']}:")
print(f" Distance: {notam['distance_from_location_nm']} NM")
print(f" Description: {notam['item_e'][:100]}...")
else:
print(f"Error: {result['error']}")
XML Structure¶
Document Hierarchy¶
<Pib>
<FIRSection>
<ADSection>
<NotamList>
<Notam>
<Series>A</Series>
<Number>7668</Number>
<Year>25</Year>
<NOF>A7668/25</NOF>
<ItemE>Description of NOTAM...</ItemE>
<ItemF>Lower altitude limit</ItemF>
<ItemG>Upper altitude limit</ItemG>
<QLine>
<Code23>WU</Code23>
</QLine>
<Coordinates>5408N00316W</Coordinates>
<Radius>5</Radius>
<StartValidity>2512290700</StartValidity>
<EndValidity>2601101800</EndValidity>
</Notam>
</NotamList>
</ADSection>
</FIRSection>
</Pib>
Key XML Elements¶
| Element | Description | Example | Format |
|---|---|---|---|
| Series | NOTAM series letter | A |
Single letter (A-Z) |
| Number | NOTAM sequence number | 7668 |
Numeric |
| Year | Year of issue (2-digit) | 25 |
YY |
| NOF | NOTAM identifier | A7668/25 |
{Series}{Number}/{Year} |
| ItemE | NOTAM description | UNMANNED ACFT WI 5NM RADIUS... |
Free text |
| ItemF | Lower altitude limit | SFC or 1000FT AGL |
ICAO format |
| ItemG | Upper altitude limit | 1500FT AGL |
ICAO format |
| Code23 | Subject/activity code | WU |
2-letter code |
| Coordinates | Centre point | 5408N00316W |
DDMMSS format |
| Radius | Radius in nautical miles | 5 |
Decimal |
| StartValidity | Start date/time (UTC) | 2512290700 |
YYMMDDHHmm |
| EndValidity | End date/time (UTC) | 2601101800 |
YYMMDDHHmm |
Code23 Values (Relevant to UAS)¶
| Code | Meaning | UAS Relevance |
|---|---|---|
| WU | UAS Activity | High - Specific to unmanned aircraft operations |
| WP | Manned aircraft prohibited flight | High - may restrict UAS as well |
| WR | Restricted airspace | High - may affect UAS operations |
| WD | Danger area | Medium - assess case-by-case |
| WT | Military/training exercise | Medium - check altitude and times |
Full code list: https://www.icao.int/safety/OPS/OPS-Tools/Pages/NOTAM-Decode.aspx
Coordinate Format Conversion¶
DDMMSS Format¶
NOTAMs use DDMMSS (Degrees Minutes Seconds) format with hemisphere indicators:
Format Pattern:
Latitude: DDMMSS[N|S]
Longitude: DDDMMSS[E|W]
Combined: DDMMSS[N|S]DDDMMSS[E|W]
Example: 5408N00316W
- 5408N = 54 degrees, 08 minutes North = 54.133°N
- 00316W = 003 degrees, 16 minutes West = 3.267°W
- Decimal: (54.133, -3.267)
Alternative DDMM Format¶
Some NOTAMs use simplified DDMM format (no seconds):
Example: 5408N00316W could also be:
- 5408N = 54 degrees, 08 minutes (00 seconds assumed)
- 00316W = 003 degrees, 16 minutes (00 seconds assumed)
Conversion Formula¶
# Latitude (DDMMSS format)
degrees = int(lat_str[:2]) # First 2 digits
minutes = int(lat_str[2:4]) # Next 2 digits
seconds = int(lat_str[4:6]) # Last 2 digits (0 if DDMM format)
decimal_degrees = degrees + (minutes / 60.0) + (seconds / 3600.0)
# Apply hemisphere
if hemisphere == 'S' or hemisphere == 'W':
decimal_degrees = -decimal_degrees
Implementation: - Automatic detection of DDMMSS vs DDMM format (by string length) - Handles both latitude (4-6 digits) and longitude (5-7 digits) - Validation of hemisphere indicators (N/S for lat, E/W for lon)
Filtering Strategy¶
Three-Stage Filtering Process¶
The NOTAMAPI applies three sequential filters to reduce NOTAMs from ~500+ to typically 0-10 relevant items:
All NOTAMs (500+)
│
▼
┌──────────────────────┐
│ 1. Geographic Filter │ → Within NOTAM radius?
└──────────┬───────────┘
│ (~50-100 NOTAMs)
▼
┌──────────────────────┐
│ 2. UAS Relevance │ → Code23=WU or "UAS" in description?
└──────────┬───────────┘
│ (~10-30 NOTAMs)
▼
┌──────────────────────┐
│ 3. Validity Date │ → Active on flight date?
└──────────┬───────────┘
│
▼
Final NOTAMs (0-10)
1. Geographic Filtering¶
Algorithm: Haversine distance calculation (great-circle distance)
Logic:
distance = haversine(flight_location, notam_center)
if distance <= notam_radius:
include_notam()
Parameters: - Flight Location: User-provided lat/lon (WGS84 decimal degrees) - NOTAM Centre: Parsed from Coordinates element (DDMMSS → decimal) - NOTAM Radius: Extracted from Radius element (nautical miles)
Implementation Details:
- Uses Earth radius = 3440.065 nautical miles
- Accounts for Earth's spherical geometry (not flat distance)
- Distance stored in NOTAM dict as distance_from_location_nm
Example:
Flight Location: Edinburgh (55.9533°N, 3.1883°W)
NOTAM Centre: Glasgow (55.8642°N, 4.2518°W)
Distance: ~35 NM
NOTAM Radius: 50 NM
Result: ✓ Included (35 < 50)
NOTAM Radius: 20 NM
Result: ✗ Excluded (35 > 20)
2. UAS Relevance Filtering¶
Criteria (OR logic): 1. Code23 = 'WU' (UAS-specific NOTAM code) 2. ItemE contains 'UAS' (unmanned aircraft mentioned in description)
Rationale: - Not all UAS-relevant NOTAMs have Code23=WU - Some general airspace restrictions mention UAS in ItemE - Conservative approach: include if either condition matches
Example Inclusions:
<!-- Included: Code23=WU -->
<Notam>
<QLine><Code23>WU</Code23></QLine>
<ItemE>UNMANNED ACFT FLYING DISPLAY</ItemE>
</Notam>
<!-- Included: "UAS" in description -->
<Notam>
<QLine><Code23>WR</Code23></QLine>
<ItemE>RESTRICTED AREA. UAS OPERATIONS PROHIBITED.</ItemE>
</Notam>
Example Exclusions:
<!-- Excluded: Not UAS-related -->
<Notam>
<QLine><Code23>WT</Code23></QLine>
<ItemE>MILITARY TRAINING EXERCISE. MANNED ACFT ONLY.</ItemE>
</Notam>
3. Validity Date Filtering¶
Algorithm: Date range intersection
Logic:
if start_validity <= flight_date <= end_validity:
include_notam()
Date Parsing:
- Input Format: YYMMDDHHmm (e.g., 2512290700 = 29 Dec 2025, 07:00 UTC)
- Flight Date: YYYY-MM-DD (e.g., 2026-01-10)
- Comparison: Date-only (time component ignored for day-level granularity)
Edge Cases: - Missing start/end dates → Excluded - Invalid date formats → Logged and excluded - Permanent NOTAMs (very distant end dates) → Included if active
Example:
Flight Date: 2026-01-10
NOTAM 1:
Start: 2025-12-29 07:00 UTC
End: 2026-01-10 18:00 UTC
Result: ✓ Included (intersects flight date)
NOTAM 2:
Start: 2026-01-11 00:00 UTC
End: 2026-01-15 23:59 UTC
Result: ✗ Excluded (starts after flight date)
Response Format¶
Successful Response¶
{
"success": true,
"notam_data": {
"has_notams": true,
"count": 2,
"notams": [
{
"nof": "A7668/25",
"item_e": "UNMANNED ACFT FLYING DISPLAY WI 5NM RADIUS 540800N 0031600W (EDINBURGH CASTLE). FOR INFO CONTACT 0131 123 4567.",
"item_f": "SFC",
"item_g": "1500FT AGL",
"code23": "WU",
"center_lat": 54.133333,
"center_lon": -3.266667,
"radius": 5.0,
"start_validity": "2025-12-29T07:00:00+00:00",
"end_validity": "2026-01-10T18:00:00+00:00",
"distance_from_location_nm": 2.3
},
{
"nof": "A7669/25",
"item_e": "RESTRICTED AIRSPACE. UAS OPERATIONS REQUIRE ATC PERMISSION.",
"item_f": "SFC",
"item_g": "2000FT AMSL",
"code23": "WR",
"center_lat": 54.150000,
"center_lon": -3.300000,
"radius": 10.0,
"start_validity": "2025-12-01T00:00:00+00:00",
"end_validity": "2026-02-28T23:59:00+00:00",
"distance_from_location_nm": 4.7
}
]
},
"metadata": {
"fetched_at": "2026-01-02T14:30:00+00:00",
"cache_valid_until": "2026-01-03T14:30:00+00:00",
"xml_size_kb": 0,
"total_notams_parsed": 523,
"filtered_count": 2
},
"error": null
}
Error Response¶
{
"success": false,
"notam_data": {
"has_notams": false,
"count": 0,
"notams": []
},
"metadata": {
"fetched_at": null,
"cache_valid_until": null,
"xml_size_kb": 0,
"total_notams_parsed": 0,
"filtered_count": 0
},
"error": "Request timeout - NATS PIB server did not respond"
}
Response Fields¶
| Field | Type | Description |
|---|---|---|
| success | boolean | True if fetch succeeded, false if error |
| notam_data.has_notams | boolean | True if any NOTAMs found after filtering |
| notam_data.count | integer | Number of relevant NOTAMs |
| notam_data.notams | array | List of NOTAM objects |
| notam.nof | string | NOTAM identifier (e.g., "A7668/25") |
| notam.item_e | string | NOTAM description/details |
| notam.item_f | string | Lower altitude limit (ICAO format) |
| notam.item_g | string | Upper altitude limit (ICAO format) |
| notam.code23 | string | Subject/activity code (e.g., "WU") |
| notam.center_lat | float | NOTAM centre latitude (decimal degrees) |
| notam.center_lon | float | NOTAM centre longitude (decimal degrees) |
| notam.radius | float | Affected radius (nautical miles) |
| notam.start_validity | string | Start date/time (ISO 8601 UTC) |
| notam.end_validity | string | End date/time (ISO 8601 UTC) |
| notam.distance_from_location_nm | float | Distance from flight location (NM, 1 decimal) |
| metadata.fetched_at | string | Timestamp of API call (ISO 8601 UTC) |
| metadata.cache_valid_until | string | Cache expiry timestamp (ISO 8601 UTC) |
| metadata.total_notams_parsed | integer | Total NOTAMs in XML file |
| metadata.filtered_count | integer | NOTAMs after all filters |
| error | string/null | Error message if success=false |
Caching Strategy¶
Cache Implementation¶
Storage Location:
- Database Column: Project.notam_data (JSON field)
- Cache Duration: 24 hours (configurable via NOTAM_CACHE_HOURS env var)
- Validation: NOTAMAPI.is_cache_valid() method
Cache Lifecycle¶
# On project creation
notam_response = NOTAMAPI.fetch_notams(lat, lon, date)
project.notam_data = notam_response # Store in database
# On project view (automatic cache refresh)
if not NOTAMAPI.is_cache_valid(project.notam_data):
# Cache expired - refetch
notam_response = NOTAMAPI.fetch_notams(lat, lon, date)
project.notam_data = notam_response
db.session.commit()
Cache Validation Logic¶
def is_cache_valid(notam_data_json):
if not notam_data_json:
return False
cache_expiry = notam_data_json['metadata']['cache_valid_until']
cache_expiry_dt = datetime.fromisoformat(cache_expiry)
now_utc = datetime.utcnow()
return now_utc < cache_expiry_dt
Cache Expiry Reasons:
1. Time-based: 24 hours elapsed since fetched_at
2. Missing metadata: Cache structure invalid/corrupt
3. Null data: notam_data field is None
Cache Benefits¶
- Performance: Avoids 30-second XML download on every page load
- API Courtesy: Reduces load on NATS servers
- User Experience: Fast page loads with cached data
- Data Freshness: 24-hour window balances freshness vs performance
Error Handling¶
Error Types¶
| Error Type | Cause | Response | User Impact |
|---|---|---|---|
| Timeout | XML download >30s | success=false, error message | No NOTAMs displayed |
| Network Error | Connection failed, DNS error | success=false, error message | No NOTAMs displayed |
| HTTP Error | 404, 500, 503 response | success=false, error message | No NOTAMs displayed |
| XML Parse Error | Malformed XML | success=false, error message | No NOTAMs displayed |
| Coordinate Parse Error | Invalid DDMMSS format | NOTAM skipped (logged) | Individual NOTAM excluded |
| Date Parse Error | Invalid YYMMDDHHmm format | NOTAM skipped (logged) | Individual NOTAM excluded |
Error Handling Strategy¶
try:
result = NOTAMAPI.fetch_notams(lat, lon, date)
if result['success']:
# Use NOTAM data
notams = result['notam_data']['notams']
if result['notam_data']['has_notams']:
print(f"⚠ {len(notams)} active NOTAMs found")
else:
print("✓ No active NOTAMs for this location/date")
else:
# Handle error gracefully
print(f"NOTAM fetch failed: {result['error']}")
print("Continuing without NOTAM data (check manually)")
except Exception as e:
# Unexpected error (should not happen in production)
logger.error(f"Unexpected NOTAM API error: {e}")
Logging Levels¶
- INFO: Successful fetches, cache hits, filter statistics
- WARNING: Individual NOTAM parse failures, cache validation failures
- ERROR: API timeouts, network errors, XML parse errors
- DEBUG: Detailed filtering stats, coordinate parsing details
Fallback Behaviour¶
If NOTAM fetch fails: 1. System continues normal operation (non-blocking) 2. User informed via flash message or UI indicator 3. Project can still be created (NOTAMs optional for system) 4. User advised to check NOTAMs manually via official sources
Official NOTAM Sources: - NATS PIB: https://pibs.nats.co.uk/ - Drone Safe UK: https://dronesafe.uk/
Implementation Notes¶
Timeout Configuration¶
Why 30 seconds? - PIB XML file is 2-5 MB (large) - Download can take 10-20 seconds on slow connections - 30s provides buffer while preventing indefinite hangs
Configurable:
NOTAMAPI.API_TIMEOUT = 30 # seconds (default)
Performance Characteristics¶
Typical Operation: - XML Download: 10-20 seconds (depends on network) - XML Parsing: 1-2 seconds (~500 NOTAMs) - Filtering: <1 second (all three filters) - Total Time: ~12-23 seconds (first fetch) - Cached Fetch: <100ms (database lookup only)
Optimization Opportunities: - Consider XML streaming parser for very large files - Parallel processing of NOTAM parsing (currently sequential) - Incremental updates (if NATS provides delta feed in future)
Coordinate Accuracy¶
Haversine Formula Accuracy: - Error: <0.5% for distances <1000 NM - Sufficient: For NOTAM radius checks (typically 5-50 NM) - Alternative: Vincenty formula (more accurate but slower)
DDMMSS Parsing Precision: - Seconds Resolution: ~30 meters latitude, ~20 meters longitude (at UK latitudes) - Sufficient: NOTAMs use kilometer-scale radii
UAS-Specific Considerations¶
Code23=WU Adoption: - Increasing: More NOTAMs using WU code for UAS operations - Not Universal: Many UAS-relevant NOTAMs still use generic codes (WR, WD) - Filter Logic: Intentionally broad (includes "UAS" text mentions)
Altitude Filtering: - Not Implemented: ItemF/ItemG (altitude limits) not currently filtered - Rationale: Most UAS operations <400ft AGL, NOTAMs often specify "SFC to X" - Future Enhancement: Could add altitude-based filtering if needed
References¶
Official Documentation¶
- NATS PIB: https://pibs.nats.co.uk/
- ICAO NOTAM Codes: https://www.icao.int/safety/OPS/OPS-Tools/Pages/NOTAM-Decode.aspx
- UK AIP (Aeronautical Information Publication): https://www.aurora.nats.co.uk/htmlAIP/
Related Documentation¶
- NOTAM Integration Design - Architecture and implementation strategy
- Project Model Schema - Database schema including notam_data column
External Resources¶
- Haversine Formula: https://en.wikipedia.org/wiki/Haversine_formula
- ICAO NOTAM Format: https://www.faa.gov/air_traffic/publications/atpubs/notam_html/
- WGS84 Coordinate System: https://en.wikipedia.org/wiki/World_Geodetic_System
Aviation Authority Resources¶
- UK CAA Drone Registration: https://register-drones.caa.co.uk/
- Drone Safe UK: https://dronesafe.uk/
- NATS Drone Information: https://www.nats.aero/ae-home/drones/