FAA Registration Status Codes Explained: Valid, Pending and Expired

On this page

Problem statement

Not every row in the FAA registry is a registered aircraft. The STATUS CODE column distinguishes valid registrations from pending ones, aircraft whose sale has been reported, N-numbers assigned to an aircraft that was never registered, and registrations on their way to cancellation. The FAA's file documentation lists the codes, from letters such as V and R to the numbers 1 to 29, and an analysis that counts every row counts some aircraft that are not on the register. This page decodes the codes in the 16 October 2026 file, checks the seven-year expiry, and shows which figures change when the filter is applied.

Quick answer

Count each code and attach its meaning from the FAA's documentation:

STATUS = {"V": "valid", "R": "registration pending", "M": "valid, manufacturer's dealer certificate", "T": "valid, trainee",
          "N": "non-citizen corporation, flight hours not reported", "7": "sale reported", "2": "N-number assigned, not yet registered",
          "10": "assigned, not registered, pending cancellation", "17": "sale reported, pending cancellation",
          "19": "registration pending, pending cancellation", "18": "sale reported, canceled", "3": "assigned (non type certificated), not yet registered"}
st = reg["STATUS CODE"].value_counts()
print(pd.DataFrame({"records": st.head(10), "meaning": st.head(10).index.map(STATUS)}).to_string())
print(f"valid (V, M, T): {reg['STATUS CODE'].isin(['V', 'M', 'T']).mean():.1%}")
             records                                             meaning
STATUS CODE                                                             
V             310844                                               valid
R               2155                                registration pending
M               1330            valid, manufacturer's dealer certificate
7                870                                       sale reported
2                854               N-number assigned, not yet registered
N                282  non-citizen corporation, flight hours not reported
10               250      assigned, not registered, pending cancellation
17               209                 sale reported, pending cancellation
19               143          registration pending, pending cancellation
18               131                             sale reported, canceled
valid (V, M, T): 98.3%

310,844 records are valid (V), 2,155 pending (R) and 1,330 valid under a manufacturer's dealer certificate (M). With the trainee code T, valid records are 98.3% of the file. The rest are sale reports, assigned but unregistered N-numbers and cancellations in progress.

Horizontal bars on a log scale of the ten commonest status codes with their meanings, from V valid with 310,844 records down to 18 sale reported, canceled with 131. Horizontal bars on a log scale of the ten commonest status codes with their meanings, from V valid with 310,844 records down to 18 sale reported, canceled with 131.
The ten commonest status codes in the FAA registry.

What the codes change

1. Every status, grouped

group = reg["STATUS CODE"].map(lambda c: "valid" if c in ("V", "M", "T") else "pending" if c in ("R", "19") else
                               "sale or transfer" if c in ("7", "17", "18") else "assigned, never registered" if c in ("2", "3", "4", "10", "11", "12") else "other")
print(group.value_counts().to_string())
print(f"distinct codes: {reg['STATUS CODE'].nunique()}")
STATUS CODE
valid                         312174
pending                         2298
assigned, never registered      1388
sale or transfer                1210
other                            534
distinct codes: 21

Grouped, 312,174 records are valid in one form or another, 2,298 pending, 1,388 assigned but never registered and 1,210 sold or being transferred. The file uses 21 distinct codes.

2. Expiry dates: seven years

v = reg[reg["STATUS CODE"] == "V"].copy()
v["issued"] = pd.to_datetime(v["CERT ISSUE DATE"], format="%Y%m%d", errors="coerce")
v["expires"] = pd.to_datetime(v["EXPIRATION DATE"], format="%Y%m%d", errors="coerce")
recent = v[v.issued >= "2024-01-01"]
gap = (recent.expires - recent.issued).dt.days / 365.25
print(f"certificates issued since 2024: {len(recent):,}; years to expiry, median {gap.median():.2f}, share 6.9-7.1: {gap.between(6.9, 7.1).mean():.1%}")
print(v.expires.dt.year.value_counts().sort_index().loc[2026:2034].to_string())
certificates issued since 2024: 86,364; years to expiry, median 7.04, share 6.9-7.1: 100.0%
expires
2026.0        3
2027.0    43742
2028.0    65002
2029.0    64763
2030.0    48871
2031.0    28244
2032.0    31416
2033.0    26717

Every certificate issued since 2024, 86,364 of them, expires about seven years after issue (median 7.04 years): the FAA moved from three-year to seven-year registrations in 2023. Expiry dates in the file therefore run from 2027 to 2033, and very few valid registrations expire in 2026.

Bar chart of valid registrations by year of expiry: 3 in 2026, then 43,742 in 2027 rising to 65,002 in 2028 and tapering to 26,717 in 2033. Bar chart of valid registrations by year of expiry: 3 in 2026, then 43,742 in 2027 rising to 65,002 in 2028 and tapering to 26,717 in 2033.
Expiry years run from 2027 to 2033 under seven-year certificates.

3. Valid but already past expiry

today = pd.Timestamp("2026-10-16")
print(f"status V with an expiry date before 16 October 2026: {(v.expires < today).sum():,}")
print(v[v.expires < today][["N-NUMBER", "EXPIRATION DATE", "LAST ACTION DATE"]].head(4).to_string(index=False))
status V with an expiry date before 16 October 2026: 4
N-NUMBER EXPIRATION DATE LAST ACTION DATE
   303RJ        20260531         20241212
   4980S        20250520         20250520
   664NK        20250430         20250410
   757MK        20260630         20250804

Four status-V records have an expiry date before the day the file was made. Their last action dates are recent, so they are most likely renewals in progress; a status code is not always in step with the dates beside it.

How the counts move with the filter

The data used on this page

The FAA's Releasable Aircraft Database, public domain, downloaded on 16 October 2026 and saved on this site, because the FAA replaces it every day. The saved copy leaves out the owners' names and street addresses and keeps five-digit ZIP codes; every other column is as published. With it come the Census Bureau's Vintage 2025 county population estimates, 2024 county boundaries and 2024 ZCTA gazetteer.

from pathlib import Path
import zipfile

import geopandas as gpd
import numpy as np
import pandas as pd
import requests

Path("data").mkdir(exist_ok=True)
# FAA Releasable Aircraft Database, 16 October 2026: MASTER.txt without the NAME, STREET, STREET2 and OTHER NAMES
# columns and with five-digit ZIP codes (MASTER-no-names.txt), ACFTREF.txt and ENGINE.txt as published; Census
# Vintage 2025 county estimates, 2024 cartographic boundary counties and 2024 ZCTA gazetteer. Saved here unchanged
# otherwise, because the FAA replaces the file daily.
# Live: https://registry.faa.gov/database/ReleasableAircraft.zip (needs a browser User-Agent),
# https://www2.census.gov/geo/docs/maps-data/data/gazetteer/2024_Gazetteer/2024_Gaz_zcta_national.zip
ARCHIVE = "faa-aircraft-registry-2026-10-16.zip"
if not Path(f"data/{ARCHIVE}").exists():
    Path(f"data/{ARCHIVE}").write_bytes(requests.get(f"https://data.spatialworkflow.io/{ARCHIVE}", timeout=600).content)
Z = zipfile.ZipFile(f"data/{ARCHIVE}")


def faa(name):
    """One registry file: comma-separated, every field padded with spaces, a byte-order mark and a trailing comma."""
    d = pd.read_csv(Z.open(name), dtype=str, encoding="utf-8-sig", keep_default_na=False)
    d.columns = d.columns.str.strip()
    d = d.loc[:, [c for c in d.columns if c and not c.startswith("Unnamed")]]
    return d.apply(lambda c: c.str.strip())


reg = faa("MASTER-no-names.txt")
ref = faa("ACFTREF.txt")
TYPE_AIRCRAFT = {"1": "glider", "2": "balloon", "3": "blimp", "4": "fixed wing single engine", "5": "fixed wing multi engine",
                 "6": "rotorcraft", "7": "weight-shift", "8": "powered parachute", "9": "gyroplane", "H": "hybrid lift", "O": "other"}
REGISTRANT = {"1": "individual", "2": "partnership", "3": "corporation", "4": "co-owned", "5": "government", "7": "LLC",
              "8": "non-citizen corporation", "9": "non-citizen co-owned", "": "blank"}
Path("data/cb_2024_us_county_20m.zip").write_bytes(Z.read("cb_2024_us_county_20m.zip"))
counties = gpd.read_file("data/cb_2024_us_county_20m.zip")
STATEFP = counties.drop_duplicates("STUSPS").set_index("STUSPS").STATEFP
pop = pd.read_csv(Z.open("co-est2025-alldata.csv"), encoding="latin-1", dtype={"STATE": str, "COUNTY": str})
pop = pop[pop.SUMLEV == 50].assign(GEOID=lambda p: p.STATE + p.COUNTY)
print(f"FAA registry, 16 October 2026: {len(reg):,} aircraft, {len(ref):,} make-and-model codes")
FAA registry, 16 October 2026: 317,604 aircraft, 94,115 make-and-model codes

Example 1: aircraft by state, with and without the filter

us = reg[reg.COUNTRY == "US"]
a, b = us.STATE.value_counts(), us[us["STATUS CODE"] == "V"].STATE.value_counts()
d = pd.DataFrame({"all records": a, "status V": b}).dropna()
d["share not valid"] = 1 - d["status V"] / d["all records"]
print(d[d["all records"] >= 2000].sort_values("share not valid", ascending=False).head(5).round(3).to_string())
       all records  status V  share not valid
STATE                                        
MN            6903      6430            0.069
KS            5728      5410            0.056
GA            8786      8423            0.041
IA            4494      4389            0.023
SD            2274      2225            0.022

Minnesota has the largest share of records that are not valid, 6.9%, then Kansas at 5.6%. Any state count made without the filter is off by up to that much, and the error varies by state.

Example 2: which aircraft are pending

pend = reg[reg["STATUS CODE"] == "R"]
pend_issued = pd.to_datetime(pend["LAST ACTION DATE"], format="%Y%m%d", errors="coerce")
print(f"pending: {len(pend):,}; last action in 2026: {(pend_issued.dt.year == 2026).mean():.1%}; median last action {pend_issued.median():%Y-%m-%d}")
print(pend["TYPE REGISTRANT"].map(REGISTRANT).value_counts().head(3).to_string())
pending: 2,155; last action in 2026: 91.5%; median last action 2026-07-16
TYPE REGISTRANT
individual    2155
Horizontal bars of the share of each state's registry records that are not valid, for states with 2,000 or more records, led by Minnesota at 6.9% and Kansas at 5.6%. Horizontal bars of the share of each state's registry records that are not valid, for states with 2,000 or more records, led by Minnesota at 6.9% and Kansas at 5.6%.
Share of each state's records that are not status V.

The 2,155 pending registrations are recent: 91.5% had their last action in 2026, the median on 16 July. Every one of them is listed with an individual as the registrant type.

FAQ

What does status code V mean in the FAA registry?

A valid registration. In the 16 October 2026 file 310,844 of 317,604 records are V; the rest are pending, dealer, sale-reported, assigned-but-unregistered and cancellation states.