USPS v3 Migration Guide:
Node.js & Python SDK
The USPS Web Tools XML API died on January 25, 2026. If your code still imports usps-api, usps-webtools, or sends XML to production.shippingapis.com, it's broken. This guide walks through the complete migration from legacy Web Tools to the v3 REST API using production-ready SDKs for Python and Node.js. Side-by-side code, endpoint mapping, OAuth setup, and a testing checklist.
Web Tools XML API — permanently retired
All Web Tools endpoints return errors as of January 25, 2026. The usps-api PyPI package (13,725 downloads/month) and usps-webtools npm package are broken. Additionally, EasyPost auto-enrolls accounts on March 17 — 7 days away.
What Changed: XML to REST in 60 Seconds
The migration surface is smaller than it looks. The core operations (validate addresses, get rates, create labels, track packages) all still exist. What changed is the transport layer and authentication model:
| Aspect | Web Tools (Dead) | v3 REST (Current) |
|---|---|---|
| Format | XML request/response | JSON request/response |
| Authentication | User ID in XML body | OAuth 2.0 client_credentials |
| Base URL | production.shippingapis.com | apis.usps.com |
| Rate Limits | None enforced | 60 req/hr default |
| Token Lifecycle | None (static User ID) | 8-hour access tokens, refresh required |
| Field Naming | PascalCase XML tags | camelCase JSON keys |
The good news: if you use an SDK, you don't need to deal with OAuth token management, rate limit handling, or field name translation. The SDK abstracts all of it. For the complete endpoint-by-endpoint mapping, see the full endpoint mapping reference.
Step 1: Get Your USPS v3 Credentials (10 Minutes)
The v3 API uses OAuth 2.0 with a Consumer Key (client ID) and Consumer Secret (client secret). These are different from your old Web Tools User ID.
Create an account at the USPS Developer Portal
Go to developer.usps.com and create a developer account. Use a business email — USPS sometimes rejects free email providers for production access.
Create an application
In the developer portal, create a new application. You'll get a Consumer Key (this is your client ID) and Consumer Secret. Save both — the secret is only shown once.
Complete CRID registration (if you need labels)
Address validation and tracking work immediately. For label creation, you need a CRID (Customer Registration ID) and Mailer ID (MID) linked through the USPS Customer Onboarding Portal. See our complete CRID/MID enrollment guide.
Test the OAuth flow
Before writing any code, verify your credentials work:
curl -X POST https://apis.usps.com/oauth2/v3/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials&client_id=YOUR_KEY&client_secret=YOUR_SECRET"
# Success: {"access_token":"eyJ...", "token_type":"Bearer", "expires_in":28800}
# 28800 seconds = 8 hours Getting a 401? See the OAuth troubleshooting guide — the most common mistake is sending JSON instead of form-urlencoded.
Step 2: Install the SDK (1 Minute)
Both SDKs handle OAuth token management (8-hour lifetime, automatic refresh before expiry), field name translation (snake_case Python / camelCase JS to USPS camelCase), and retry logic for 429 rate limit errors.
Uninstall dead packages first
If your project has usps-api, usps-webtools, or usps-webtools-promise in your dependencies, remove them. They target a dead API and will never work again. See our dead package replacement guide.
Step 3: Code Migration — Side by Side
Below is every common operation with the old Web Tools code on the left and the new v3 SDK code on the right. Copy-paste ready.
Address Validation
# ❌ This no longer works
from usps import USPSApi, Address
usps = USPSApi("YOUR_USER_ID")
address = Address(
name="White House",
address_1="1600 Pennsylvania Ave NW",
city="Washington",
state="DC",
zipcode="20500",
)
result = usps.validate_address(address)
print(result.result)
# → ConnectionError: API endpoint dead # ✓ Current — works today
from usps_v3 import USPSClient
client = USPSClient(
client_id="your_consumer_key",
client_secret="your_consumer_secret",
)
result = client.addresses.validate(
street_address="1600 Pennsylvania Ave NW",
city="Washington",
state="DC",
zip_code="20500",
)
print(result.address.street_address)
print(result.address.zip_code)
# → 1600 PENNSYLVANIA AVE NW
# → 20500-0005 // ❌ This no longer works
const USPS = require('usps-webtools');
const usps = new USPS({
userId: 'YOUR_USER_ID'
});
usps.verify({
street1: '1600 Pennsylvania Ave NW',
city: 'Washington',
state: 'DC',
zip: '20500',
}, (err, result) => {
console.log(result);
});
// → ECONNREFUSED // ✓ Current — works today
import { USPSClient } from 'usps-v3';
const client = new USPSClient({
clientId: 'your_consumer_key',
clientSecret: 'your_consumer_secret',
});
const result = await client.addresses.validate({
streetAddress: '1600 Pennsylvania Ave NW',
city: 'Washington',
state: 'DC',
zipCode: '20500',
});
console.log(result.address.streetAddress);
console.log(result.address.zipCode);
// → 1600 PENNSYLVANIA AVE NW
// → 20500-0005 Package Tracking
# ❌ Dead
result = usps.track("9400111899223")
print(result.result) # ✓ Current
result = client.tracking.get("9400111899223")
print(result.status_summary)
print(result.tracking_events[0].event_type)
# → "Your item has been delivered" // ❌ Dead
usps.track('9400111899223',
(err, result) => {
console.log(result);
}); // ✓ Current
const result = await client.tracking.get(
'9400111899223'
);
console.log(result.statusSummary);
console.log(result.trackingEvents[0]); Rate Shopping / Price Lookup
# ❌ Dead — used RateV4 XML endpoint
# No Python SDK for this existed
# Most devs used raw XML with requests
xml = """<RateV4Request USERID="...">
<Package ID="1">
<Service>PRIORITY</Service>
<ZipOrigination>94105</ZipOrigination>
<ZipDestination>20500</ZipDestination>
<Pounds>1</Pounds>
<Ounces>0</Ounces>
</Package>
</RateV4Request>""" # ✓ Current
rates = client.prices.rate(
origin_zip="94105",
destination_zip="20500",
weight=16, # ounces
mail_class="PRIORITY_MAIL",
processing_category="MACHINABLE",
)
print(f"${rates.total_price}")
# → $8.95 // ❌ Dead — callback-based XML
usps.pricingRateV4({
Service: 'PRIORITY',
ZipOrigination: '94105',
ZipDestination: '20500',
Pounds: 1, Ounces: 0,
}, (err, result) => {
console.log(result.Postage);
}); // ✓ Current — async/await, typed
const rates = await client.prices.rate({
originZip: '94105',
destinationZip: '20500',
weight: 16, // ounces
mailClass: 'PRIORITY_MAIL',
processingCategory: 'MACHINABLE',
});
console.log(`${rates.totalPrice}`);
// → $8.95 Service Standards / Delivery Estimates
standards = client.standards.get(
origin_zip="94105",
destination_zip="20500",
mail_class="PRIORITY_MAIL",
accept_date="2026-03-10",
)
print(standards.delivery_date)
# → 2026-03-13 const standards = await client.standards.get({
originZip: '94105',
destinationZip: '20500',
mailClass: 'PRIORITY_MAIL',
acceptDate: '2026-03-10',
});
console.log(standards.deliveryDate);
// → 2026-03-13 Step 4: Complete Endpoint Mapping
Every Web Tools XML endpoint mapped to its v3 REST equivalent and the corresponding SDK method:
| Operation | Web Tools (Dead) | SDK Method |
|---|---|---|
| Address validate | AddressValidateRequest | client.addresses.validate() |
| Track package | TrackV2Request | client.tracking.get() |
| Get rates | RateV4Request | client.prices.rate() |
| Create label | eVSRequest | client.labels.create() |
| City/state lookup | CityStateLookupRequest | client.addresses.lookup_city_state() |
| ZIP code lookup | ZipCodeLookupRequest | client.addresses.lookup_zip() |
| Service standards | SDCGetLocationsRequest | client.standards.get() |
| Post office locator | POLocatorRequest | client.locations.search() |
Full endpoint-by-endpoint mapping with request/response examples: USPS Web Tools to v3 REST Endpoint Mapping
Step 5: Authentication Migration
The biggest conceptual change. Web Tools used a static User ID embedded in XML. v3 uses OAuth 2.0 client_credentials flow with tokens that expire after 8 hours.
1. Obtain token
POST https://apis.usps.com/oauth2/v3/token
Body: grant_type=client_credentials
&client_id=YOUR_KEY
&client_secret=YOUR_SECRET
Content-Type: application/x-www-form-urlencoded
↑ NOT application/json — this is the #1 mistake
2. Use token
GET https://apis.usps.com/addresses/v3/address?...
Authorization: Bearer eyJhbGciOiJSUzI1NiIs...
↑ Token valid for 8 hours (28,800 seconds)
3. Refresh before expiry
SDK refreshes at 7h 30m automatically.
If you're rolling your own: check expires_in,
request new token 30 min before expiry.
4. Handle 401
On 401 response → token expired or revoked.
Request new token, retry original request once.
SDK handles this automatically. If you're using the SDK, you don't need to manage tokens. Both the Python and Node.js SDKs handle the complete OAuth lifecycle: initial token fetch, caching, proactive refresh before expiry, and automatic retry on 401. The code examples above work out of the box without any token management code.
Step 6: Testing Checklist
Before deploying to production, verify each endpoint against the USPS sandbox. The SDK uses production URLs by default — for sandbox testing, configure the base URL.
from usps_v3 import USPSClient
from usps_v3.exceptions import USPSError
client = USPSClient(
client_id="your_consumer_key",
client_secret="your_consumer_secret",
)
# Test 1: Address validation
try:
result = client.addresses.validate(
street_address="1600 Pennsylvania Ave NW",
city="Washington", state="DC", zip_code="20500",
)
assert "PENNSYLVANIA" in result.address.street_address
print("✓ Address validation working")
except USPSError as e:
print(f"✗ Address validation failed: {e}")
# Test 2: Rate lookup
try:
rates = client.prices.rate(
origin_zip="94105", destination_zip="20500",
weight=16, mail_class="PRIORITY_MAIL",
processing_category="MACHINABLE",
)
assert float(rates.total_price) > 0
print(f"✓ Rate lookup working: ${rates.total_price}")
except USPSError as e:
print(f"✗ Rate lookup failed: {e}") Production Cutover Playbook
Your tests pass in sandbox. Time to cut over to production. Follow this sequence:
Deploy with feature flag
Deploy the new SDK code behind a feature flag. Route 1% of traffic through v3, 99% through your existing path (even if it's broken — better to know the error rate).
Monitor for 24 hours
Watch for 401s (credential issues), 429s (rate limits), and 400s (field mapping errors). The SDK logs these automatically.
Ramp to 100%
If error rate is <0.1% after 24h, ramp to 100%. Remove the old XML code path. Update environment variables: swap USPS_USER_ID for USPS_CLIENT_ID + USPS_CLIENT_SECRET.
Request rate limit increase
Once live on v3, email USPS at emailus.usps.com with your CRID, app name, and actual usage numbers (now you have real data, not estimates). See rate limit strategies.
Coming from EasyPost? The Same SDKs Work
If you're migrating away from EasyPost before the March 17 auto-enrollment deadline, the usps-v3 SDK is a drop-in replacement for EasyPost's USPS operations. No abstraction tax, no per-label fees, no $20/month BYOCA surcharge.
| EasyPost Method | usps-v3 SDK Method |
|---|---|
| client.address.create(verify=["delivery"]) | client.addresses.validate() |
| client.Shipment.create() + .buy() | client.labels.create() |
| client.Tracker.create() | client.tracking.get() |
| shipment.lowestRate() | client.prices.rate() |
| EasyPostClient("API_KEY") | USPSClient(client_id, client_secret) |
Key difference: EasyPost wraps USPS behind a proprietary abstraction layer with per-label pricing. The usps-v3 SDK maps directly to the USPS v3 REST API schema, so you're working with USPS native data structures. Zero vendor lock-in. For the full comparison, see EasyPost vs RevAddress pricing compared.
Want Someone Else to Handle the Infrastructure?
The SDK gives you direct USPS v3 access with automatic token management. If you also want caching, rate-limit smoothing (300-600 req/min), retry logic, and BYOK credential isolation handled at the infrastructure level, RevAddress wraps the v3 API with those features built in.
usps-v3 SDK works with both direct USPS and RevAddress. Change the base URL and API key — your code stays identical. Start the migration now
Install the SDK, test in sandbox, deploy when ready. Free sandbox API key in 30 seconds — no credit card. The Web Tools API is already dead and EasyPost auto-enrolls in 7 days.
Related Articles
USPS API Rate Limits: From 6,000/min to 60/hour
Why it happened, the real-world impact, and five architecture patterns with code.
usps-api / usps-webtools Are Dead
One-command migration from broken PyPI/npm packages to usps-v3.
USPS OAuth 401 Troubleshooting Guide
Fix authentication errors with the v3 API. Complete debugging checklist.
EasyPost March 17 Deadline: Migration Options
EasyPost auto-enrolls in 7 days. Cost comparison, code examples, and migration checklist.