The FAA is partway through replacing the systems that distribute every NOTAM in US airspace. Most of the coverage so far has been written for pilots, who mainly need to know that their EFB still works. This guide is for developers who consume NOTAM data programmatically and need to know what's changing underneath them, what the new API looks like, and how to migrate without losing notices along the way.

One caveat first, because dates in this project have moved before. Everything below reflects public reporting up to late September 2026. Treat the FAA's own NOTAM Management Service page as the source of truth for current status, and re-check it before you schedule a cutover.

What's changing, and when

The modernisation replaces two legacy systems with one cloud-hosted service, the NOTAM Management Service (NMS).

MilestoneStatusTiming
NMS machine-to-machine API offered to an initial group of usersDoneLate 2025
Legacy US NOTAM System (USNS) retired, cut over to NMSDoneApril 2026
Legacy Federal NOTAM System (FNS) retired, making NMS the single authoritative sourcePlannedLater in 2026, already moved once
Transition to the ICAO NOTAM formatPlannedCurrently late 2027 or early 2028

The first phase went well. The FAA completed the USNS cutover in April, reported as ahead of schedule. FNS is the harder part. It's the interface airports, heliports and other issuing authorities use to originate NOTAMs, and its retirement date has already slipped from late spring to later in 2026. Reporting from FlightOps.News in June described whether it holds to that revised timeline as the near-term thing to watch.

For a developer, the practical reading is this. NMS is where US NOTAM data comes from now, and when FNS goes it will be the only source. If you're still consuming a legacy feed, migrating is overdue rather than optional.

What the NMS API gives you

The FAA doesn't publish full endpoint documentation openly. Paths, parameters and schemas come with your credentials. What's publicly known:

Formats. Responses are available in AIXM 5.1 or GeoJSON. AIXM is the international aeronautical exchange standard: verbose, XML-based and complete. GeoJSON is far easier to work with if you're putting notices on a map or into a spatial database. Unless you already run an AIXM toolchain, start with GeoJSON.

Access is not self-serve. You request it by emailing [email protected], via the API access section of the NMS portal. Start this early. It's the longest lead-time item in any migration plan, and nothing else can be tested without it.

Beyond that, the most detailed public description of the API's shape comes from the open-source faa-nms-api Node client. It's a third-party package, not an FAA release, so confirm the details against the documentation you receive. According to it:

  • Authentication is OAuth 2.0 client credentials. You exchange a client ID and secret for a short-lived token.
  • Location queries take an airport identifier, with classification filters such as domestic, international, FDC and military.
  • Geospatial queries take a latitude, longitude and radius together, which is what you want for area or route briefings.
  • Incremental updates use a last-updated timestamp to return only what changed since a given time.
  • An initial load returns a compressed dump of all active NOTAMs, for bootstrapping.
  • Separate environments exist for production, staging and a test environment.

The last three matter most. Together they define the architecture any serious consumer should build.

The pattern: bootstrap once, then sync deltas

Polling every airport for its full NOTAM list on a timer is how most legacy integrations work, and it's the thing to stop doing. It's slow and wasteful, and it can miss short-lived notices that appear and cancel between polls.

The NMS shape supports a better pattern:

  1. Bootstrap from the initial load, so your store holds every active NOTAM.
  2. Sync deltas on a short interval, asking only for changes since your last high-water mark.
  3. Apply each change to your store according to its type: new, replacement or cancellation.
  4. Expire notices whose end time has passed.

The subtle part is step 3. A NOTAM can be N (new), R (replaces a previous NOTAM) or C (cancels a previous NOTAM). A store that only appends will keep showing notices that were replaced or cancelled hours ago. That's a correctness bug in a domain where stale information has real consequences.

Here's a store that handles it. It's written against a minimal client interface, initial_load() and changes_since(timestamp), because the real endpoint paths come with your credentials. The logic is the same whatever the transport.

from dataclasses import dataclass
from datetime import datetime, timedelta, timezone

@dataclass
class Notam:
    key: str                     # stable identifier, e.g. "A0373/26" or domestic "03/0373"
    kind: str                    # "N" new, "R" replaces, "C" cancels
    replaces: str | None         # key of the NOTAM an R or C refers to
    location: str
    effective: datetime | None
    expires: datetime | None     # None for PERM
    updated: datetime            # when the source last changed this record
    raw: str                     # keep the original text, always


class NotamStore:
    """Current-state NOTAM table that can be fed the same record twice."""

    def __init__(self):
        self.active: dict[str, Notam] = {}
        self.retired: set[str] = set()     # replaced or cancelled — never re-add
        self.high_water: datetime | None = None

    def apply(self, n: Notam) -> None:
        if n.kind in ("R", "C") and n.replaces:
            self.active.pop(n.replaces, None)       # the old notice is gone either way
            self.retired.add(n.replaces)
        if n.kind in ("N", "R") and n.key not in self.retired:
            current = self.active.get(n.key)
            if current is None or n.updated >= current.updated:
                self.active[n.key] = n              # idempotent: replays are harmless
        if self.high_water is None or n.updated > self.high_water:
            self.high_water = n.updated

    def expire(self, now: datetime) -> int:
        gone = [k for k, n in self.active.items() if n.expires and n.expires <= now]
        for k in gone:
            del self.active[k]
        return len(gone)


OVERLAP = timedelta(minutes=5)   # re-read a little history on every delta call


def initial_load(client, store: NotamStore) -> None:
    for n in client.initial_load():
        store.apply(n)


def sync_changes(client, store: NotamStore, now: datetime) -> int:
    since = store.high_water - OVERLAP if store.high_water else now - timedelta(hours=1)
    changes = client.changes_since(since)
    for n in sorted(changes, key=lambda n: n.updated):
        store.apply(n)
    store.expire(now)
    return len(changes)

A few decisions in there are worth explaining, because each one prevents a specific failure.

Deltas re-read a five-minute overlap. Clocks drift, requests fail halfway through, and records can be committed slightly out of order upstream. Asking for changes since exactly your last timestamp will eventually skip one. Re-reading a small window costs almost nothing, as long as applying a record twice is harmless.

So every apply is idempotent. A record only overwrites an existing one if it's at least as recent. Replaying the same window leaves the store unchanged, and a stale copy arriving late can't overwrite a newer one.

Replaced and cancelled notices are tombstoned. Without the retired set there's a nasty bug. If the overlap window ever replays an original N record after its R or C has been applied, the replaced notice comes back to life. We wrote a test for exactly that case before adding the tombstones, and it failed. It passes now.

Permanent notices never expire. A NOTAM can be PERM, with no end date, and here that's expires=None. Code that compares end times directly will either throw or treat permanent notices as already expired.

The raw text is always kept. Whatever you parse, store the original. You'll need it for display, auditing and the format change below.

An aerial view of runway 7R at Daytona Beach International Airport

Choosing between GeoJSON and AIXM

GeoJSON suits almost everyone building an application. It loads straight into PostGIS, map libraries and most spatial tooling, and a radius or polygon query becomes a single spatial index lookup.

AIXM 5.1 suits operators who already run aeronautical data tooling, or anyone who needs to exchange data with systems that speak AIXM natively. It carries more structure and is much heavier to parse.

Whichever you pick, keep the parsed geometry and the raw notice side by side. Geometry drives your queries and the text drives what a human reads.

Migration checklist

  1. Request NMS API access now. It's the long pole.
  2. Build against staging first. The separate environments exist so you don't learn about schema surprises in production.
  3. Run old and new in parallel. For a period, consume both your legacy source and NMS, and diff the active sets per airport every day. Differences are either bugs in your mapping or real behavioural differences you need to understand before cutting over.
  4. Handle N, R and C properly. If your current integration ignores replacements and cancellations because the legacy feed hid them from you, this is where you'll find out.
  5. Treat identifiers carefully. US domestic NOTAMs carry domestic identifiers, and international notices carry ICAO-style ones. Key your store on whichever is stable in the feed you receive, and test that a replacement actually points at the key you stored.
  6. Don't assume a Q-code. Many US domestic NOTAMs are filed without an ICAO Q-line. Classification logic that depends on Q-codes will silently drop them. Decoding NOTAMs programmatically covers the fallback.
  7. Set up monitoring for staleness. Alert if your high-water mark stops advancing. A silently stalled delta sync looks exactly like a quiet day.

Plan for the next change now

The ICAO format transition, currently planned for late 2027 or early 2028, will change how US NOTAMs are formatted and structured. That's the second migration, and the cheapest way to prepare is to avoid building anything that depends on today's domestic text format:

  • Store the raw notice and the structured fields separately.
  • Key parsing logic on structured fields, not on regular expressions over the text.
  • Keep your identifier handling able to cope with both domestic and ICAO-style identifiers.
  • Wrap the source behind your own interface, as the store above does, so that a format change is a mapping change rather than a rewrite.

Teams that write regular expressions against today's text format will be rewriting them in 2028.

An aeronautical chart whose caution box tells pilots to consult NOTAMs

Or let someone else absorb the churn

Running your own NMS integration is the right call if NOTAMs are central to your product, you need AIXM, or you want the most direct path to the source. It's a real commitment, though: onboarding, OAuth token handling, sync infrastructure, monitoring, and two format migrations in three years.

If you'd rather consume NOTAMs than operate a NOTAM pipeline, our NOTAM endpoint returns active notices for any airport worldwide, with the raw ICAO text and parsed fields. We handle the upstream sourcing, including NMS for US notices, so changes like this migration and the coming ICAO format transition are ours to absorb rather than yours. It's on the free trial, and the NOTAM API overview covers what the response looks like.

Either way, the principles in this guide still apply. Keep the raw text, handle replacements and cancellations, and don't let your code depend on a text format that has a retirement date.