USPS Web Tools Migration Checklist (2026)
Updated 14 min read By RevAddress
Topics: Web Tools migration, USPS v3, USPS license
USPS Web Tools stopped answering on January 25, 2026. Every call to secure.shippingapis.com returns an error, and every integration built on a USERID and an XML body is dead.
The checklist runs in the order the work has to happen. The next section sets the sequencing, which changed after the first version of this guide.
Start the Addresses API license on day one
The paperwork takes longer than the code. Effective August 1, 2026, access to the USPS Addresses API requires a signed license agreement, and usage bills on a monthly consumption curve against a funded Enterprise Payment Account. Web Tools was free, unmetered and gated by nothing but a USERID.
Start the license request on day one, in parallel with the code. The pricing post has the published fee curve and the full signature sequence.
Phase 0: pick the route
There are two paths, and the choice drives everything below.
Direct to USPS. You hold the license, the payment account and the OAuth lifecycle. You get your own reporting, your own compliance posture and your own line to USPS support. You also write the token cache, the retry logic and the rate-limit budgeting, and you own the enrollment queue.
Through a managed API. You send one header and skip the OAuth plumbing. The license stays in the picture; only the plumbing moves. Anything carrying USPS data, meaning DPV verification, rates, service standards, tracking and labels, runs on your USPS license (BYOK): you sign the agreement, USPS bills your Enterprise Payment Account, and the managed layer handles credentials, token lifecycle, caching and retries. Everything that is not USPS data, including standardization, geocoding and address extract against Census Bureau reference data, runs with no license at all, which means you can build and test the whole client before the paperwork clears.
Pick direct if you are already deep in USPS enrollment for labels and payment, and managed if you want the enrollment to be the only USPS work you do.
Phase 1: inventory (before you write any code)
1.1 Grep for the old surface. Search the codebase for secure.shippingapis.com, ShippingAPI.dll, USERID, and each XML request name. Every hit is a migration item.
1.2 Map each call to its v3 replacement. The paths and, in several cases, the HTTP methods changed:
| Web Tools request | USPS v3 replacement |
|---|---|
AddressValidateRequest |
GET /addresses/v3/address |
CityStateLookupRequest |
GET /addresses/v3/city-state |
ZipCodeLookupRequest |
GET /addresses/v3/zipcode |
RateV4Request |
POST /prices/v3/base-rates/search |
IntlRateV2Request |
POST /international-prices/v3/base-rates/search |
TrackV2Request / TrackFieldRequest |
GET /tracking/v3/tracking/{trackingNumber} |
eVSRequest / DelivConfirmCertifyV4 |
POST /labels/v3/label |
SDCGetLocationsRequest |
GET /locations/v3/post-office-locations |
FirstClassMailRequest |
GET /service-standards/v3/estimates |
Two rows change shape. ZipCodeLookupRequest and CityStateLookupRequest are different v3 endpoints answering opposite questions. And the pricing calls moved from GET with query parameters to POST with a JSON body, which means the migration is a rewrite of the call, not a URL swap.
1.3 Retire the dead dependencies.
| Dead package | Replacement |
|---|---|
usps-webtools (npm) |
usps-v3 (npm) |
usps-api (pip) |
usps-v3 (pip) |
usps (pip) |
usps-v3 (pip) |
| Any XML parser used only for USPS | Delete. v3 returns JSON. |
1.4 Measure your real call volume. Count calls per hour at peak, not per day at average. The v3 default is 60 requests per hour per application, shared across every endpoint, and a daily average hides the peak hour. A checkout that makes four USPS calls per order reaches the limit at 15 orders an hour.
| Peak-hour USPS calls | What to plan for |
|---|---|
| Under 60 | Direct works. Budget the license fee and build the token cache. |
| 60 to a few hundred | Cache aggressively and queue non-urgent calls. A managed layer smooths the bursts with caching and queuing; it does not raise your USPS ceiling, since BYOK calls spend your own app’s quota. |
| Sustained high volume | Request a rate-limit increase from USPS in writing, and design for the response time on that request. |
Phase 2: credentials (start this first)
2.1 Register an application in the USPS Business Portal and record the Consumer Key and Consumer Secret.
2.2 Note the naming mismatch. The portal calls them Consumer Key and Consumer Secret. The OAuth token request expects them as client_id and client_secret. They are the same two values.
2.3 Create and fund an Enterprise Payment Account. Address usage fees debit from it, and it must be funded before the first billable call.
2.4 Request the Addresses API license. Accept the updated terms, request the license, sign the order form and license agreement through DocuSign, and wait for USPS to countersign. Nothing about address validation authorizes until both parties have signed.
2.5 Link credentials and refresh. After countersignature, link your API credentials to the license, refresh your claims, then refresh your OAuth token. A stale token issued before the link still carries the old entitlements.
2.6 For labels only: complete CRID and MID enrollment. Label creation needs a CRID, a Master MID, a label MID and an EPS account number, and those feed a second token that is not the OAuth Bearer token. The CRID and MID guide walks each screen; the OAuth guide covers the second token.
Phase 3: authentication
Web Tools put a USERID in the query string. v3 uses OAuth 2.0 client credentials.
# Dead since 2026-01-25. USERID inside an XML body.
import requests
import xml.etree.ElementTree as ET
xml = f"""<AddressValidateRequest USERID="{USERID}">
<Address>
<Address1></Address1>
<Address2>1600 Pennsylvania Ave NW</Address2>
<City>Washington</City>
<State>DC</State>
<Zip5>20500</Zip5>
</Address>
</AddressValidateRequest>"""
resp = requests.get(
"https://secure.shippingapis.com/ShippingAPI.dll",
params={"API": "Verify", "XML": xml},
)
root = ET.fromstring(resp.text)
city = root.find(".//City").text
# OAuth token, then a JSON GET.
import requests
token = requests.post(
"https://apis.usps.com/oauth2/v3/token",
data={
"grant_type": "client_credentials",
"client_id": CONSUMER_KEY, # portal calls it Consumer Key
"client_secret": CONSUMER_SECRET, # portal calls it Consumer Secret
},
).json()["access_token"]
resp = requests.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}"},
)
city = resp.json()["address"]["city"]
# One header, no token lifecycle.
import requests
resp = requests.get(
"https://api.revaddress.com/api/address/validate",
params={
"streetAddress": "1600 Pennsylvania Ave NW",
"city": "Washington",
"state": "DC",
"ZIPCode": "20500",
},
headers={"X-API-Key": "rv_live_your_key_here"},
)
city = resp.json()["address"]["city"]
3.1 Cache the token. It lasts 28800 seconds, which is 8 hours. Every token request counts against the 60-per-hour quota.
3.2 Refresh early, not on the deadline. USPS can invalidate a token before its stated expiry. Treat the token as dead at expires_in minus 1800 seconds, and re-fetch on any 401 rather than trusting the countdown.
3.3 Lock around the fetch. In a multi-process or multi-threaded service, an uncoordinated refresh sends every worker to the token endpoint at once and turns one expiry into a burst of 429s.
Phase 4: migrate one endpoint at a time
Convert one endpoint, deploy it, watch it in production, then take the next. One endpoint per release keeps each change traceable.
4.1 Address validation
Field names changed. Web Tools used Address2 for the street line:
| Web Tools field | v3 field |
|---|---|
Address1 (secondary unit) |
secondaryAddress |
Address2 (street) |
streetAddress |
City |
city |
State |
state |
Zip5 |
ZIPCode |
Zip4 |
ZIPPlus4 (also returned in the response) |
The parameter requirement also changed. v3 requires streetAddress plus either city and state, or ZIP Code, or all three.
4.2 Rate lookups
RateV4Request becomes a POST with a JSON body. Origin ZIP, destination ZIP and weight are required; dimensions are part of the USPS request, so send them rather than letting a wrapper guess.
Rates are a BYOK capability on the managed route: the call below runs against your own USPS credentials, not a pooled account.
<RateV4Request USERID="xxx">
<Package ID="1ST">
<Service>PRIORITY</Service>
<ZipOrigination>10001</ZipOrigination>
<ZipDestination>90210</ZipDestination>
<Pounds>2</Pounds>
<Ounces>0</Ounces>
<Container>RECTANGULAR</Container>
<Width>6</Width>
<Length>12</Length>
<Height>4</Height>
</Package>
</RateV4Request>
curl -X POST "https://api.revaddress.com/api/rates" \
-H "X-API-Key: rv_live_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"originZIPCode": "10001",
"destinationZIPCode": "90210",
"weight": 2,
"length": 12,
"width": 6,
"height": 4,
"mailClass": "USPS_GROUND_ADVANTAGE"
}'
4.3 Tracking
TrackV2Request and TrackFieldRequest become a GET, and the payload shape changed from deeply nested XML elements to flat JSON. Every field accessor in your tracking parser needs updating.
Tracking is also BYOK on the managed route. The webhook plumbing and the delivery of events are part of the subscription; the USPS tracking data behind them runs on your license.
curl "https://api.revaddress.com/api/tracking/9400111899223100001234" \
-H "X-API-Key: rv_live_your_key_here"
Phase 5: map the error surface
Web Tools returned errors as XML with inconsistent structures and numeric codes. v3 returns a structured JSON error object.
{
"apiVersion": "v3",
"error": {
"code": "ADDRESS_NOT_FOUND",
"message": "Address Not Found.",
"source": "USPS"
}
}
| Web Tools condition | v3 equivalent | What to do |
|---|---|---|
<Error><Number>-2147219401</Number> |
404 with ADDRESS_NOT_FOUND |
Input problem. Surface it to the user, do not retry. |
<Error><Number>80040B19</Number> |
400 |
A required field is missing or malformed. Fix the request. |
Authorization failure |
401 |
Token expired or credentials wrong. Refresh once, then fail. |
| No equivalent | 403 |
Your application lacks the scope, or enrollment is incomplete. |
| No equivalent | 429 |
Rate limited. Back off; do not retry immediately. |
| Empty response or timeout | 5xx |
Upstream problem. Retry with backoff. |
The last three rows are new code. Web Tools had no OAuth and no rate limit, so a 401 refresh and a 429 backoff are new paths, not translations of old ones.
One overload to know: USPS returns 404 for Address Not Found. A 404 carrying a USPS error body means you reached the API and it answered about a bad address. A 404 with no USPS error body means your path is wrong.
Phase 6: budget the throughput
- Peak-hour call count measured against the 60-per-hour default
- Address results cached, with a TTL your business can defend
- Non-urgent calls queued out of the checkout path
- 429 handling with exponential backoff and a retry ceiling
- A rate-limit increase requested from USPS in writing if the numbers do not fit, with the response time built into the plan
- Rate-limit headers read at runtime rather than a ceiling hardcoded in the client
The rate limit guide has the architecture patterns in full.
Phase 7: the test checklist
Run all of these against the testing host before you point anything at production.
- Address validation returns
DPVConfirmation: "Y"for a known-good address - Address validation returns
DPVConfirmation: "N"for a known-bad address - An address with a missing unit number returns
D, and your code prompts for the unit - An address with a wrong unit number returns
S, and your code offers a correction rather than re-prompting from empty - A request with
streetAddressand no city, state or ZIP returns 400 and is handled - Rate lookup returns pricing between two valid ZIPs for your primary mail class
- Tracking returns status for a known tracking number, and every field accessor resolves
- An expired token triggers exactly one refresh and one retry, not a loop
- A 429 backs off instead of retrying immediately
- A 404 with a USPS error body is treated as a bad address, not an outage
- Standardized output is title-cased before it reaches the interface
- If using labels: the non-production label flow is verified before live postage is switched on
Phase 8: cut over, then clean up
- Deploy behind a flag so a bad response can be reverted without a release
- Monitor error rates and 401/429 counts for 24 hours
- Remove the Web Tools USERID from every environment and secret store
- Remove
secure.shippingapis.comfrom allowlists, firewall rules and outbound proxies - Remove XML parsing dependencies that existed only for USPS
- Delete the old code path once the flag has been on for a week
Five details to set before go-live
Title-case standardized output. v3 returns standardized uppercase with ZIP+4 appended. If the interface renders the raw response, users see 1600 PENNSYLVANIA AVE NW where they typed mixed case. Title-case it on your side, and keep the standardized form for the label.
Read Y as a confirmed address record. USPS is explicit in its own specification that DPVConfirmation: "Y" does not necessarily imply USPS delivers to that address. Carrier route codes R777 and R779 in particular can mean the recipient collects mail elsewhere.
Assert on a populated tracking field. The flat JSON shape means old nested accessors resolve to undefined rather than throwing, so the test needs a populated field to check.
Fund the payment account before go-live. Address usage fees debit from a funded Enterprise Payment Account, so fund it before the first production request.
Route other carriers separately. RevAddress is USPS-only, so multi-carrier shipping runs USPS through the cheapest route and a separate integration for the rest; the shipping API comparison compares the options.
Timeline
| Date | What changed |
|---|---|
| Jan 25, 2026 | USPS Web Tools XML API stopped answering |
| Apr 2026 | USPS API Access Control tightened, adding CRID and MID validation |
| Aug 1, 2026 | Addresses API moved behind a signed license and consumption-tier billing |
Start here
- Get a free API key and build the client while the paperwork clears. 1,000 requests a month, no credit card, Census-based standardization and geocoding with no USPS license at all.
- Web Tools shutdown covers what broke and why.
- Endpoint mapping is the endpoint-by-endpoint reference.
- Address validation quickstart is the working call, start to finish.
- OAuth troubleshooting covers every 401 and 403.
- API reference has the schemas with a live try-it.
- Pricing marks which capabilities run on your own USPS license.
Questions
- When did USPS Web Tools stop working?
- January 25, 2026. Requests to secure.shippingapis.com return errors, and any integration built on a USERID and XML request bodies is dead. The replacement is the USPS v3 REST API on apis.usps.com.
- How long does a Web Tools to v3 migration take?
- The code takes days; the credentials are the long pole. Address validation now requires a signed Addresses API license countersigned by USPS, and signatures move at legal-department speed on both sides, so start the paperwork before you start the refactor.
- How long does a USPS v3 OAuth token last?
- 28800 seconds, which is 8 hours. Cache it with a buffer and refresh early. USPS can invalidate a token before its stated expiry, so a strict countdown against expires_in produces intermittent 401s.
- Do I still need CRID and MID after migrating?
- For labels and payment, yes. Label creation needs a CRID, a Master MID, a label MID and an Enterprise Payment Account number to obtain a Payment Authorization Token, which is a second token separate from the OAuth Bearer token.
- Is the USPS v3 address API free like Web Tools was?
- No. Effective August 1, 2026 the Addresses API requires a signed license agreement and bills on a monthly consumption curve, with a $10 flat fee for the first tier and no free allowance. Web Tools was free and unmetered; that is the single biggest budget change in this migration.
Read next
-
USPS Web Tools Shut Down on January 25, 2026: What Broke and the Fastest Way to the v3 API
USPS retired Web Tools on January 25, 2026. If ShippingAPI.dll, USERID, or secure.shippingapis.com is failing, map the XML route to v3 REST here.
-
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.
-
USPS 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.