laravel-ups maintained by beziworld
Laravel UPS
UPS REST API client for Laravel:
- OAuth 2.0 client credentials with cached tokens, CIE (sandbox) and production
- typed shipment builder: shipper, recipient, packages with weight and dimensions, references, declared value, Saturday delivery, COD, UPS Access Point delivery, negotiated rates, duties paid by the shipper, third-party billing
- international shipments with a UPS-generated commercial invoice and paperless documents (Forms History)
- labels in GIF, ZPL, EPL or SPL, decoded from base64; label recovery, also as PDF; void shipments or single packages
- rating (one service or "shop" all services) with published and negotiated prices
- tracking with typed events and normalised statuses, tracking by reference
- address validation and classification, UPS Access Point locator, on-call pickups
- several UPS accounts (named connections and
Ups::using()for runtime credentials), Laravel events,ups:checkcommand
Why this package
| Package | API | Laravel 12/13 | Notes |
|---|---|---|---|
gabrielbull/ups-api |
legacy XML with an access key | no | last release 2022 |
ptondereau/laravel-ups-api |
wraps gabrielbull/ups-api |
yes | legacy API underneath |
rawilk/laravel-ups |
legacy | no (Laravel 7/8, PHP 7.4) | |
shipstream/ups-rest-php-sdk |
REST (generated) | not a Laravel package | PHP 7.4 SDK, no token cache |
laravelshipping/ups |
REST | Laravel 10 to 12 | new, part of a suite |
This package targets the current OAuth 2.0 REST APIs (Shipping v2409, Rating v2409, Tracking v1, Address
Validation v2, Locator v3, Pickup v2409, Paperless v2) and adds a typed builder, decoded labels and documents,
multiple accounts, the Laravel HTTP client (Http::fake() works in your tests), a strict no-retry policy for writes
and Laravel events.
Installation
composer require beziworld/laravel-ups
php artisan vendor:publish --tag=ups-config # optional
UPS_CLIENT_ID=... # application on developer.ups.com
UPS_CLIENT_SECRET=...
UPS_ACCOUNT_NUMBER=A1B2C3 # six-character UPS shipper number
UPS_SANDBOX=true # wwwcie.ups.com (Customer Integration Environment)
UPS_SHIPPER_NAME="ACME Sp. z o.o."
UPS_SHIPPER_CONTACT="Jan Nowak"
UPS_SHIPPER_PHONE=500600700
UPS_SHIPPER_EMAIL=shop@example.com
UPS_SHIPPER_ADDRESS="ul. Prosta 1"
UPS_SHIPPER_CITY=Warszawa
UPS_SHIPPER_POSTAL_CODE=00-001
UPS_SHIPPER_COUNTRY=PL
UPS_UNITS=metric # metric (KGS, CM) or imperial (LBS, IN)
UPS_LABEL_FORMAT=GIF # GIF, ZPL, EPL or SPL
Check the credentials (requests a token, no other call): php artisan ups:check.
Create a shipment
use Beziworld\Ups\Data\Money;
use Beziworld\Ups\Data\Parcel;
use Beziworld\Ups\Data\Party;
use Beziworld\Ups\Enums\LabelFormat;
use Beziworld\Ups\Enums\Service;
use Beziworld\Ups\Facades\Ups;
$shipment = Ups::shipment()
->to(new Party('Max Mustermann', 'Potsdamer Str. 1', 'Berlin', '10785', 'DE', phone: '030123456', email: 'max@example.com'))
->service(Service::Standard)
->parcel(new Parcel(weight: 2.5, length: 30, width: 20, height: 10, reference: 'ORDER-1'))
->parcel(new Parcel(weight: 1.2, declaredValue: Money::of(200, 'EUR')))
->reference('ORDER-1')
->labelFormat(LabelFormat::Zpl)
->create();
$shipment->shipmentId; // 1Z12345E6605272234
$shipment->trackingNumbers; // one per package
$shipment->label()->content; // decoded label (GIF image or ZPL commands)
$shipment->totalCharges; // Money: published price; negotiatedCharges with ->negotiatedRates()
Ups::voidShipment($shipment->shipmentId); // whole shipment
Ups::voidShipment($shipment->shipmentId, ['1Z...']); // selected packages
Ups::recoverLabel('1Z12345E6605272234', LabelFormat::Pdf); // list<Label>, PDF on demand
Other builder options: shipper(), shipFrom(), description(), saturdayDelivery(),
cashOnDelivery(Money, $fundsCode), toAccessPoint($id, Party $location, holdForPickup: true),
billReceiver(), billThirdParty(), dutiesPaidByShipper(), negotiatedRates(), shipDate(), validateCity(),
documentsOnly(), invoiceLineTotal() and with([...]) for any other element of the Shipment container.
toArray() returns the request body without sending it.
International shipments
use Beziworld\Ups\Data\Customs;
use Beziworld\Ups\Data\CustomsItem;
use Beziworld\Ups\Data\PaperlessDocument;
$shipment = Ups::shipment()
->to(new Party('John Smith', '5th Avenue 1', 'New York', '10001', 'US', stateCode: 'NY'))
->service(Service::WorldwideSaver)
->parcel(new Parcel(1.0))
->description('Cotton T-shirts')
->customs(new Customs(
items: [new CustomsItem('Cotton T-shirt', 3, '12.00', hsCode: '610910', originCountry: 'PL')],
currency: 'USD',
invoiceNumber: 'FV/1/2026',
termsOfShipment: 'DAP',
))
->create();
$shipment->documents[0]->content; // commercial invoice generated by UPS (PDF)
// Paperless: upload your own invoice and attach it to the shipment.
$ids = Ups::uploadDocuments([PaperlessDocument::fromPath(storage_path('invoice.pdf'))]);
Ups::shipment()->/* ... */->paperlessDocuments($ids)->create();
Ups::attachDocuments($ids, $shipmentId, now()); // for a shipment created without them
Rates
$builder = Ups::shipment()->to($recipient)->parcel(new Parcel(3))->negotiatedRates();
$rate = $builder->service(Service::Standard)->rate(); // Rate
$rates = $builder->shop(); // list<Rate>, cheapest first
$rate->price(); // negotiated price when available, otherwise published
$rate->businessDaysInTransit; // from GuaranteedDelivery, when returned
Tracking
$package = Ups::track('1Z12345E6605272234');
$package->status; // TrackingStatus::Delivered, InTransit, Exception, PickedUp, LabelCreated, Unknown
$package->deliveredAt; // CarbonImmutable
foreach ($package->events as $event) { // newest first
$event->occurredAt; // local scan time with the UPS offset
$event->description;
$event->city;
}
Ups::trackShipment('1Z...'); // every package of the shipment
Ups::trackByReference('ORDER-1', now()->subWeek(), now());
Addresses, Access Points and pickups
use Beziworld\Ups\Data\PickupPiece;
use Beziworld\Ups\Data\PickupRequest;
$result = Ups::validateAddress(new Party('ACME', '26601 Aliso Creek Rd', 'Aliso Viejo', '92656', 'US', stateCode: 'CA'));
$result->valid; $result->isResidential(); $result->candidates;
$points = Ups::accessPoints(new Party('x', 'Marszałkowska 1', 'Warszawa', '00-001', 'PL'), limit: 10, radius: 5);
$points = Ups::accessPointsNear(52.23, 21.01, 'PL');
Ups::shipment()->toAccessPoint($points[0]->id, $points[0]->toParty());
$pickup = Ups::schedulePickup(new PickupRequest(
date: now()->addDay(),
readyTime: '09:00',
closeTime: '17:00',
pieces: [new PickupPiece(Service::Standard, quantity: 2, destinationCountryCode: 'DE')],
totalWeight: 5.0,
));
Ups::cancelPickup($pickup->prn);
Street level address validation covers the United States and Puerto Rico; in the CIE environment UPS answers only for New York addresses.
Multiple accounts
// config/ups.php: 'connections' => ['default' => [...], 'eu' => [...]]
Ups::connection('eu')->shipment()->...;
// Credentials stored per tenant:
Ups::using(['client_id' => $tenant->ups_id, 'client_secret' => $tenant->ups_secret, 'account_number' => $tenant->ups_account])->track('1Z...');
Tokens are cached per client id and account in the configured cache store (UPS_CACHE_STORE).
Events
ShipmentCreated, ShipmentVoided and PickupScheduled (namespace Beziworld\Ups\Events) carry the connection
name and the result object.
Errors
| Exception | When |
|---|---|
ApiException |
UPS rejected the request or could not be reached: errorCode() (e.g. 120100), errors(), httpStatus(), isNotFound(), isRateLimited() with retryAfter() |
AuthenticationException |
the OAuth endpoint refused the client id or secret (e.g. 10401 ClientId is Invalid) |
InvalidConfiguration |
missing credentials, account number, shipper, recipient or parcels |
All extend UpsException. Read-only calls are retried on connection errors and HTTP 5xx (UPS_RETRIES);
shipments, voids, pickups and paperless uploads are never retried automatically. A request rejected with HTTP 401
is repeated once with a new token.
Development
composer install
composer ci # pint --test, phpstan (level 8), pest
Author
Created and maintained by Kamil Ćwiertnia, beziworld.eu.
- E-mail: biuro@beziworld.eu
- Issues: GitHub issues
Need an integration? Get in touch through beziworld.eu.
Other packages by the author
laravel-fedex · laravel-dhl-express · laravel-dpd · laravel-inpost · laravel-furgonetka
License
MIT. See LICENSE.md. This package is not affiliated with, endorsed or sponsored by United Parcel Service. UPS and the UPS brandmark are trademarks of United Parcel Service of America, Inc.