Understanding USPS DPV Footnote Codes

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

Footnote groups and the experience each implies Three groups. Confirmed codes accept the address silently. Secondary-specific codes trigger a one-field prompt for the apartment or suite with the building already confirmed. Primary-failure codes require the whole street line to be re-entered. A fourth note explains that special delivery types are accepted but excluded from precision metrics. BB · AA+BB confirmed — accept silently no interruption; geocode and cache the standardised form N1 · CC building found — ask for the unit only one field, building shown as already confirmed; the highest-converting prompt M1 · M3 · P1 primary failed — re-enter the street line the number is missing or does not exist on that street F1, G1 and U1 are accepted normally but excluded from rooftop-precision reporting.

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

Footnote mix as a supplier scorecard Two stacked bars comparing the footnote distribution of two intake channels. The first is almost entirely confirmed results with a small tail. The second has a large share of missing-secondary and primary-failure results, indicating the form that feeds it does not collect a unit field. Footnote distribution by intake channel checkout form 88% confirmed · 10% needs unit · 2% primary failed partner feed 36% confirmed · 51% needs unit · 13% primary failed A partner whose feed is half missing-secondary is not sending bad addresses — it is not collecting a unit field.

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.

Which code wins when several are returned A three-step decision. Primary-failure codes are checked first because a missing street number makes any unit question pointless. Secondary codes are checked next and drive the one-field prompt. Confirmation is checked last. Special delivery types are read separately as a flag rather than as part of the decision. 1 · primary failed? M1 · M3 · P1 2 · secondary issue? N1 · CC 3 · confirmed? BB Asking for an apartment in a building that does not exist wastes an interaction and produces a second failure, which is the whole reason primary failures are inspected first. F1, G1 and U1 are read in parallel as a reporting flag, never as part of this decision.

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.