Skip to content
· 15 min read · Migration Guide

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.

1

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.

2

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.

3

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.

4

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.

Python
pip install usps-v3

# 67 tests, 9 API modules
# Automatic OAuth token caching
# snake_case → camelCase translation
# httpx + file-based token cache

PyPI · GitHub

Node.js
npm install usps-v3

// Full TypeScript type definitions
// Automatic OAuth token caching
// Native fetch, zero dependencies
// ESM and CommonJS support

npm · GitHub

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

Web Tools XML (Dead) — Python
# ❌ 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
v3 REST SDK — Python
# ✓ 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
Web Tools (Dead) — Node.js
// ❌ 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
v3 REST SDK — Node.js
// ✓ 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

Web Tools (Dead) — Python
# ❌ Dead
result = usps.track("9400111899223")
print(result.result)
v3 REST SDK — Python
# ✓ Current
result = client.tracking.get("9400111899223")
print(result.status_summary)
print(result.tracking_events[0].event_type)
# → "Your item has been delivered"
Web Tools (Dead) — Node.js
// ❌ Dead
usps.track('9400111899223',
  (err, result) => {
    console.log(result);
});
v3 REST SDK — Node.js
// ✓ Current
const result = await client.tracking.get(
  '9400111899223'
);
console.log(result.statusSummary);
console.log(result.trackingEvents[0]);

Rate Shopping / Price Lookup

Web Tools (Dead) — Python
# ❌ 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>"""
v3 REST SDK — Python
# ✓ 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
Web Tools (Dead) — Node.js
// ❌ Dead — callback-based XML
usps.pricingRateV4({
  Service: 'PRIORITY',
  ZipOrigination: '94105',
  ZipDestination: '20500',
  Pounds: 1, Ounces: 0,
}, (err, result) => {
  console.log(result.Postage);
});
v3 REST SDK — Node.js
// ✓ 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

Python
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
Node.js
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.

OAuth 2.0 token lifecycle
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.

Address validation: Test with known good addresses (1600 Pennsylvania Ave NW, 20500), known bad addresses (123 Fake St), and addresses that require standardization (abbreviations, missing ZIP+4)
Tracking: Test with a real tracking number from a recent shipment. Verify all event fields are populated in your data model.
Rate lookup: Compare returned rates against postcalc.usps.com for the same origin/destination/weight.
Error handling: Test 401 (bad credentials), 429 (rate limit), and 400 (malformed request) responses. The SDK should handle 401 and 429 automatically.
Token refresh: Verify your integration survives an 8+ hour session without manual intervention. Run a long-running test or set the token TTL to 60 seconds in a test environment.
Rate limits: Confirm your caching and queuing strategy works under the 60 req/hr default. See rate limit architecture guide for patterns.
Quick integration test — Python
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:

1

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

2

Monitor for 24 hours

Watch for 401s (credential issues), 429s (rate limits), and 400s (field mapping errors). The SDK logs these automatically.

3

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.

4

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.

Same SDK, same code. The usps-v3 SDK works with both direct USPS and RevAddress. Change the base URL and API key — your code stays identical.
Flat pricing. No per-label fees. No overage charges. Starter at $29/mo, Growth at $79/mo, Pro at $199/mo. See full pricing.
BYOK included at every tier. Use your own USPS credentials, your rate limits, your MIDs. AES-GCM encrypted. No $20/month surcharge (unlike EasyPost BYOCA).

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