Magento USPS AC-15210:
Fix Broken Shipping After Web Tools Retirement
Adobe's official patch swaps USPS endpoint URLs and bolts on OAuth. It does not handle the fact that the v3 API returns JSON instead of XML. Your tracking breaks silently, rate shopping returns nothing, and every request mints a new OAuth token. Here is what AC-15210 actually broke and how to fix it.
What Is AC-15210?
AC-15210 is Adobe Commerce's official patch to address the USPS Web Tools retirement. USPS decommissioned their legacy XML APIs (secure.shippingapis.com) and replaced them with v3 REST endpoints at apis.usps.com. The patch is Adobe's response.
Patch availability by version:
| Magento Version | Minimum Patch Level | Status |
|---|---|---|
| 2.4.4 | -p8 and later | Patch available |
| 2.4.5 | -p7 and later | Patch available |
| 2.4.6 | -p5 and later | Patch available |
| 2.4.7 | -p1 and later | Patch available |
What AC-15210 claims to fix: USPS carrier integration for the new v3 REST API. OAuth 2.0 authentication. Updated endpoint URLs. Continued shipping rate calculation and tracking.
What it actually does: Swaps secure.shippingapis.com URLs to apis.usps.com, adds a basic OAuth token request before each API call, and calls it done. The response parsing? Still XML. The token caching? Absent. The CRID/MID admin fields? Missing.
What AC-15210 Actually Breaks
The patch is a minimum viable URL swap. It changes where requests go but not how responses are handled. Five things break immediately.
1. Tracking number lookup is broken
The v3 tracking endpoint (/tracking/v3/tracking/{trackingNumber}) returns JSON. The Magento carrier model calls simplexml_load_string() on the response. JSON is not XML. The function returns false, and tracking silently reports "not found" for every package.
// Magento\Usps\Model\Carrier — AFTER AC-15210
// The patch swapped the base URL but _parseTrackingResponse()
// still expects Web Tools XML structure
protected function _parseTrackingResponse(\$trackingValue, \$response)
{
// AC-15210 sends to: apis.usps.com/tracking/v3/tracking/{trackingNumber}
// v3 returns JSON. This method calls simplexml_load_string().
\$xml = simplexml_load_string(\$response); // FATAL: not XML
if (!\$xml) {
return false; // Silently fails — tracking "not found"
}
// ...
} 2. Rate shopping returns incorrect or empty results
Web Tools returned XML with <Postage> elements and human-readable service names. The v3 pricing API returns JSON with rateOptions arrays and machine-readable mailClass codes. The carrier model's rate parser expects the XML structure and finds nothing.
// Magento\Usps\Model\Carrier::_parseXmlResponse()
// Web Tools returned service names like "Priority Mail"
// v3 returns classId codes like "PRIORITY_MAIL"
// Before (Web Tools XML):
<Postage CLASSID="1">
<MailService>Priority Mail</MailService>
<Rate>8.70</Rate>
</Postage>
// After (v3 JSON — what AC-15210 actually receives):
{
"rateOptions": [{
"mailClass": "PRIORITY_MAIL",
"totalBasePrice": 8.70
}]
}
// Carrier model tries xpath() on this. Returns empty rates. Customers see "no shipping methods available" at checkout. Orders stop.
3. 429 rate limit errors under load
The USPS v3 API enforces a 60 request/hour rate limit. AC-15210 requests a new OAuth token before every single API call. Each token request counts against your rate limit. A Magento store processing 15 orders in an hour makes ~60 API calls for rates and labels, plus ~60 token requests. That is 120 calls against a 60/hr limit. You hit 429 within 30 minutes.
4. OAuth token not cached
USPS v3 OAuth tokens are valid for 8 hours (28,800 seconds). AC-15210 does not use Magento's cache framework to store the token. Every collectRates() call, every tracking request, every label creation starts with a fresh POST /oauth2/v3/token. This is the root cause of issue #3 above and doubles every API operation's latency.
5. Missing CRID/MID configuration fields
The v3 API requires a CRID (Customer Registration ID) and MID (Mailer ID) for label creation and payment authorization. AC-15210 does not add system.xml fields for these values. Store admins have no way to enter their CRID/MID through the Magento admin panel.
The Root Cause
AC-15210 treats the Web Tools → v3 migration as a URL change. It is not. It is a protocol change:
Web Tools (deprecated) v3 REST (current)
───────────────────── ─────────────────────
XML request bodies → JSON request bodies
XML responses → JSON responses
API key (USERID param) → OAuth 2.0 (Bearer token)
No rate limit → 60 req/hr
No auth state → 8-hour token lifecycle
No CRID/MID required → CRID + MID for labels
The Magento\Usps\Model\Carrier class has methods that call simplexml_load_string(), xpath(), and reference XML element names like <MailService> and <Rate>. AC-15210 did not touch these methods. The patch changed the transport layer and left the parsing layer for Web Tools.
Fix 1: Patch the Patch (Carrier Override)
Create a custom Magento 2 module that overrides the USPS carrier with a version that properly handles v3 JSON responses. This preserves AC-15210's URL and OAuth changes while fixing the parsing layer.
Step 1: DI configuration — Register your carrier override:
<!-- app/code/YourVendor/UspsV3Fix/etc/di.xml -->
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
<!-- Override the stock USPS carrier with v3-aware version -->
<preference for="Magento\Usps\Model\Carrier"
type="YourVendor\UspsV3Fix\Model\Carrier" />
<!-- Inject cached token provider -->
<type name="YourVendor\UspsV3Fix\Model\Carrier">
<arguments>
<argument name="tokenProvider"
xsi:type="object">YourVendor\UspsTokenCache\Model\TokenProvider</argument>
</arguments>
</type>
</config> Step 2: Carrier override — Replace XML parsing with JSON in the two critical methods:
// app/code/YourVendor/UspsV3Fix/Model/Carrier.php
// Overrides Magento\Usps\Model\Carrier to handle v3 JSON responses
namespace YourVendor\UspsV3Fix\Model;
use Magento\Usps\Model\Carrier as MagentoUspsCarrier;
class Carrier extends MagentoUspsCarrier
{
// Map v3 mailClass codes to human-readable names
private const SERVICE_MAP = [
'PRIORITY_MAIL' => 'Priority Mail',
'PRIORITY_MAIL_EXPRESS' => 'Priority Mail Express',
'FIRST_CLASS_MAIL' => 'First-Class Mail',
'USPS_GROUND_ADVANTAGE' => 'USPS Ground Advantage',
'PARCEL_SELECT' => 'Parcel Select Ground',
'MEDIA_MAIL' => 'Media Mail',
'LIBRARY_MAIL' => 'Library Mail',
];
public function __construct(
// ... standard Magento DI args ...
private \YourVendor\UspsTokenCache\Model\TokenProvider \$tokenProvider,
// ... remaining args ...
) {
parent::__construct(/* ... */);
}
/**
* Override: parse v3 JSON rate response instead of XML
*/
protected function _parseResponse(\$response): array
{
\$data = json_decode(\$response, true);
if (!\$data || !isset(\$data['rateOptions'])) {
return [];
}
\$rates = [];
foreach (\$data['rateOptions'] as \$option) {
\$code = \$option['mailClass'] ?? '';
\$rates[] = [
'service' => self::SERVICE_MAP[\$code] ?? \$code,
'price' => (float) (\$option['totalBasePrice'] ?? 0),
'code' => \$code,
];
}
return \$rates;
}
/**
* Override: parse v3 JSON tracking response instead of XML
*/
protected function _parseTrackingResponse(\$trackingValue, \$response)
{
\$data = json_decode(\$response, true);
if (!\$data || isset(\$data['error'])) {
return false;
}
\$result = \$this->_trackFactory->create();
\$tracking = \$this->_trackStatusFactory->create();
\$tracking->setCarrier('usps');
\$tracking->setCarrierTitle('USPS');
\$tracking->setTracking(\$trackingValue);
// v3 nests tracking under 'trackingEvents' array
\$events = \$data['trackingEvents'] ?? [];
if (!empty(\$events)) {
\$latest = \$events[0]; // Most recent first
\$tracking->setStatus(\$latest['eventType'] ?? '');
}
\$result->append(\$tracking);
return \$result;
}
}
This override gives you working rates and tracking with the v3 API while keeping everything else in the stock Magento USPS module intact. The SERVICE_MAP constant translates v3's machine-readable mail class codes back to the human-readable names that Magento's checkout UI expects.
Fix 2: Drop-in Carrier Module with RevAddress
Instead of patching Adobe's patch, replace the USPS carrier entirely with a clean module that calls api.revaddress.com. No OAuth. No rate limits. No XML-to-JSON translation. One API key.
// app/code/YourVendor/RevAddressShipping/Model/Carrier/RevAddress.php
// Clean carrier module — no OAuth, no rate limits, no XML parsing
namespace YourVendor\RevAddressShipping\Model\Carrier;
use Magento\Quote\Model\Quote\Address\RateRequest;
use Magento\Shipping\Model\Carrier\AbstractCarrierOnline;
use Magento\Shipping\Model\Carrier\CarrierInterface;
use Magento\Framework\HTTP\ClientFactory;
class RevAddress extends AbstractCarrierOnline implements CarrierInterface
{
protected \$_code = 'revaddress';
private const API_BASE = 'https://api.revaddress.com/v1';
public function collectRates(RateRequest \$request)
{
if (!\$this->getConfigFlag('active')) {
return false;
}
\$apiKey = \$this->getConfigData('api_key');
\$client = \$this->httpClientFactory->create();
\$client->setHeaders([
'Authorization' => 'Bearer ' . \$apiKey,
'Content-Type' => 'application/json',
]);
\$client->post(self::API_BASE . '/rates', json_encode([
'originZIPCode' => \$request->getPostcode(),
'destinationZIPCode' => \$request->getDestPostcode(),
'weight' => \$request->getPackageWeight(),
'mailClass' => 'ALL',
]));
\$rates = json_decode(\$client->getBody(), true);
\$result = \$this->_rateResultFactory->create();
foreach (\$rates['rateOptions'] ?? [] as \$option) {
\$method = \$this->_rateMethodFactory->create();
\$method->setCarrier(\$this->_code);
\$method->setCarrierTitle('USPS');
\$method->setMethod(\$option['mailClass']);
\$method->setMethodTitle(\$option['description']);
\$method->setPrice(\$option['totalBasePrice']);
\$method->setCost(\$option['totalBasePrice']);
\$result->append(\$method);
}
return \$result;
}
public function getAllowedMethods(): array
{
return [
'PRIORITY_MAIL' => 'Priority Mail',
'PRIORITY_MAIL_EXPRESS' => 'Priority Mail Express',
'USPS_GROUND_ADVANTAGE' => 'USPS Ground Advantage',
'FIRST_CLASS_MAIL' => 'First-Class Mail',
];
}
} Admin configuration — Add a simple system.xml so store admins can enable the carrier and enter their API key through the Magento admin:
<!-- app/code/YourVendor/RevAddressShipping/etc/adminhtml/system.xml -->
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Config:etc/system_file.xsd">
<system>
<section id="carriers">
<group id="revaddress" translate="label" sortOrder="25" showInDefault="1">
<label>RevAddress (USPS)</label>
<field id="active" translate="label" type="select" sortOrder="10"
showInDefault="1" showInWebsite="1">
<label>Enabled</label>
<source_model>Magento\Config\Model\Config\Source\Yesno</source_model>
</field>
<field id="api_key" translate="label" type="obscure" sortOrder="20"
showInDefault="1" showInWebsite="1">
<label>RevAddress API Key</label>
<backend_model>Magento\Config\Model\Config\Backend\Encrypted</backend_model>
</field>
<field id="title" translate="label" type="text" sortOrder="30"
showInDefault="1" showInWebsite="1">
<label>Title</label>
</field>
</group>
</section>
</system>
</config> This carrier module is 80 lines of PHP. The stock Magento USPS carrier after AC-15210 is over 1,200 lines, and most of them are now wrong. The RevAddress API handles OAuth token management, rate limiting, response normalization, and CRID/MID authentication on the server side. Your Magento module just sends JSON and gets JSON back.
Rate Limit Reality for Magento Stores
The math on USPS rate limits is brutal for Magento stores. Here's the breakdown per customer journey:
// Magento store: 100 orders/day
// Each customer journey triggers multiple USPS API calls:
Cart page: rate check ×1 // estimate shipping
Checkout step 1: address validate ×1 // verify address
Checkout step 2: rate check ×1 // final rate with exact address
Order placed: label create ×1 // generate shipping label
Post-purchase: tracking poll ×3 // status updates over 3 days
─────────────────────────────────────────────────
Total per order: 7 API calls
100 orders/day × 7 calls = 700 calls/day
USPS limit: 60/hr × 24hr = 1,440 calls/day ← looks fine daily
But peak hour (2-5 PM): 40 orders × 4 calls = 160 calls
USPS limit per hour: 60 calls
Result: 429 errors starting at order #15
// Plus: AC-15210 requests a NEW OAuth token per request
// That doubles your call count. 160 → 320 calls/hr. And this is a moderate store. Magento installations doing 500+ orders/day need 3,500+ API calls. At 60/hr, that is physically impossible through direct USPS integration. AC-15210 makes it worse by doubling every call with an uncached token request.
OAuth Token Caching Fix
If you are staying with direct USPS integration (Fix 1), you must cache the OAuth token. AC-15210 does not do this. The token is valid for 8 hours. Here is a TokenProvider that uses Magento's cache framework to store it:
// app/code/YourVendor/UspsTokenCache/Model/TokenProvider.php
namespace YourVendor\UspsTokenCache\Model;
use Magento\Framework\Cache\FrontendInterface;
use Magento\Framework\HTTP\ClientFactory;
use Magento\Framework\App\Config\ScopeConfigInterface;
class TokenProvider
{
private const CACHE_KEY = 'usps_oauth_v3_token';
private const TOKEN_URL = 'https://apis.usps.com/oauth2/v3/token';
private const REFRESH_BUFFER = 1800; // 30 min before expiry
public function __construct(
private FrontendInterface \$cache,
private ClientFactory \$httpClientFactory,
private ScopeConfigInterface \$scopeConfig,
) {}
public function getToken(): string
{
\$cached = \$this->cache->load(self::CACHE_KEY);
if (\$cached) {
return \$cached; // Cache hit — no HTTP call
}
return \$this->refreshToken();
}
private function refreshToken(): string
{
\$clientId = \$this->scopeConfig->getValue('carriers/usps/client_id');
\$clientSecret = \$this->scopeConfig->getValue('carriers/usps/client_secret');
\$client = \$this->httpClientFactory->create();
\$client->setHeaders(['Content-Type' => 'application/x-www-form-urlencoded']);
\$client->post(self::TOKEN_URL, [
'grant_type' => 'client_credentials',
'client_id' => \$clientId,
'client_secret' => \$clientSecret,
]);
\$data = json_decode(\$client->getBody(), true);
\$token = \$data['access_token'];
\$ttl = (\$data['expires_in'] ?? 28800) - self::REFRESH_BUFFER;
// Cache for (expires_in - 30 min) = ~7.5 hours
\$this->cache->save(\$token, self::CACHE_KEY, [], \$ttl);
return \$token;
}
}
This cuts your USPS API call count in half immediately. Instead of 120 calls/hour (60 API + 60 tokens), you make 60 API calls and 1 token call every 7.5 hours. The REFRESH_BUFFER of 30 minutes ensures the token is refreshed before it expires, avoiding 401 errors during the refresh window.
Migration Checklist
From broken AC-15210 to working USPS shipping. Work through these in order.
- 1 Verify AC-15210 is applied. Check
composer show magento/module-uspsversion matches your patch level. The patch must be present before overrides work. - 2 Register on the USPS Developer Portal. Get OAuth Client ID + Client Secret from developer.usps.com. These are different from your old Web Tools API key.
- 3 Get your CRID and MID. Register through the USPS Business Customer Gateway. Required for label creation and payment authorization.
- 4 Choose your fix path. Fix 1 (carrier override) if you want to keep direct USPS integration. Fix 2 (RevAddress carrier module) if you want managed infrastructure.
- 5 Install the token caching fix. (Fix 1 only.) Deploy the
TokenProvidermodule and configure it viadi.xml. Verify caching works by checking thatbin/magento cache:statusshows the default cache is enabled. - 6 Test in staging. Run
bin/magento setup:upgrade && bin/magento cache:flush. Place a test order. Verify rates appear at checkout, tracking returns data, and no 429 errors invar/log/system.log. - 7 Request a USPS rate limit increase. (Fix 1 only.) Email USPS API support with your CRID, app name, and estimated monthly volume. See our rate limit guide for the exact process.
- 8 Deploy and monitor. Watch
var/log/shipping.logfor the first 24 hours. Look for 401 (token issues), 429 (rate limits), and empty rate responses.
Common Errors After AC-15210
Every error we have seen from Magento stores running AC-15210, what causes it, and how to fix it.
| Error | Cause | Fix |
|---|---|---|
| simplexml_load_string(): String is not valid XML | v3 returns JSON, carrier parses XML | Override _parseTrackingResponse() and _parseResponse() |
| Invalid or missing token | Token not cached, re-requested and hit rate limit | Add TokenProvider with Magento cache framework |
| Rate limit exceeded (429) | 60 req/hr exhausted — AC-15210 gets new token per request | Cache OAuth token (8hr lifetime, refresh at 7.5hr) |
| Carrier not available for selected address | Rate response JSON not parsed, returns empty rates | Override rate parsing to handle v3 JSON structure |
| Tracking number not found | v3 tracking endpoint returns JSON, simplexml returns false | Override _parseTrackingResponse() for JSON |
| SOAP-ENV:Server | Code still hitting legacy secure.shippingapis.com URL | Verify AC-15210 patch applied correctly to all files |
| Missing CRID or MID | AC-15210 has no admin fields for CRID/MID | Add system.xml fields or hardcode in carrier override |
| cURL error 28: Operation timed out | USPS v3 slower under load, Magento default timeout too low | Increase carrier timeout to 30s in admin config |
Why This Happened
USPS gave the industry 18 months of notice before retiring Web Tools. Adobe waited until the deadline, then shipped a minimal patch that changes URLs without changing the code that interprets responses. The carrier model in Magento\Usps\Model\Carrier was written for XML and never refactored.
This is not a criticism of the USPS v3 API — it is actually a significant improvement over Web Tools. JSON responses are cleaner, OAuth is more secure than API keys in URLs, and the structured rate format is easier to work with. The problem is entirely in the translation layer between Magento and the new API. AC-15210 bridges the transport but not the protocol.
Skip the patching entirely
RevAddress gives your Magento store a single REST endpoint with no OAuth, no rate limits, no XML-to-JSON translation, and no CRID/MID enrollment. The carrier module above is 80 lines and works today.