Skip to content
All Posts
Troubleshooting

USPS v3 OAuth Troubleshooting: Every Error and How to Fix It

· 6 min read · By RevAddress · Troubleshooting

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:

TokenEndpointLifetimeRequired for
OAuth Bearer Token/oauth2/v3/token8 hours (28800s)Every API call
Payment Authorization Token/payments/v3/payment-authorization8 hoursLabel 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.

Request an OAuth Bearer Token
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:

Token response
{
"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:

Authenticated API call
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:

  1. Token expired. The token lifetime is 28800 seconds (8 hours) but USPS can revoke tokens early. A strict expires_in countdown is not reliable.
  2. Missing “Bearer” prefix. The header must be Authorization: Bearer <token>, not Authorization: <token>.
  3. Wrong token for the endpoint. Using a Payment Authorization Token where a Bearer Token is required (or vice versa).
  4. 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.

Token expiry with 30-minute buffer (Python)
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:

  1. 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 labels scope.
  2. Account not enrolled. Some USPS APIs (especially labels and payment) require additional BCG enrollment steps beyond application registration.
  3. 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.

Request a Payment Authorization Token
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"
    }
  ]
}'
Payment Authorization response
{
"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.

Token caching with file persistence
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 0600 permissions — 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

SymptomLikely causeFix
401 on every callRe-authenticating per requestImplement token caching
401 on label calls onlyMissing Payment Authorization TokenEnroll in COP, fetch second token
403 on label endpointApplication not enrolled for Labels APIEnable Labels in BCG application
403 in production, works in sandboxWrong base URLUse apis.usps.com for production
429 after deployToken cache removed or disabledRe-enable caching, check rate limit
”invalid_client” on token requestWrong client_id or client_secretVerify credentials in BCG dashboard
Token works, label still 401Using Bearer Token where Payment Auth Token requiredAdd paymentAuthorizationToken to label request body
401 after 7.5 hours30-minute buffer not appliedRefresh at expires_in - 1800, not expires_in
COP enrollment failsClaims linking not completedComplete manually at cop.usps.com — no API path
Sandbox 403Using production credentials against sandbox URLSeparate 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.

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.