Skip to content

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/

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/