USPS v3 API with PHP:
Complete Quickstart Guide
The revaddress/usps-v3-php Composer package gives you a zero-dependency PHP interface to the USPS v3 REST API. This tutorial covers installation, authentication, and the six core operations: address validation, package tracking, rate shopping, label creation, error handling, and framework integration for Magento, WooCommerce, and Laravel.
Prerequisites
- USPS Developer Portal account — Register at
developer.usps.comand create an application to get your Client ID and Client Secret. - PHP 8.1+ with
ext-jsonandext-openssl. Compatible with PHP 8.1, 8.2, 8.3, and 8.4. - Zero dependencies — No Guzzle, no PSR-7. The SDK uses PHP's built-in
file_get_contentswith stream contexts.
# Zero dependencies. PHP 8.1+ with ext-json and ext-openssl.
composer require revaddress/usps-v3-php Authentication
The USPS v3 API uses OAuth 2.0 client credentials grant. Tokens last 8 hours. The SDK handles the entire flow automatically — tokens are cached to disk so they persist across PHP requests (important for FPM / shared hosting environments).
use RevAddress\USPSv3\Client;
// Client ID and Client Secret from USPS Developer Portal
// https://developer.usps.com/apis
$usps = new Client('your-client-id', 'your-client-secret');
// That's it. OAuth tokens are managed automatically:
// - Token fetched on first API call
// - Cached to disk (8-hour lifetime)
// - Refreshed 30 minutes before expiry
// - Custom cache dir: new Client('id', 'secret', cacheDir: '/var/cache/usps') Token management: Check token status with $usps->tokenStatus() — returns TTL, validity, and payment auth availability. Force a refresh with $usps->refreshTokens(). Tokens are cached to sys_get_temp_dir() by default.
Address Validation
Address validation is the most common API call. It standardizes street addresses, confirms deliverability via DPV (Delivery Point Validation), and flags vacant addresses.
use RevAddress\USPSv3\Client;
$usps = new Client('your-client-id', 'your-client-secret');
// Validate a US address
$result = $usps->validateAddress([
'streetAddress' => '1600 Pennsylvania Ave NW',
'city' => 'Washington',
'state' => 'DC',
'ZIPCode' => '20500',
]);
// Access standardized fields
echo $result['address']['streetAddress']; // 1600 PENNSYLVANIA AVE NW
echo $result['address']['city']; // WASHINGTON
echo $result['address']['state']; // DC
echo $result['address']['ZIPCode']; // 20500
echo $result['address']['ZIPPlus4']; // 0005
// Delivery Point Validation
echo $result['address']['DPVConfirmation']; // Y
echo $result['address']['vacant']; // N | DPV Code | Meaning | Action |
|---|---|---|
| Y | Confirmed deliverable | Accept the address |
| S | Secondary info missing (apt/suite) | Prompt user for unit number |
| D | Secondary confirmed but not matched | Verify unit number with user |
| N | Not deliverable | Reject or flag for manual review |
Address2 was the street and Address1 was the apt/suite — confusingly swapped. The v3 REST API uses streetAddress and secondaryAddress. No more field swaps.
Package Tracking
Track any USPS package by tracking number. The response includes the current status, delivery date, and a full event history with timestamps and locations.
// Track a package by tracking number
$tracking = $usps->trackPackage('9400111899223456789012');
// Latest status
echo $tracking['trackingNumber']; // "9400111899223456789012"
echo $tracking['statusCategory']; // "Delivered"
echo $tracking['status']; // "Delivered, In/At Mailbox"
// Full event history (most recent first)
foreach ($tracking['trackingEvents'] as $event) {
printf(
"%s %s | %s, %s | %s\n",
$event['eventDate'],
$event['eventTime'],
$event['eventCity'],
$event['eventState'],
$event['eventDescription']
);
}
// Output:
// 2026-03-08 10:15 | Washington, DC | Delivered, In/At Mailbox
// 2026-03-08 06:30 | Washington, DC | Out for Delivery
// 2026-03-07 22:10 | Washington, DC | Arrived at Hub Tip: Tracking data is real-time. Poll at reasonable intervals (every 30-60 minutes) or use USPS webhook notifications. The old <TrackID> XML body is gone — the tracking number is now a path parameter handled automatically by the SDK.
Rate Shopping
Get shipping prices for domestic and international packages. The v3 API requires a mailClass per request (unlike the old RateV4 which returned all classes at once). The SDK validates mail class constants before sending.
// Get domestic shipping prices
$rates = $usps->getDomesticPrices([
'originZIPCode' => '10001', // New York, NY
'destinationZIPCode' => '90210', // Beverly Hills, CA
'weight' => 2.5, // pounds
'mailClass' => 'PRIORITY_MAIL',
'processingCategory' => 'MACHINABLE',
'rateIndicator' => 'DR', // Dimensional Rectangular
'priceType' => 'RETAIL',
]);
// Access rate details
echo $rates['totalBasePrice']; // "12.10"
// International rates
$intlRates = $usps->getInternationalPrices([
'originZIPCode' => '10001',
'destinationCountryCode' => 'CA', // ISO alpha-2 (not "Canada")
'weight' => 2.5,
'mailClass' => 'PRIORITY_MAIL_EXPRESS',
]); | Mail Class Constant | Service |
|---|---|
| PRIORITY_MAIL_EXPRESS | 1-2 day guaranteed |
| PRIORITY_MAIL | 1-3 day |
| USPS_GROUND_ADVANTAGE | 2-5 day ground |
| FIRST-CLASS_PACKAGE_SERVICE | 1-5 day (under 1 lb) |
| PARCEL_SELECT | 2-8 day economy |
| MEDIA_MAIL | 2-8 day (books/media only) |
Label Creation
Create USPS shipping labels programmatically. The API returns a tracking number and raw PDF label data. Label creation requires BYOK (Bring Your Own Keys) credentials — CRID, MID, and EPA account linked through the USPS Business Customer Gateway.
// Create a shipping label (requires BYOK credentials)
$usps = new Client(
'client-id',
'client-secret',
crid: '56982563',
masterMid: '904128936',
labelMid: '904128937',
epaAccount: 'your-eps-account',
);
$label = $usps->createLabel(
fromAddress: [
'firstName' => 'RevAddress',
'streetAddress' => '228 Park Ave S',
'city' => 'New York',
'state' => 'NY',
'ZIPCode' => '10003',
],
toAddress: [
'firstName' => 'Jane',
'lastName' => 'Doe',
'streetAddress' => '1600 Pennsylvania Ave NW',
'city' => 'Washington',
'state' => 'DC',
'ZIPCode' => '20500',
],
mailClass: 'PRIORITY_MAIL',
weight: 2.5,
);
echo $label['trackingNumber'];
file_put_contents('label.pdf', $label['labelData']); // PDF bytes BYOK credentials: Get your CRID, MID, and EPA account from the USPS Business Customer Gateway enrollment guide. Or use a RevAddress Growth plan which handles CRID/MID/COP claims for you.
Error Handling
The SDK throws typed exception classes for every error category. The most important one to handle is RateLimitException — USPS caps direct access at 60 requests/hour. The exception exposes a getRetryAfter() method (seconds) parsed from the response header.
use RevAddress\USPSv3\Client;
use RevAddress\USPSv3\Exception\USPSException;
use RevAddress\USPSv3\Exception\AuthException;
use RevAddress\USPSv3\Exception\RateLimitException;
use RevAddress\USPSv3\Exception\ValidationException;
$usps = new Client('your-client-id', 'your-client-secret');
try {
$result = $usps->validateAddress([
'streetAddress' => '1600 Pennsylvania Ave NW',
'city' => 'Washington',
'state' => 'DC',
]);
} catch (RateLimitException $e) {
// 429 — wait and retry using Retry-After header
$retryAfter = $e->getRetryAfter() ?? 60;
sleep($retryAfter);
} catch (AuthException $e) {
// 401 — bad credentials or expired token
$usps->refreshTokens();
} catch (ValidationException $e) {
// Bad input — check which field failed
echo $e->getField(); // "streetAddress"
} catch (USPSException $e) {
// Catch-all — check if retryable (500, 502, 503, 504)
if ($e->isRetryable()) {
// Safe to retry
}
$body = $e->getResponseBody(); // Raw USPS error
} | Exception | HTTP Status | Cause |
|---|---|---|
| AuthException | 401 | Bad credentials or expired token |
| ValidationException | 400 | Missing or invalid request parameters |
| RateLimitException | 429 | Exceeded 60 req/hr limit |
| USPSException | 5xx | USPS server error (retry safe via isRetryable()) |
Magento 2 Integration
If you're fixing the AC-15210 USPS shipping breakage in Magento 2, this SDK is the fastest path. Create a custom carrier module that calls the v3 REST API directly — no XML parsing, no USERID query params, proper OAuth caching.
// Magento 2 — Custom carrier module (app/code/RevAddress/Shipping/)
// Model/Carrier/RevAddress.php
namespace RevAddress\Shipping\Model\Carrier;
use Magento\Quote\Model\Quote\Address\RateRequest;
use Magento\Shipping\Model\Carrier\AbstractCarrier;
use RevAddress\USPSv3\Client;
class RevAddress extends AbstractCarrier
{
protected $_code = 'revaddress';
private Client $usps;
public function __construct(/* ... */)
{
parent::__construct(/* ... */);
$this->usps = new Client(
$this->getConfigData('client_id'),
$this->getConfigData('client_secret'),
);
}
public function collectRates(RateRequest $request)
{
$rates = $this->usps->getDomesticPrices([
'originZIPCode' => $request->getPostcode(),
'destinationZIPCode' => $request->getDestPostcode(),
'weight' => $request->getPackageWeight(),
'mailClass' => 'USPS_GROUND_ADVANTAGE',
'processingCategory' => 'MACHINABLE',
'rateIndicator' => 'DR',
'priceType' => 'RETAIL',
]);
// ... build RateResult from $rates
}
}
See the full Magento AC-15210 fix guide for the complete module structure including etc/config.xml, admin configuration fields, and deployment steps.
Laravel Integration
Register the USPS client as a singleton in a service provider. The client manages its own token lifecycle and is safe for reuse across requests — instantiate once, inject everywhere.
// Laravel — Service provider + singleton
// app/Providers/USPSServiceProvider.php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use RevAddress\USPSv3\Client;
class USPSServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->singleton(Client::class, fn() =>
new Client(
config('services.usps.client_id'),
config('services.usps.client_secret'),
cacheDir: storage_path('framework/cache/usps'),
)
);
}
}
// In any controller or service:
use RevAddress\USPSv3\Client;
class AddressController
{
public function validate(Request $request, Client $usps)
{
$result = $usps->validateAddress([
'streetAddress' => $request->street,
'city' => $request->city,
'state' => $request->state,
'ZIPCode' => $request->zip,
]);
return response()->json([
'valid' => $result['address']['DPVConfirmation'] === 'Y',
'standardized' => $result['address'],
]);
}
} Config: Add usps.client_id and usps.client_secret to config/services.php. The cacheDir option points token cache at Laravel's cache directory so tokens persist across FPM workers.
Next Steps
You've got the fundamentals. Here's where to go from here:
- Packagist — Installation, changelog, version history. 59 tests, 102 assertions.
- GitHub repo — Source code, issues, CI status (PHP 8.1–8.4). MIT licensed.
- Full API Documentation — Complete endpoint reference with request/response schemas for all 41 routes.
- Magento AC-15210 Fix — Complete Magento carrier module with admin config, OAuth caching, and deployment.
- WooCommerce Migration — Fix broken USPS shipping in WooCommerce with the PHP SDK.
- Rate Limit Strategies — Caching, queuing, and architecture patterns for production workloads beyond 60 req/hr.
Need higher rate limits?
RevAddress provides a managed USPS v3 API with 600 req/min, built-in caching, automatic token management, and BYOK support. Drop in the SDK and scale.
Related Articles
Magento AC-15210 Fix
Complete Magento carrier module to fix USPS shipping after the AC-15210 patch.
WooCommerce USPS Migration
Fix broken USPS shipping in WooCommerce with the PHP SDK and RevAddress.
Using Python instead? Python Quickstart
The same walkthrough with the usps-v3 Python SDK on PyPI.
Using Node.js instead? Node.js Quickstart
TypeScript-first, zero deps. The same walkthrough with the usps-v3 npm SDK.