Skip to content
All Posts
Migration

USPS Web Tools Migration Checklist (2026)

·Updated ·14 min read·By RevAddress·Migration

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.

This is the checklist, in the order the work actually has to happen. Read the next paragraph before you plan the sprint, because the sequencing changed after the first version of this guide.

Read this before you scope it

The code is the short part. The paperwork is the long pole. 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. None of that is true of the replacement.

Teams that scope this as a refactor get to a working client in a week and then wait on a countersignature. 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

Two honest 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. This does not remove the license from the picture, it moves the plumbing. 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 US Census Bureau reference data, runs with no license at all, which means you can build and test the whole client before the paperwork clears.

Most teams pick direct if they are already deep in USPS enrollment for labels and payment, and managed if they want the enrollment to be the only USPS work they 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 traps in that table. ZipCodeLookupRequest and CityStateLookupRequest were often used interchangeably against Web Tools; in v3 they are different 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 peak hour is where a daily average hides the failure. A checkout that makes four USPS calls per order breaks 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; the label change has cost more than one afternoon.

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, which is earlier than most plans assume.

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.

Authentication, before and after
# 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

3.1 Cache the token. It lasts 28800 seconds, which is 8 hours. Every token request counts against the 60-per-hour quota, so a service that re-authenticates per call exhausts its rate limit before it does any work.

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. Batching the whole surface into one release removes your ability to tell which change broke what.

4.1 Address validation

Field names changed. Web Tools used Address2 for the street line, which is the single most common source of a silent empty result:

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. Web Tools tolerated a street line with a ZIP alone in more cases; v3 returns a 400.

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.

Rate lookup
<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>

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; none of them will fail loudly.

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.

Package trackingbash
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.

v3 error responsejson
{
"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 the migration risk. Web Tools had no OAuth and no rate limit, so no existing Web Tools integration has ever handled a 401 refresh or a 429 backoff. Those are new code 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. Most addresses do not change.
  • 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 streetAddress and 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.com from 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

Pitfalls worth naming

Addresses come back looking wrong. 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.

A Y is treated as a delivery guarantee. 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.

Tracking silently returns nothing. The flat JSON shape means old nested accessors resolve to undefined rather than throwing. A tracking migration that ships without an assertion on a populated field looks healthy and displays blank.

The payment account is empty at go-live. Address usage fees debit from a funded Enterprise Payment Account, and non-payment has consequences beyond a declined call. Fund it before the first production request.

You need carriers other than USPS. RevAddress is USPS-only. For multi-carrier, run USPS through the cheapest route and a separate integration for the rest; the shipping API comparison lays out the tradeoffs.

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

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 is 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.