USPS v3 Address Validation: One Request, Start to Finish
The request itself is one GET. Two things sit in front of it, and they are where the day goes: an OAuth token, and since August 1, 2026, a signed license agreement with USPS.
This walks the whole path. The credential chain, the exact call, every field that comes back, and the four failures that account for most of the support traffic on this endpoint.
What you need before the first call
The USPS v3 REST API replaced the Web Tools XML API on January 25, 2026. Registration alone no longer gets you address data. The current sequence for the Addresses API runs:
- Create or log into a USPS Business Portal (COP) account.
- Create and fund an Enterprise Payment Account. Fees for address lookups debit from it.
- Accept the updated terms, then request an Addresses API license.
- Sign the order form and license agreement through DocuSign.
- Wait for USPS to countersign. Signatures move at legal-department speed on both sides.
- Link your API credentials to the license, refresh your claims, and refresh your OAuth token.
Budget weeks for that, not an afternoon. Charges land monthly as an Addresses Usage Fee against the payment account, assessed at the start of each month for the prior month. The pricing breakdown has the published fee curve; the CRID and MID guide walks the enrollment screens with the errors we hit.
Step 1: get an OAuth Bearer token
Standard client credentials grant. Your consumer key and secret come from the application you registered in the portal.
curl -X POST "https://apis.usps.com/oauth2/v3/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials" \
-d "client_id=$USPS_CONSUMER_KEY" \
-d "client_secret=$USPS_CONSUMER_SECRET"The token lasts 28800 seconds, which is 8 hours. Cache it. Every token request counts against your hourly quota, so a service that re-authenticates per call will exhaust its rate limit before it validates anything. The OAuth guide has a cache implementation with the 30-minute expiry buffer USPS behavior actually requires.
Step 2: call the Addresses endpoint
curl -G "https://apis.usps.com/addresses/v3/address" \
-H "Authorization: Bearer $USPS_TOKEN" \
--data-urlencode "streetAddress=1600 Pennsylvania Ave NW" \
--data-urlencode "city=Washington" \
--data-urlencode "state=DC" \
--data-urlencode "ZIPCode=20500"import httpx
resp = httpx.get(
"https://apis.usps.com/addresses/v3/address",
params={
"streetAddress": "1600 Pennsylvania Ave NW",
"city": "Washington",
"state": "DC",
"ZIPCode": "20500",
},
headers={"Authorization": f"Bearer {token}"},
timeout=10.0,
)
resp.raise_for_status()
data = resp.json()
print(data["address"]["ZIPPlus4"]) # 0005
print(data["additionalInfo"]["DPVConfirmation"]) # Yconst params = new URLSearchParams({
streetAddress: "1600 Pennsylvania Ave NW",
city: "Washington",
state: "DC",
ZIPCode: "20500",
});
const res = await fetch(
"https://apis.usps.com/addresses/v3/address?" + params,
{ headers: { Authorization: "Bearer " + token } },
);
if (!res.ok) throw new Error("USPS " + res.status + ": " + (await res.text()));
const data = await res.json();
console.log(data.address.ZIPPlus4); // 0005
console.log(data.additionalInfo.DPVConfirmation); // YThe parameter rule catches people. streetAddress is always required. On top of that USPS requires either city and state, or ZIPCode, or all three. A street address on its own comes back 400. Secondary unit information can ride at the end of streetAddress or sit in its own secondaryAddress parameter; both are accepted.
Use apis-tem.usps.com for testing and apis.usps.com for production. Credentials are not interchangeable between them.
Step 3: read the response
{
"firm": "",
"address": {
"streetAddress": "1600 PENNSYLVANIA AVE NW",
"streetAddressAbbreviation": "1600 PENNSYLVANIA AVE NW",
"secondaryAddress": "",
"city": "WASHINGTON",
"cityAbbreviation": "WASHINGTON",
"state": "DC",
"ZIPCode": "20500",
"ZIPPlus4": "0005",
"urbanization": ""
},
"additionalInfo": {
"deliveryPoint": "00",
"carrierRoute": "C000",
"DPVConfirmation": "Y",
"DPVCMRA": "N",
"business": "Y",
"centralDeliveryPoint": "",
"vacant": "N"
},
"corrections": [],
"matches": []
}Everything comes back uppercase and standardized. If your interface renders the API response directly, users will see 1600 PENNSYLVANIA AVE NW where they typed mixed case. Title-case it on your side; do not send the pretty version back to USPS.
Every DPV code, and what USPS actually claims
DPVConfirmation is the field the whole call exists for. The USPS specification defines exactly four values:
| Value | Meaning |
|---|---|
Y |
Confirmed for the primary number and, if present, the secondary number. |
D |
Confirmed for the primary number only. Secondary information was missing. |
S |
Confirmed for the primary number only. Secondary information was present but did not confirm. |
N |
Neither the primary nor the secondary number confirmed. |
Two things practitioners get wrong here.
Y is not a delivery guarantee. USPS states in its own specification that a Y does not necessarily imply USPS delivers to that address. DPV confirms the address maps to a known USPS address record. Whether carriers deliver there is a separate question, and carrierRoute values such as R777 and R779 are the tell: those can mean the recipient collects mail somewhere other than the physical address.
D and S are different problems. D means you never collected the apartment number and should ask for it. S means the customer gave you one and it did not match the building, which is a correction flow, not a collection flow. Treating both as “prompt for unit” sends the second group back through a form they already filled in correctly enough to get a building match.
The rest of additionalInfo is where the operational signal lives:
| Field | Use |
|---|---|
DPVCMRA |
Y marks a Commercial Mail Receiving Agency. Freight and age-restricted shipments usually need to reject these. |
vacant |
Y means USPS has flagged the delivery point as unoccupied. High-value mail to a vacant point is money spent on nothing. |
business |
Y marks a business delivery point, which changes residential surcharge assumptions on other carriers. |
carrierRoute |
Route code. Also the R777 and R779 signal above. |
deliveryPoint |
Two-digit delivery point, needed to build a delivery point barcode. |
centralDeliveryPoint |
Populated for centralized delivery such as a cluster box unit. |
The four failures you will actually hit
401 with invalid_client on the token request. The consumer key or secret is wrong, or you are sending production credentials at the testing host. This one fails at the token endpoint, before you reach any address route, which is how you tell it apart from the next case.
401 on the address call with a token that worked ten minutes ago. The token expired or USPS revoked it early. A strict countdown against expires_in is not reliable; refresh at 28800 minus 1800 seconds and re-fetch on any 401 rather than trusting the clock.
429 Too Many Requests. The default is 60 requests per hour per application, shared across every v3 endpoint your app touches. Address validation, tracking and rate calls all draw on the same window. Back off and retry rather than hammering; the rate limit guide covers the queueing and caching patterns that survive it.
404 with a USPS error body. USPS overloads 404 for Address Not Found, which is an input condition, not a missing route. A 404 carrying a USPS error body means you reached the API and it answered. A 404 with no USPS error body means the path is wrong.
import time
import httpx
def validate(params, token_fn, attempts=3):
for attempt in range(attempts):
token = token_fn()
resp = httpx.get(
"https://apis.usps.com/addresses/v3/address",
params=params,
headers={"Authorization": f"Bearer {token}"},
timeout=10.0,
)
if resp.status_code == 200:
return resp.json()
# Token died early. Force a refresh, then retry once more.
if resp.status_code == 401:
token_fn.invalidate()
continue
# Rate limited. Honour Retry-After when USPS sends one.
if resp.status_code == 429:
wait = int(resp.headers.get("Retry-After", 2 ** attempt))
time.sleep(wait)
continue
# 404 with a USPS error body is a bad address, not a bad route.
if resp.status_code == 404 and "error" in resp.text:
return {"error": "address_not_found", "input": params}
resp.raise_for_status()
raise RuntimeError("USPS validation failed after retries")The SDKs
Three open-source clients wrap the token lifecycle, the cache and the typed errors so you are not writing the block above by hand. They talk to USPS directly on your own credentials.
# pip install usps-v3
from usps_v3 import USPSClient
client = USPSClient(
consumer_key="your_key",
consumer_secret="your_secret",
)
result = client.addresses.validate(
street_address="1600 Pennsylvania Ave NW",
city="Washington",
state="DC",
zip_code="20500",
)
print(result["address"]["ZIPPlus4"]) # 0005// npm install usps-v3
import { USPSClient } from "usps-v3";
const client = new USPSClient({
consumerKey: "your_key",
consumerSecret: "your_secret",
});
const result = await client.addresses.validate({
streetAddress: "1600 Pennsylvania Ave NW",
city: "Washington",
state: "DC",
ZIPCode: "20500",
});
console.log(result.address.ZIPPlus4); // 0005<?php
// composer require revaddress/usps-v3-php
use RevAddress\USPS\USPSClient;
$client = new USPSClient(
consumerKey: 'your_key',
consumerSecret: 'your_secret'
);
$result = $client->addresses->validate(
streetAddress: '1600 Pennsylvania Ave NW',
city: 'Washington',
state: 'DC',
ZIPCode: '20500'
);
echo $result['address']['ZIPPlus4']; // 0005Packages: usps-v3 on PyPI · usps-v3 on npm · revaddress/usps-v3-php on Packagist. Full comparison on the SDK page.
Running it through RevAddress instead
The managed route swaps the OAuth block for a single header. One request, an X-API-Key, and the token lifecycle, the expiry buffer, the retry-on-401 and the rate-limit budgeting sit on our side.
What the subscription buys is the infrastructure, not the USPS data. Standardization, geocoding and address extract run on Census Bureau reference data and are included in every plan. DPV verification, rates, service standards and tracking run on your USPS license (BYOK), which means you sign the Addresses API agreement, USPS bills your Enterprise Payment Account for the lookups, and we manage the credentials and the lifecycle around them. Labels are the same arrangement, on your own license and your own payment account, because postage has to be paid by the entity that owes it.
curl -G "https://api.revaddress.com/api/address/validate" \
-H "X-API-Key: rv_live_your_key_here" \
--data-urlencode "streetAddress=1600 Pennsylvania Ave NW" \
--data-urlencode "city=Washington" \
--data-urlencode "state=DC" \
--data-urlencode "ZIPCode=20500"Three differences worth knowing before you wire it in.
Field names are forgiving. street_address, address, address1 and line1 all map to streetAddress; zip, zipcode and postal_code all map to ZIPCode. Import scripts written against another vendor usually work unchanged.
The response adds a resolution block alongside the USPS fields: a classification, a nextAction and a plain-English userMessage. It turns the DPV table above into a branch your checkout code can switch on without re-deriving the DPV semantics in every codebase.
Read the route header, do not infer it from the body. Every response carries X-RevAddress-DPV-Lane. A value of verified means the answer came from USPS under a license and carries USPS attributes. A value of standardization means the address was standardized against US Census Bureau reference data, which returns correct postal form and coordinates but no DPV, no ZIP+4 and no deliverability flag, because those are USPS data and USPS data now requires a license.
Which one you get depends on the credentials behind the key. Connect your USPS license and verified answers flow through with the full USPS attribute set: DPV, ZIP+4, CMRA, vacant and business flags. Without one, you get Census-based standardization and geocoding, which is the whole job for list hygiene and mapping and is available on every plan including Free.
What it costs to run
USPS bills the Addresses API on monthly consumption at a single tier. The first tier is a $10 flat fee covering up to 2,000 lookups, and there is no free allowance, so one lookup and 2,000 lookups cost the same. Above that the curve runs per 1,000 or fraction thereof. Per lookup USPS is cheap; the license and the 60-per-hour ceiling are the real constraints. The pricing post has the published table and a calculator.
If what you have is a spreadsheet rather than an integration, an API key is the wrong tool. List Clean takes a CSV and returns it standardized, deduplicated and geocoded against Census Bureau data, with no USPS license involved.
Start here
- Get a free API key, 1,000 requests a month, no credit card. Census-based standardization, geocoding and address extract run on it with no USPS license at all.
- OAuth troubleshooting covers every 401 and 403, plus the second token that label creation needs.
- Batch validation does 50 addresses in one call, with the failure semantics list work needs.
- Rate limits in 2026 covers the drop from Web Tools throughput to 60 per hour.
- API reference has the request and response schemas with a live try-it.
- Pricing shows which capabilities run on your own USPS license and which do not.
Questions
- What is the USPS v3 address validation endpoint?
- GET https://apis.usps.com/addresses/v3/address. It takes query parameters, returns JSON, and requires an OAuth 2.0 Bearer token in the Authorization header. The testing host is apis-tem.usps.com.
- Is the USPS address validation API still free?
- No. Effective August 1, 2026, access to the Addresses API requires a signed license agreement, and usage bills on a consumption-tier curve assessed against an Enterprise Payment Account. The first tier is a $10 flat monthly fee covering up to 2,000 lookups; there is no free allowance.
- What parameters does the USPS address endpoint require?
- streetAddress is always required. Beyond that you must send either city and state, or ZIPCode, or all three. Sending only a street address returns a 400.
- What does DPVConfirmation Y mean?
- Y means the address was DPV confirmed for both the primary number and, if present, the secondary number. USPS is explicit in its own specification that Y does not by itself imply USPS delivers to that address; a carrierRoute of R777 or R779 in particular can mean the recipient collects mail somewhere else.
- How many USPS address lookups can I make per hour?
- 60 requests per hour per application by default. The limit is shared across every v3 endpoint your app calls, not applied per endpoint, so tracking and rate calls draw on the same window as address validation.
Read next
USPS v3 OAuth Troubleshooting: Every Error and How to Fix It
401, 403, and 429 from USPS v3, with the exact fix for each, including the Addresses API license that has gated address calls since August 1, 2026.
9 min readGuideUSPS API Pricing 2026 — What It Actually Costs
USPS address validation stopped being free on August 1, 2026. The published fee curve, the license that gates it, and what direct vs third-party costs.
8 min readBreaking ChangeUSPS API Rate Limits in 2026: From 6,000/min to 60/hour
USPS v3 throughput fell from roughly 6,000 requests a minute on Web Tools to 60 per hour. What that does to checkout at peak, and five architecture patterns.
12 min read