Skip to content
All Posts
Troubleshooting

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

·Updated ·9 min read·By RevAddress·Troubleshooting

USPS v3 authentication fails in two ways that look identical from the outside.

The first is structural: USPS issues two separate tokens, not one. Label calls return 401 while address and tracking calls work fine, and nothing in the error says why.

The second is new. Since August 1, 2026 the Addresses API sits behind a signed license agreement, and the USPS Developer Portal states plainly that “customers who did not complete onboarding no longer have access to the Addresses API.” Correct credentials no longer guarantee a 200 on address calls.

Below: the token lifecycle, then each error code with the fix.

Three portals, three different things

Half of USPS auth debugging is knowing which system owns the thing you are missing. They are separate logins on separate hosts.

Portal Host Issues
Developer Portal developers.usps.com Your client_id and client_secret, and the API products attached to your app
Business Customer Gateway (BCG) gateway.usps.com CRID, Mailer IDs, Enterprise Payment Account
Business Portal / Customer Onboarding Portal (COP) cop.usps.com The Addresses API license, and claims linking that pushes CRID, MIDs, and EPA into your token

Note the expansion: COP is the Customer Onboarding Portal. It is not a payment system and not a certificate of posting, and searching for either will send you somewhere useless.

The two-token lifecycle

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, including addresses, tracking, rates, locations, and standards, requires it in the Authorization header.

The Payment Authorization Token comes from a different endpoint and travels in a different header. It requires enrollment-specific credentials (CRID, MID, EPS account number) issued through BCG. Without it, any call to /labels/v3/label returns 401 no matter how valid your Bearer Token is.

Most developers meet the second token the hard way, after addresses and tracking have been working in production for weeks.

Getting your OAuth Bearer Token

Your client_id and client_secret come from your app in the Developer Portal at developers.usps.com. This is the credential pair people most often go looking for in BCG, where it does not exist.

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 responsejson
{
"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 callbash
curl "https://apis.usps.com/addresses/v3/address?streetAddress=1600+Pennsylvania+Ave+NW&city=Washington&state=DC" \
-H "Authorization: Bearer eyJraWQiOiJNV..."

Reading a v3 error at all

Two response shapes, and confusing them wastes an afternoon.

The token endpoint (/oauth2/v3/token) speaks plain OAuth 2.0: a flat body with error and error_description as strings. "error": "invalid_client" here means the credential pair is wrong.

Every resource endpoint returns a nested envelope instead:

v3 resource-endpoint error envelopejson
{
"apiVersion": "3.0",
"error": {
  "code": "401",
  "message": "Unauthorized request.",
  "errors": [
    { "status": "401", "code": "...", "title": "..." }
  ]
}
}

Read the headers too, because USPS puts the actionable part there. A 401 carries WWW-Authenticate, telling you which security scheme the endpoint wanted. A 429 carries Retry-After in seconds. Both are defined in the USPS Addresses API specification, and most client libraries throw them away.

Error: 401 Unauthorized

Symptoms: A resource endpoint returns 401 with "message": "Unauthorized request." and a WWW-Authenticate header.

Causes:

  1. Token expired. The lifetime is 28800 seconds (8 hours), but USPS can invalidate a token 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. A Payment Authorization Token where a Bearer Token belongs, or the reverse.
  4. Re-authenticating every call. Fetching a new token per request invites race conditions and clock skew, which surface as intermittent 401s.
  5. The token predates a claims change. Claims are baked in at issue. Linking a license or refreshing claims changes nothing until you request a new token.

Fix:

Cache the token with a 30-minute expiry buffer, not 5 minutes. If expires_in is 28800, treat the token as dead at 27000 seconds (7h 30m). Check the cache before every call and re-authenticate only when the buffer is crossed.

Token expiry with 30-minute buffer (Python)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: 401 or 403 on address calls, since August 1, 2026

This one is new, and it is now the most common address-endpoint failure with nothing wrong in the code.

Symptoms: /addresses/v3/address worked in July and returns 401 or 403 now. Tracking and pricing on the same credentials still work.

Cause: the Addresses API moved behind a signed license agreement effective August 1, 2026. The USPS Developer Portal states it directly: “Customers who did not complete onboarding no longer have access to the Addresses API.” No code change caused this, and no scope string fixes it.

Fix: the license path runs through the Business Portal at cop.usps.comMy Account → API Licenses → Add an Addresses API License. USPS returns an order form and license agreement by DocuSign, then countersigns. Your Enterprise Payment Account has to exist and be funded first, because usage bills against it at $10 flat for up to 2,000 lookups a month.

Then three separate actions that all look automatic and are not: link your credentials to the license, refresh your claims, and request a new OAuth token. Skip the last one and you will keep calling with a token issued before the license existed.

Timeline is the hard part. Portal steps take an afternoon; the countersignature depends on a human at USPS with no published SLA. Full walkthrough in the CRID and MID enrollment guide, fee curve in the pricing guide.

Error: 403 Forbidden on labels or payments

Symptoms: A valid Bearer Token returns 403 with "error": "insufficient_scope", or 401 Insufficient OAuth scope on /labels/v3/label.

Causes:

  1. Your app is on Public Access I. The Developer Portal grants that bundle by default. It covers OAuth, addresses, pricing, tracking, and other read endpoints, and it excludes Labels and Payments. Asking for scope=labels payments on a Public Access I app returns a token without them rather than an error.
  2. Claims never linked. Even after USPS grants the products, your CRID, MIDs, and EPA reach the token only through claims linking at cop.usps.com.
  3. Test and production mixed up. The test environment is apis-tem.usps.com; production is apis.usps.com.

Fix:

Check the scope field in the token response and base64-decode the payload. If api_products still reads [Public Access I], no amount of scope-string editing will help.

There is no self-service path to Labels and Payments. Submit a service request at emailus.usps.com/s/web-tools-inquiry, selecting “USPS APIs” then “Customer Access,” and name your app, CRID, MIDs, and EPA account number. USPS answers in roughly 1–4 weeks, often with a questionnaire to complete before the grant moves. After the grant, refresh claims in COP and request a new token.

On environments: TEM at https://apis-tem.usps.com/ takes your production credentials rather than a separate sandbox key pair, and the same product restrictions apply there. A label flow that fails in TEM for lack of the Labels product will fail identically in production.

Error: 429 Too Many Requests

Symptoms: Calls return 429 with a Retry-After header. Usually during a batch run, or right after a deploy that dropped token caching.

Cause:

USPS defaults to 60 requests per hour per application, and the window is shared across every v3 endpoint. Address validation, tracking, pricing, and label creation all draw on the same 60. Token fetches count too.

That last detail is what turns a rate limit into an outage. Re-authenticate on every call and each validation costs two requests, so 100 validations become 200 requests and you are cut off at the 30th.

Fix:

Cache the token. This is not an optimization, it is the difference between 30 and 60 usable calls an hour.

Then honor Retry-After rather than inventing a backoff. USPS returns the wait in seconds on the 429 itself, which is more accurate than any fixed sleep you would guess.

If 60 an hour is genuinely too few, request an increase at emailus.usps.com with your app name, CRID, and a specific monthly volume (“5,000 address validations + 2,000 labels”). Reported grants run from about 300 an hour for small businesses to 5,000-plus for enterprise, answered in 1–5 business days against no published criteria. The rate limit guide covers the queueing and caching architecture that survives the default.

Payment Authorization Token

The Payment Authorization Token is a separate credential for label creation. It is not a scope on your Bearer Token. It is a different token, from a different endpoint, sent in a different header.

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 for label printing. Often the same as the Master MID.
  • EPS Account Number. Your Enterprise Payment System account, 10 digits.

All four come from BCG. Without them there are no labels. Getting them into your token is claims linking, done by hand in the Customer Onboarding Portal at cop.usps.com, with no API to automate it.

Request a Payment Authorization Tokenbash
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 responsejson
{
"paymentAuthorizationToken": "USPS-PO-PAYMENT-...",
"roles": [
  {
    "roleName": "PAYER",
    "CRID": "12345678",
    "MID": "900012345",
    "accountType": "EPS",
    "accountNumber": "XXXXXXXX",
    "permit": {
      "permitNumber": "XXXXXXXX",
      "permitZIP": "XXXXX",
      "permitZIP4": "XXXX"
    }
  }
]
}

Where the token actually goes

This is the step that costs people an afternoon. The paymentAuthorizationToken is not a body field. It travels as its own request header, alongside the Bearer token:

Label creation — both tokens, two headersbash
curl -X POST "https://apis.usps.com/labels/v3/label" \
-H "Authorization: Bearer YOUR_BEARER_TOKEN" \
-H "X-Payment-Authorization-Token: YOUR_PAYMENT_TOKEN" \
-H "X-Idempotency-Key: 8f14e45f-ceea-467a-9a1b-2c3d4e5f6a7b" \
-H "Content-Type: application/json" \
-d '{ "toAddress": { "...": "..." } }'

The USPS Labels API specification marks X-Payment-Authorization-Token required on label creation, reprint, and cancellation. Put the token in the JSON body and the header is simply absent, which USPS reports as an authorization failure rather than a malformed request. That is why the error reads like a credential problem when the credential is fine.

X-Idempotency-Key is worth wiring at the same time. It is a client-generated UUID that keys reprints and cancellations back to the original label. Label creation itself is not idempotent: resubmitting a used UUID mints a second label and bills you for it. Full schemas in the API reference.

Free API key

A key you can test against in 30 seconds

Debugging OAuth is easier with a working baseline. A free RevAddress key runs Census standardization, geocoding, and address extract with no USPS license at all — 1,000 requests a month, no credit card.

Bot check loads above; the button enables once it passes.

By signing up you agree to our Terms and Privacy Policy.

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

Symptom Likely cause Fix
Address calls 401/403 since August 2026 No signed Addresses API license Request one at cop.usps.com, then link credentials and take a new token
401 on every call Re-authenticating per request Implement token caching
401 on label calls only Missing Payment Authorization Token Fetch the second token after COP claims linking
Token works, label still 401 Payment token sent in the JSON body Send it as the X-Payment-Authorization-Token header
403 on the label endpoint App is on Public Access I Service request at emailus.usps.com. No self-service path
Scope string includes labels, token does not Product not granted to the app Decode the JWT. api_products is the real answer, not the scope you asked for
429 after a deploy Token cache removed or disabled Re-enable caching, then honor Retry-After
invalid_client on the token request Wrong client_id or client_secret Verify in the Developer Portal, not BCG
401 after 7.5 hours 30-minute buffer not applied Refresh at expires_in - 1800, not expires_in
COP shows zero authorizations Labels/Payments not granted, or claims not refreshed Refresh claims. Still zero means the grant is pending
TEM call fails the same way production does Same product restrictions apply TEM uses your production credentials. Fix the grant, not the URL

What RevAddress takes off your plate, and what it cannot

The honest split, because the enrollment maze is real and nobody can shortcut a USPS signature for you.

The token machinery, yes. Bring your own USPS credentials and RevAddress runs the OAuth lifecycle against them: the two-token flow, caching with the expiry buffer, the X-Payment-Authorization-Token header, Retry-After handling, and refresh on invalidation. Your code sends one API key. Available on every plan, including Free.

The USPS data itself, on your license. DPV, ZIP+4, rates, service standards, and tracking are USPS data. They run through your own USPS credentials on every plan, never bundled into the monthly fee. What the plan buys is the infrastructure around them: managed OAuth, the encrypted credential vault, retries, and the dashboard.

The paperwork, no. The Addresses API license, the Labels and Payments grant, and COP claims linking are agreements between USPS and your business entity. Postage is paid by the entity that owes it, which means your CRID, your MID, your EPA.

Something that works today, while USPS moves. Standardization, geocoding, and address extract run against the US Census Bureau’s public address inventory. No USPS license, no DocuSign, 1,000 requests a month, no credit card. It puts an address in correct postal form and returns street-level coordinates. It does not return DPV, ZIP+4, or USPS deliverability flags, because those are USPS data and USPS data requires a license.

Start here

Questions

How long does a USPS v3 OAuth token last?
Eight hours. The token response returns expires_in as 28800 seconds, and the Payment Authorization Token used for label creation has the same eight-hour lifetime. USPS can invalidate a token before its stated expiry, so refresh on a buffer rather than on the exact countdown.
Why does address validation return 401 when my credentials are correct?
Since August 1, 2026 the Addresses API requires a signed license agreement. USPS states that customers who did not complete onboarding no longer have access to the Addresses API. Valid client credentials are no longer sufficient on their own, and a token issued before your license was linked will not carry the new claims.
Where does the Payment Authorization Token go in a label request?
In the X-Payment-Authorization-Token request header, not in the JSON body. The USPS Labels API specification marks that header required on every label, reprint, and cancellation call, alongside the standard Authorization Bearer header.
What is the difference between the Developer Portal and the Business Customer Gateway?
The Developer Portal at developers.usps.com issues your client ID and client secret. The Business Customer Gateway at gateway.usps.com issues your CRID, Mailer IDs, and Enterprise Payment Account. The Customer Onboarding Portal at cop.usps.com links the two so your token carries the right claims.
Does USPS tell me how long to wait after a 429?
Yes. A 429 response carries a Retry-After header. Honor that value instead of guessing a backoff interval, and treat the header as the source rather than a fixed sleep in your retry loop.