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-tested 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 and usps-webtools npm package are broken. EasyPost March 17 plan enforcement is also live now, so migration pressure is no longer theoretical.
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}")
# → $X.XX (actual rate varies by zone/weight) // ❌ 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}`);
// → $X.XX (actual rate varies by zone/weight) 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 after March 17 plan enforcement, 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 stays close to the USPS v3 schema, so you're working with USPS-native data structures without extra middleware concepts. For the full comparison, see EasyPost vs RevAddress pricing compared.
Want Someone Else to Handle the Infrastructure?
The SDK gives you USPS-native v3 access with automatic token management. If you also want managed caching, request smoothing, 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 the free proof path, and deploy when ready. Start with validation and rate surfaces, then expand into protected workflows once the USPS integration is grounded.
Managed tracking routes remain proof-depth-limited publicly until scan-backed Stage B closes.
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 Enforcement: Migration Options
Cost comparison, migration options, and working code after EasyPost's March 17 plan enforcement.