TL;DR: DPV footnotes distinguish “address confirmed”, “building confirmed but the unit is missing”, “the primary number does not exist” and several special delivery types. Each implies a different action, and collapsing them into valid/invalid throws away the only signal that tells a checkout form which single field to ask for. The parent workflow is USPS CASS certification guidelines.
The Codes That Matter Most
Delivery point validation returns a short string of two-character footnotes describing what was and was not confirmed. A handful account for almost all production traffic, and each one implies a specific next step.
| Footnote | Meaning | Pipeline action |
|---|---|---|
AA |
the address matched the ZIP+4 file | continue; necessary but not sufficient on its own |
BB |
the whole address, including any unit, is a confirmed delivery point | accept and geocode |
CC |
the primary number is confirmed; the secondary is present but not recognised | keep the unit, flag as unconfirmed, do not block |
N1 |
the primary number is confirmed; a secondary is required and none was supplied | prompt for the apartment or suite only |
M1 |
the primary number is missing | re-prompt for the street line |
M3 |
the primary number is present but invalid for that street | re-prompt for the street line |
F1 |
a military address (APO, FPO, DPO) | accept; exclude from rooftop metrics |
G1 |
general delivery | accept; route to a postal centroid |
U1 |
a unique ZIP assigned to a single high-volume recipient | accept; no street-level precision exists |
P1 |
a PO Box or rural route with a missing box number | re-prompt for the box number |
AA on its own is the code most often misread. It confirms that the address exists in the
ZIP+4 file, which is not the same as confirming that mail can be delivered there — BB is
the code that asserts deliverability, and a pipeline that treats AA as success will accept
addresses that no carrier can serve.
Three Groups, Three Behaviours
The middle band is where the commercial value sits. A prompt that says “we found 100 Main
Street — which apartment?” converts far better than a generic validation error, and the only
thing that makes it possible is reading N1 distinctly from M1.
Parsing the Footnote String
from __future__ import annotations
from dataclasses import dataclass
CONFIRMED = {"BB"}
NEEDS_SECONDARY = {"N1", "CC"}
PRIMARY_FAILED = {"M1", "M3", "P1"}
SPECIAL_TYPE = {"F1", "G1", "U1"}
@dataclass(frozen=True)
class DpvResult:
codes: frozenset[str]
@classmethod
def parse(cls, footnotes: str) -> "DpvResult":
"""Split a concatenated footnote string into individual codes."""
cleaned = footnotes.replace(" ", "").upper()
pairs = {cleaned[i:i + 2] for i in range(0, len(cleaned) - 1, 2)}
return cls(frozenset(pairs))
@property
def action(self) -> str:
if self.codes & PRIMARY_FAILED:
return "reprompt_address"
if self.codes & NEEDS_SECONDARY:
return "prompt_unit"
if self.codes & CONFIRMED:
return "accept"
return "review"
@property
def excluded_from_rooftop(self) -> bool:
return bool(self.codes & SPECIAL_TYPE)
Note the ordering inside action: primary failures are checked before secondary ones,
because a response can carry both and the street line must be fixed first. Asking for an
apartment number in a building that does not exist wastes the customer’s time and produces
another failure.
Store the Raw String, Not Just the Verdict
The temptation is to store action and discard the codes. Keeping the raw footnote string
costs a short text column and preserves the ability to answer questions nobody has asked
yet — which share of failures are missing units versus invalid streets, whether a particular
partner’s data is systematically missing secondaries, whether the mix changed after a form
redesign.
It also protects against your own mapping being wrong. If the interpretation of a code turns out to be mistaken, a stored raw string can be re-interpreted across historical rows in a single query; a stored verdict cannot be recovered at all. That asymmetry is the general argument for persisting provider responses rather than only your reading of them.
Reading the Mix as a Data-Quality Signal
That reframing is worth making explicitly to whoever owns the upstream form. “Half your records need an apartment number we cannot ask for” is a specific, fixable observation; “your data quality is poor” is not, and it is the same finding.
Footnotes and the Retention Question
Storing footnotes raises a question worth settling early: how long should they be kept, and against which record?
The codes describe a moment in time — what the reference data said on the day the address
was validated — so they age in a specific way. A CC from eighteen months ago says the unit
was unrecognised then and implies nothing about today, which is exactly why the
re-validation policy exists. Keeping the footnote alongside a timestamp makes that
distinction available; keeping it without one makes the whole column ambiguous.
For the address record itself, the footnotes from the most recent validation are the ones that matter operationally, and they belong on the row. The full history belongs in an append-only validation log, keyed by address and time, which is also where the standardised form returned by each validation should live. That separation keeps the hot row narrow while preserving the ability to answer how an address’s status changed.
Retention on the log should match the retention on the address itself rather than being set independently. A validation history that outlives the customer record it describes is a data-protection problem with no operational benefit, and one that expires sooner than the record leaves the address with a status nobody can explain.
Edge Cases and Failure Modes
Codes concatenated without separators. Footnote strings arrive as AABB or AAN1 with
no delimiter, which is why the parser splits on fixed pairs rather than on whitespace. A
split on a separator returns one long token and matches nothing.
Vendor-specific extensions. Some services add codes beyond the USPS set. Keep unknown
codes in the stored string and fall through to review rather than raising — an unfamiliar
code is a reason to look, not a reason to fail a batch.
Treating CC as a rejection. A supplied unit that DPV does not recognise is often a new
building whose unit list has not been published. Blocking on it converts a data-freshness lag
into lost orders; flagging it does not.
Interpreting footnotes without the match code. Footnotes describe the delivery point; the separate match result describes whether standardisation succeeded at all. Read both, or a standardisation failure can present as an ambiguous delivery-point result.
Reading Order Inside One Response
A single response frequently carries several footnotes, and the order in which your code inspects them determines which prompt the customer sees.
In short: the footnotes on the row answer what to do now, and the footnotes in the log answer what happened and when. Both are cheap; only one of them is enough on its own.
Integration Note
The action produced here feeds directly into handling missing unit numbers in delivery validation, which models the three-state unit field and the retry policy for unconfirmed units. The special-type codes feed the accuracy reporting described in validating geocoding accuracy and confidence scoring, where excluding them from rooftop metrics is what stops a rural or military-heavy file from looking like a quality regression.
Both together give you the operational answer and the audit trail, at the cost of one text column and one append-only table.
Related
- USPS CASS Certification Guidelines — what certification covers and what it does not.
- Step-by-Step Guide to CASS Address Validation — the call that returns these codes.
- Handling Missing Unit Numbers in Delivery Validation — turning
N1into a one-field prompt.