beziworld / laravel-ups
UPS REST API client for Laravel: OAuth2, shipments with GIF/ZPL/EPL labels and customs forms, void, label recovery, rating, tracking, address validation, Access Point locator, pickups and paperless documents.
Requires
- php: ^8.3
- ext-json: *
- guzzlehttp/guzzle: ^7.8
- illuminate/cache: ^12.0|^13.0
- illuminate/console: ^12.0|^13.0
- illuminate/contracts: ^12.0|^13.0
- illuminate/events: ^12.0|^13.0
- illuminate/http: ^12.0|^13.0
- illuminate/support: ^12.0|^13.0
Requires (Dev)
- larastan/larastan: ^3.0
- laravel/pint: ^1.18
- orchestra/testbench: ^10.0|^11.0
- pestphp/pest: ^3.7|^4.0
- pestphp/pest-plugin-laravel: ^3.0|^4.0
- phpstan/phpstan-strict-rules: ^2.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-11-04 19:46:20 UTC
README
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.