USPS v3 OAuth Troubleshooting: Every Error and How to Fix It
The USPS v3 API uses OAuth 2.0 client credentials — but most authentication failures come from one misunderstood detail: USPS has two separate tokens, not one. Miss the second token and your label calls return 401s even when your address and tracking calls are working fine.
This guide covers the full token lifecycle, every error you’ll hit, and exactly how to fix each one.
The two-token lifecycle
USPS v3 authentication is split across two independent token systems:
| Token | Endpoint | Lifetime | Required for |
|---|---|---|---|
| OAuth Bearer Token | /oauth2/v3/token | 8 hours (28800s) | Every API call |
| Payment Authorization Token | /payments/v3/payment-authorization | 8 hours | Label creation only |
The OAuth Bearer Token is the standard client credentials grant. Every endpoint — addresses, tracking, rates, locations, standards — requires it in the Authorization header.
The Payment Authorization Token is a second, separate token returned from a different endpoint. It requires enrollment-specific credentials (CRID, MID, EPS account number) that come from your Business Customer Gateway account. Without it, any call to /labels/v3/label returns 401 regardless of whether your Bearer Token is valid.
Most developers discover the second token the hard way — after their address and tracking integrations are working perfectly, label creation fails with a cryptic 401.
Getting your OAuth Bearer Token
The Bearer Token uses the standard OAuth 2.0 client credentials flow. Your client_id and client_secret come from your registered application in the USPS Business Customer Gateway.
curl -X POST "https://apis.usps.com/oauth2/v3/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials" \
-d "client_id=YOUR_CLIENT_ID" \
-d "client_secret=YOUR_CLIENT_SECRET" The response looks like this:
{
"access_token": "eyJraWQiOiJNV...",
"token_type": "Bearer",
"issued_at": "1741564800000",
"expires_in": "28800",
"status": "approved",
"scope": "addresses tracking labels prices"
} Use the token in every subsequent request:
curl "https://apis.usps.com/addresses/v3/address?streetAddress=1600+Pennsylvania+Ave+NW&city=Washington&state=DC" \
-H "Authorization: Bearer eyJraWQiOiJNV..." Error: 401 Unauthorized
Symptoms: Any API endpoint returns 401 with a body containing "error": "invalid_token" or "message": "Unauthorized".
Causes:
- Token expired. The token lifetime is 28800 seconds (8 hours) but USPS can revoke tokens early. A strict
expires_incountdown is not reliable. - Missing “Bearer” prefix. The header must be
Authorization: Bearer <token>, notAuthorization: <token>. - Wrong token for the endpoint. Using a Payment Authorization Token where a Bearer Token is required (or vice versa).
- Re-authenticating every call. If your code fetches a new token on every request, race conditions and clock skew can cause intermittent 401s.
Fix:
Cache the token with a 30-minute expiry buffer — not 5 minutes. If expires_in is 28800, treat the token as expired at 28200 seconds (7h 50m). Check the cached token before every call and only re-authenticate when the buffer is hit.
import time
class TokenCache:
def __init__(self):
self._token = None
self._expires_at = 0
self._buffer = 1800 # 30 minutes in seconds
def get_token(self, client_id, client_secret):
if self._token and time.time() < self._expires_at - self._buffer:
return self._token
self._token = self._fetch_token(client_id, client_secret)
return self._token
def _fetch_token(self, client_id, client_secret):
import httpx
r = httpx.post(
"https://apis.usps.com/oauth2/v3/token",
data={
"grant_type": "client_credentials",
"client_id": client_id,
"client_secret": client_secret,
},
)
data = r.json()
self._expires_at = time.time() + int(data["expires_in"])
return data["access_token"] Error: 403 Forbidden
Symptoms: The request is authenticated (valid Bearer Token) but returns 403 with "error": "insufficient_scope" or "message": "Forbidden".
Causes:
- Wrong scope requested. When you registered your USPS application in BCG, you selected which APIs to enable. If you enabled addresses and tracking but not labels, your token will not have the
labelsscope. - Account not enrolled. Some USPS APIs (especially labels and payment) require additional BCG enrollment steps beyond application registration.
- Sandbox vs. production mismatch. Sandbox tokens do not work against production endpoints (
apis.usps.com) and vice versa.
Fix:
Check the scope field in your token response. It lists exactly which APIs your token covers. If labels is missing, log into BCG, open your application, and request access to the Labels API. USPS approves these manually — turnaround is typically 1-3 business days.
For USPS testing, use https://apis-tem.usps.com/ before switching to apis.usps.com. Keep testing and production credentials separate.
Error: 429 Too Many Requests
Symptoms: API calls return 429 after a burst of requests. Often seen during batch operations or after a code deploy that removed token caching.
Causes:
USPS defaults to 60 requests per hour per application across most v3 endpoints. This limit applies to the total request count — not just authentication requests. Fetching a new token on every API call burns through this quota instantly.
Fix:
Token caching is mandatory, not optional. Every token fetch counts against your rate limit. A single server with 100 address validations per hour will hit the cap in 1 minute if it re-authenticates per call.
If your volume exceeds 60 req/hr legitimately, request a rate limit increase through the USPS API support contact form at emailus.usps.com. Include your application name from BCG, your CRID, and your estimated request volume.
For immediate relief: implement an in-process token cache (see the caching section below) and queue requests with a delay between them.
Payment Authorization Token
The Payment Authorization Token is a separate credential required for label creation. It is not a scope on your Bearer Token — it is a different token from a different endpoint.
What you need before calling this endpoint:
- CRID — Customer Registration ID, assigned during BCG enrollment
- Master MID — Mailer ID at the master account level
- Label MID — Mailer ID specifically for label printing (may be the same as Master MID)
- EPS Account Number — Electronic Payment System account number
All four come from your BCG enrollment. If you do not have them, you cannot generate labels. The enrollment process for payment is called COP (Customer Online Payment) claims linking and must be completed manually at cop.usps.com — there is no API to do this programmatically.
curl -X POST "https://apis.usps.com/payments/v3/payment-authorization" \
-H "Authorization: Bearer YOUR_BEARER_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"roles": [
{
"roleName": "PAYER",
"CRID": "YOUR_CRID",
"MID": "YOUR_MASTER_MID",
"manifestMID": "YOUR_LABEL_MID",
"accountType": "EPS",
"accountNumber": "YOUR_EPS_ACCOUNT_NUMBER"
}
]
}' {
"paymentAuthorizationToken": "USPS-PO-PAYMENT-...",
"roles": [
{
"roleName": "PAYER",
"CRID": "12345678",
"MID": "900012345",
"accountType": "EPS",
"accountNumber": "XXXXXXXX",
"permit": {
"permitNumber": "XXXXXXXX",
"permitZIP": "XXXXX",
"permitZIP4": "XXXX"
}
}
]
} Pass the paymentAuthorizationToken in the label creation request body under imageInfo.receiptOption or as a top-level field depending on the label type. See the API reference for the exact label request schema.
Token caching best practices
Two-layer caching — in-memory for the current process, file-based for across restarts — eliminates the majority of authentication errors.
import json
import os
import time
import threading
import httpx
CACHE_PATH = "/tmp/.usps_token_cache"
BUFFER_SECS = 1800 # 30 minutes
_lock = threading.Lock()
_memory: dict = {}
def get_bearer_token(client_id: str, client_secret: str) -> str:
with _lock:
now = time.time()
# 1. Check in-memory
if _memory.get("expires_at", 0) - BUFFER_SECS > now:
return _memory["access_token"]
# 2. Check file cache
try:
with open(CACHE_PATH) as f:
cached = json.load(f)
if cached.get("expires_at", 0) - BUFFER_SECS > now:
_memory.update(cached)
return cached["access_token"]
except (FileNotFoundError, json.JSONDecodeError, KeyError):
pass
# 3. Fetch fresh token
r = httpx.post(
"https://apis.usps.com/oauth2/v3/token",
data={
"grant_type": "client_credentials",
"client_id": client_id,
"client_secret": client_secret,
},
)
r.raise_for_status()
data = r.json()
entry = {
"access_token": data["access_token"],
"expires_at": now + int(data["expires_in"]),
}
# Write file with restricted permissions
with open(CACHE_PATH, "w") as f:
json.dump(entry, f)
os.chmod(CACHE_PATH, 0o600)
_memory.update(entry)
return entry["access_token"] Key rules:
- Store the cache file with
0600permissions — it contains a live credential. - Use a 30-minute buffer, not 5 minutes. USPS tokens can be invalidated before their stated expiry.
- Lock around the fetch to prevent stampedes in multi-threaded or multi-process environments.
- The Payment Authorization Token needs the same caching treatment — it also lasts 8 hours and has the same early-revocation behavior.
Common gotchas — quick reference
| Symptom | Likely cause | Fix |
|---|---|---|
| 401 on every call | Re-authenticating per request | Implement token caching |
| 401 on label calls only | Missing Payment Authorization Token | Enroll in COP, fetch second token |
| 403 on label endpoint | Application not enrolled for Labels API | Enable Labels in BCG application |
| 403 in production, works in sandbox | Wrong base URL | Use apis.usps.com for production |
| 429 after deploy | Token cache removed or disabled | Re-enable caching, check rate limit |
| ”invalid_client” on token request | Wrong client_id or client_secret | Verify credentials in BCG dashboard |
| Token works, label still 401 | Using Bearer Token where Payment Auth Token required | Add paymentAuthorizationToken to label request body |
| 401 after 7.5 hours | 30-minute buffer not applied | Refresh at expires_in - 1800, not expires_in |
| COP enrollment fails | Claims linking not completed | Complete manually at cop.usps.com — no API path |
| Sandbox 403 | Using production credentials against sandbox URL | Separate sandbox credentials required from BCG |
RevAddress handles this for you
Every OAuth complexity above — the two-token lifecycle, token caching, 30-minute expiry buffers, COP enrollment abstraction, scope validation — is handled inside the RevAddress API. You send one request with your API key. We handle USPS authentication, retry on token expiry, and surface clean errors when your BCG enrollment is incomplete.
- Get a free API key — no credit card, start validating addresses in 60 seconds
- API reference — core routes, request schemas, response fields, and admin diagnostics
- Pricing — plans from free to enterprise
- USPS Web Tools shutdown guide — migrating off the old XML API
Keep the free proof wedge first
USPS v3 developer toolkit. Free tier for validation and rates. Flat monthly pricing.
Validation, ZIP+4, and rates are the free proof path. Labels, tracking, BYOK, and pickup stay protected until the workflow and proof gates are actually ready.