johnpaulmedina/laravel-usps

USPS Addresses API v3 (OAuth2) for Laravel

Maintainers

Package info

github.com/johnpaulmedina/laravel-usps

Homepage

pkg:composer/johnpaulmedina/laravel-usps

Statistics

Installs: 23 012

Dependents: 0

Suggesters: 0

Stars: 33

Open Issues: 1


README

Full Laravel package for the USPS API v3 (OAuth2). Covers all 20 USPS API domains: addresses, tracking, labels, international labels, domestic prices, international prices, service standards, locations, carrier pickup, containers, payments, campaigns, package campaigns, adjustments, disputes, appointments, shipping options, and SCAN forms.

Note: The legacy USPS Web Tools XML API was retired in January 2026. This package uses the new OAuth2-based REST/JSON API at apis.usps.com.

Reference: Official USPS API examples and OpenAPI specs at github.com/USPS/api-examples.

Requirements

Installation

composer require johnpaulmedina/laravel-usps

The service provider and facade are auto-discovered.

Configuration

Publish the config file:

php artisan vendor:publish --tag=usps-config

This creates config/usps.php:

return [
    'client_id' => env('USPS_CLIENT_ID', ''),
    'client_secret' => env('USPS_CLIENT_SECRET', ''),
];

Add your credentials to .env:

USPS_CLIENT_ID=your-client-id
USPS_CLIENT_SECRET=your-client-secret

Artisan Commands

# Validate an address
php artisan usps:validate "1600 Pennsylvania Ave NW" --state=DC --zip=20500

# Track packages
php artisan usps:track 9400111899223456789012

# ZIP code lookup (city/state)
php artisan usps:zip 33101

# Calculate shipping rates
php artisan usps:price 20500 33101 16 --mail-class=PRIORITY_MAIL

# Delivery estimates
php artisan usps:standards 20500 33101

# Find USPS locations
php artisan usps:locations 33101 --type=post-office --radius=5

All commands output formatted tables to the console.

Usage

All API domains are accessible via the Usps facade. Address methods are called directly; all other domains are accessed through fluent accessor methods that return typed API clients.

Addresses

use Usps;

// Validate an address
$result = Usps::validate([
    'Address' => '1600 Pennsylvania Ave NW',
    'City' => 'Washington',
    'State' => 'DC',
    'Zip' => '20500',
]);

Response:

{
    "address": {
        "Address2": "1600 PENNSYLVANIA AVE NW",
        "Address1": "",
        "City": "WASHINGTON",
        "State": "DC",
        "Zip5": "20500",
        "Zip4": "0005"
    },
    "additionalInfo": {
        "deliveryPoint": "00",
        "carrierRoute": "C000",
        "DPVConfirmation": "Y",
        "DPVCMRA": "N",
        "business": "Y",
        "centralDeliveryPoint": "",
        "vacant": "N"
    }
}

DPV (Delivery Point Validation) confirms whether a specific address is a real USPS deliverable location — down to the apartment/suite level.

Code Meaning
Y Full match — both street and unit confirmed
D Primary address confirmed, apartment/suite missing
S Primary confirmed, apartment/suite provided but doesn't match
N Not a valid delivery point

When apartment is missing (e.g., multi-unit building):

{
    "address": {
        "Address2": "100 S BISCAYNE BLVD",
        "Address1": "",
        "City": "MIAMI",
        "State": "FL",
        "Zip5": "33131",
        "Zip4": "2011"
    },
    "corrections": [
        {
            "code": "32",
            "text": "Default address: The address you entered was found but more information is needed (such as an apartment, suite, or box number) to match to a specific address."
        }
    ],
    "additionalInfo": {
        "deliveryPoint": "99",
        "carrierRoute": "C038",
        "DPVConfirmation": "D",
        "DPVCMRA": "N",
        "business": "N",
        "centralDeliveryPoint": "",
        "vacant": "N"
    }
}

Correction codes: 32 = apartment/suite needed, 22 = multiple addresses found.

// Full address lookup (raw v3 response)
$result = Usps::addressLookup([
    'streetAddress' => '1600 Pennsylvania Ave NW',
    'city' => 'Washington',
    'state' => 'DC',
]);

Response:

{
    "firm": "",
    "address": {
        "streetAddress": "1600 PENNSYLVANIA AVE NW",
        "streetAddressAbbreviation": "1600 PENNSYLVANIA AVE NW",
        "secondaryAddress": "",
        "cityAbbreviation": "WASHINGTON",
        "city": "WASHINGTON",
        "state": "DC",
        "ZIPCode": "20500",
        "ZIPPlus4": "0005",
        "urbanization": ""
    },
    "additionalInfo": { ... },
    "corrections": [],
    "matches": [{ "code": "31", "text": "Single Response - exact match" }]
}
// City/State lookup by ZIP
$result = Usps::cityStateLookup('20500');

Response:

{
    "city": "WASHINGTON",
    "state": "DC",
    "ZIPCode": "20500"
}
// ZIP Code lookup by address
$result = Usps::zipCodeLookup([
    'streetAddress' => '1600 Pennsylvania Ave NW',
    'city' => 'Washington',
    'state' => 'DC',
]);

Response:

{
    "firm": "",
    "address": {
        "streetAddress": "1600 PENNSYLVANIA AVE NW",
        "city": "WASHINGTON",
        "state": "DC",
        "ZIPCode": "20500",
        "ZIPPlus4": "0005"
    }
}

Note: State names are auto-converted to abbreviations (FloridaFL). ZIP codes with dashes are auto-split (20500-0005 → ZIP5: 20500, ZIP4: 0005).

USPS Address API Documentation: developer.usps.com/api/81 — full response field definitions, DPV codes, correction codes, and error handling.

Tracking

// Track packages (supports up to 35 at once)
$result = Usps::tracking()->track([
    ['trackingNumber' => '9400111899223456789012'],
]);

// Register for email notifications
Usps::tracking()->registerNotifications('9400111899223456789012', [
    'uniqueTrackingID' => 'abc-123',
    'notifyEventTypes' => ['ALL_UPDATES'],
    'recipients' => [['email' => 'user@example.com']],
]);

// Request proof of delivery
Usps::tracking()->proofOfDelivery('9400111899223456789012', [
    'uniqueTrackingID' => 'abc-123',
    'recipients' => [['email' => 'user@example.com', 'firstName' => 'John', 'lastName' => 'Doe']],
]);

Domestic Labels

// Create a shipping label (requires payment authorization token)
$label = Usps::labels()->createLabel($labelData, $paymentToken);
$label = Usps::labels()->createLabel($labelData, $paymentToken, 'idempotency-key');

// Create a return label
$label = Usps::labels()->createReturnLabel($labelData, $paymentToken);

// Cancel/refund a label
Usps::labels()->cancelLabel('9205500000000000000000');

// Edit label attributes
Usps::labels()->editLabel('9205500000000000000000', $patchData);

// First-Class indicia
Usps::labels()->createIndicia($indiciaData, $paymentToken);

// Intelligent Mail Barcode
Usps::labels()->createImb($imbData, $paymentToken);
Usps::labels()->cancelImb('00000000000000000000');

// Label branding
Usps::labels()->uploadBranding($svgData);
Usps::labels()->listBranding(limit: 10, offset: 0);
Usps::labels()->getBranding('image-uuid');
Usps::labels()->deleteBranding('image-uuid');
Usps::labels()->renameBranding('image-uuid', ['name' => 'new-name']);

// Reprint a label
Usps::labels()->reprintLabel('9205500000000000000000', $reprintData, $paymentToken);

International Labels

$label = Usps::internationalLabels()->createLabel($labelData, $paymentToken);
Usps::internationalLabels()->reprintLabel('CX123456789US', $reprintData, $paymentToken);
Usps::internationalLabels()->cancelLabel('CX123456789US');
Usps::internationalLabels()->createIndicia($indiciaData, $paymentToken);

Domestic Prices

// Base rate search
$rate = Usps::domesticPrices()->baseRateSearch([
    'originZIPCode' => '20500',
    'destinationZIPCode' => '33101',
    'weight' => 2.5,
    'mailClass' => 'PRIORITY_MAIL',
]);

// Extra service rate search
$rate = Usps::domesticPrices()->extraServiceRateSearch($rateIngredients);

// List eligible products
$list = Usps::domesticPrices()->baseRateListSearch($rateIngredients);

// Total rates (base + extra services)
$total = Usps::domesticPrices()->totalRateSearch($rateIngredients);

// First-Class letter rates
$letter = Usps::domesticPrices()->letterRateSearch($rateIngredients);

International Prices

$rate = Usps::internationalPrices()->baseRateSearch($rateIngredients);
$rate = Usps::internationalPrices()->extraServiceRateSearch($rateIngredients);
$list = Usps::internationalPrices()->baseRateListSearch($rateIngredients);
$total = Usps::internationalPrices()->totalRateSearch($rateIngredients);
$letter = Usps::internationalPrices()->letterRateSearch($rateIngredients);

Service Standards

// Delivery estimates (with dates and acceptance locations)
$estimates = Usps::serviceStandards()->getEstimates('20500', '33101', [
    'mailClass' => 'PRIORITY_MAIL',
]);

// Service standards (average delivery days)
$standards = Usps::serviceStandards()->getStandards('20500', '33101');

Service Standards Directory

// Get valid 5-digit ZIP codes
$zips = Usps::serviceStandardsDirectory()->getValidZip5Codes();

// Get directory report (paginated)
$report = Usps::serviceStandardsDirectory()->getReport([
    'originZIPCode' => '230',
    'destinationZIPCode' => '330',
    'responseFormat' => '3D_BASE',
    'mailClass' => 'PERIODICALS',
    'limit' => 10000,
]);

Service Standards Files

// List available files
$files = Usps::serviceStandardsFiles()->listFiles();

// Get a signed download URL
$download = Usps::serviceStandardsFiles()->generateSignedUrl('Combined_Service_Standard_Directory_MKT');

International Service Standards

$standard = Usps::internationalServiceStandards()->getServiceStandard('CA', 'PRIORITY_MAIL_INTERNATIONAL');
// Returns: { countryCode, mailClass, serviceStandardMessage }

Locations

// Dropoff locations for destination entry parcels
$locations = Usps::locations()->getDropoffLocations('33101', [
    'mailClass' => 'PARCEL_SELECT',
]);

// Post office locations
$offices = Usps::locations()->getPostOfficeLocations([
    'ZIPCode' => '33101',
    'radius' => 10,
]);

// Parcel locker locations
$lockers = Usps::locations()->getParcelLockerLocations(['ZIPCode' => '33101']);

Carrier Pickup

// Check pickup eligibility
$eligible = Usps::carrierPickup()->checkEligibility('123 Main St', [
    'ZIPCode' => '33101',
    'state' => 'FL',
]);

// Schedule a pickup
$pickup = Usps::carrierPickup()->schedulePickup([
    'pickupDate' => '2026-04-01',
    'pickupAddress' => [...],
    'packages' => [['packageType' => 'PRIORITY_MAIL', 'packageCount' => 2]],
    'estimatedWeight' => 5.0,
    'pickupLocation' => ['packageLocation' => 'FRONT_DOOR'],
]);

// Get, update, cancel
$details = Usps::carrierPickup()->getPickup('WTC12345');
Usps::carrierPickup()->updatePickup('WTC12345', $data, $etag);
Usps::carrierPickup()->cancelPickup('WTC12345', $etag);

Containers

// Create a container label
$container = Usps::containers()->createContainer($containerData);

// Add/remove packages
Usps::containers()->addPackages('CNT-001', ['trackingNumbers' => [...]]);
Usps::containers()->removePackage('CNT-001', '9400111899223456789012');
Usps::containers()->removeAllPackages('CNT-001');

// Close container and generate manifest
Usps::containers()->createManifest(['containers' => ['CNT-001']]);

Payments

// Create payment authorization token (required for labels)
$auth = Usps::payments()->createPaymentAuthorization([
    'roles' => [
        ['roleName' => 'PAYER', 'CRID' => '12345678', 'accountType' => 'EPS', 'accountNumber' => '1234'],
        ['roleName' => 'LABEL_OWNER', 'CRID' => '12345678', 'MID' => '123456', 'manifestMID' => '123456'],
    ],
]);
$token = $auth['paymentAuthorizationToken'];

// Check account balance
$account = Usps::payments()->getPaymentAccount('12345678', 'EPS', ['amount' => 50.00]);

Informed Delivery Campaigns (Mail)

$campaign = Usps::campaigns()->createCampaign($campaignData);
$list = Usps::campaigns()->searchCampaigns(['status' => 'ACTIVE']);
$detail = Usps::campaigns()->getCampaign('CAM-001');
Usps::campaigns()->updateCampaign('CAM-001', $data);
Usps::campaigns()->cancelCampaign('CAM-001');
Usps::campaigns()->addImbs('CAM-001', ['imbs' => [...]]);

// Callback key management
$keys = Usps::campaigns()->getCallbackKeys('12345');
$summary = Usps::campaigns()->getCallbackSummary('12345', 'key-abc');
$details = Usps::campaigns()->getCallbackDetails('12345', 'key-abc');

Informed Delivery Package Campaigns

$campaign = Usps::packageCampaigns()->createCampaign($campaignData);
$list = Usps::packageCampaigns()->searchCampaigns();
$detail = Usps::packageCampaigns()->getCampaign('PKG-001');
Usps::packageCampaigns()->updateCampaign('PKG-001', $data);
Usps::packageCampaigns()->cancelCampaign('PKG-001');
Usps::packageCampaigns()->addTrackingNumbers('PKG-001', ['trackingNumbers' => [...]]);

Adjustments

$adjustments = Usps::adjustments()->getAdjustments('12345678', '920011234561234567890', 'CENSUS');
$adjustments = Usps::adjustments()->getAdjustments('12345678', '920011234561234567890', 'DUPLICATES', [
    'destinationZIPCode' => '33101',
]);

Disputes

$dispute = Usps::disputes()->createDispute([
    'EPSTransactionID' => 'TXN-001',
    'trackingID' => '920011234561234567890',
    'CRID' => '12345678',
    'reason' => 'INCORRECT_ASSESSED_WEIGHT',
    'description' => 'Weight was measured incorrectly.',
    'name' => 'John Doe',
    'disputeCount' => '1',
]);

Appointments (FAST)

// Check availability
$slots = Usps::appointments()->getAvailability(['facilityId' => 'FAC-001']);

// Create, update, cancel
$appt = Usps::appointments()->createAppointment($appointmentData);
Usps::appointments()->updateAppointment($updateData);
Usps::appointments()->cancelAppointment(['appointmentId' => 'APT-001']);

Shipping Options

// Combined pricing + service standards + available options in one call
$options = Usps::shippingOptions()->search([
    'originZIPCode' => '20500',
    'destinationZIPCode' => '33101',
    'weight' => 2.5,
    'length' => 12,
    'width' => 8,
    'height' => 4,
    'mailClass' => 'PRIORITY_MAIL',
]);

SCAN Forms

// Label shipment — link tracking numbers to a single EFN
$form = Usps::scanForms()->createLabelShipment([
    'trackingNumbers' => ['9400111899223456789012', '9400111899223456789013'],
]);

// MID shipment
$form = Usps::scanForms()->createMidShipment([
    'MID' => '123456',
    'trackingNumbers' => ['9400111899223456789012'],
]);

// Manifest MID shipment
$form = Usps::scanForms()->createManifestMidShipment([
    'manifestMID' => '654321',
    'MID' => '123456',
]);

API Domains

Domain Facade Accessor Endpoints
Addresses Direct methods 3
Tracking tracking() 3
Domestic Labels labels() 12
International Labels internationalLabels() 4
Domestic Prices domesticPrices() 5
International Prices internationalPrices() 5
Service Standards serviceStandards() 2
Service Standards Directory serviceStandardsDirectory() 2
Service Standards Files serviceStandardsFiles() 2
International Service Standards internationalServiceStandards() 1
Locations locations() 3
Carrier Pickup carrierPickup() 5
Containers containers() 5
Payments payments() 2
Campaigns campaigns() 9
Package Campaigns packageCampaigns() 6
Adjustments adjustments() 1
Disputes disputes() 1
Appointments appointments() 4
Shipping Options shippingOptions() 1
SCAN Forms scanForms() 3

Authentication

The package automatically handles OAuth2 client credentials flow. Access tokens are cached per-scope for ~50 minutes to minimize token requests. Each API domain requests only the scopes it needs.

License

MIT