ivanmitrikeski/laravel-shipping
Laravel package for multi-carrier shipping quotes and shipments, supporting UPS, FedEx, Canada Post, USPS and Purolator rate and shipment APIs, with flat-rate models and standalone use outside Laravel.
Installation:
composer require ivanmitrikeski/laravel-shipping
php artisan migrate
Run migrations to create shipments, shipment_providers, and related tables.
v1.0.10 note: this release moves the package onto Guzzle 8. The HTTP client is now a major version ahead of what most Laravel apps installed previously, so resolve it explicitly before running anything else:
composer update ivanmitrikeski/laravel-shipping guzzlehttp/guzzle -W
composer show guzzlehttp/guzzle # confirm you're on 8.x, not 7.x
If another package pins guzzlehttp/guzzle:^7.0, Composer will refuse the update — that's the first thing to check if composer require fails after upgrading.
Configure .env:
Add credentials for your target providers (e.g., UPS_CLIENT_ID, CANADA_POST_CUSTOMER_NUMBER). Set SHIPPING_SANDBOX=true for testing.
First Use Case: Fetch rates for a shipment via UPS:
use IvanMitrikeski\Shipping\Facades\Shipping;
$rates = Shipping::rates('ups')
->credentials()
->setShipper([
'name' => 'Your Company',
'address' => '123 Main St',
// ... other required fields
])
->setRecipient([
'name' => 'Recipient Name',
'address' => '456 Oak Ave',
])
->setPackage([
'weight' => 2.0, // in lbs
'dimensions' => ['length' => 10, 'width' => 5, 'height' => 5],
])
->get();
Rate Calculation:
Chain methods to build a request, then call get():
$rates = Shipping::rates('fedex')
->credentials()
->setShipper($shipperData)
->setRecipient($recipientData)
->setPackages([$package1, $package2]) // Array for multi-package shipments
->get();
->sandbox() for testing without modifying .env.Shipment Creation: For providers supporting shipment APIs (UPS, FedEx, etc.):
$shipment = Shipping::shipment('ups')
->credentials()
->setShipper($shipperData)
->setRecipient($recipientData)
->setPackages([$package])
->setService('01') // UPS Ground
->create();
Provider-Specific Logic:
Shipping::rates('usps')..env early.Reusable Credentials: Store provider credentials in a config file or database for dynamic switching:
$credentials = config('shipping.providers.ups');
$rates = Shipping::rates('ups')
->credentials($credentials)
->get();
Handling Responses: Iterate over rate results:
foreach ($rates as $rate) {
echo $rate->service . ': ' . $rate->rate->amount . ' ' . $rate->rate->currency;
}
Laravel Services: Bind the package to a service container for dependency injection:
$this->app->singleton(Shipping::class, function ($app) {
return new \IvanMitrikeski\Shipping\ShippingManager();
});
API Caching: Cache rate responses to reduce API calls (e.g., for cart pages):
$rates = Cache::remember("shipping_rates_{$cacheKey}", now()->addHours(1), function () {
return Shipping::rates('ups')->get();
});
Validation: Validate package dimensions/weights before API calls:
$validator = Validator::make($request->all(), [
'weight' => 'required|numeric|min:0.1',
'dimensions.length' => 'required|numeric|min:1',
]);
Logging: Log API responses for debugging:
Shipping::rates('ups')
->setLogger(app(\Monolog\Logger::class))
->get();
Queueable Jobs: Offload shipment creation to a queue for long-running processes:
CreateShipmentJob::dispatch($shipmentData)->onQueue('shipping');
Inject the HTTP client instead of constructing it (Guzzle 8 friendly):
Since v1.0.10 the package resolves Guzzle 8, where client construction options and PSR-7 handler behaviour have moved on. Prefer resolving the client (or the package service) from the container over new \GuzzleHttp\Client([...]) in your own code:
// app/Providers/AppServiceProvider.php
$this->app->bind(\GuzzleHttp\ClientInterface::class, function () {
return new \GuzzleHttp\Client([
'timeout' => 15,
'connect_timeout' => 5,
// Guzzle 8: check option names against the current upgrade guide
]);
});
Type-hint ClientInterface, not Client, in custom providers/parsers — that survives future major bumps.
Guzzle 8 upgrade (v1.0.10):
extend() closures, and any test doubles built on HandlerStack/MockHandler.composer update -W, then re-run your rate/shipment integration tests (sandbox credentials) before deploying. Anything that type-hints GuzzleHttp\Client concretely or relies on Guzzle 7 handler-stack middleware signatures should be switched to GuzzleHttp\ClientInterface and re-verified.psr/http-message, guzzlehttp/psr7) and promises versions now resolved in your composer.lock; older packages that pinned psr/http-message:^1.0 may need bumping alongside.Sandbox vs. Production:
SHIPPING_SANDBOX between environments..env files or a config override:
if (app()->environment('local')) {
config(['shipping.sandbox' => true]);
}
Provider-Specific Quirks:
billing_account or registered_account. Validate these early.Rate Limiting:
try {
$rates = Shipping::rates('usps')->get();
} catch (\IvanMitrikeski\Shipping\Exceptions\RateLimitException $e) {
sleep(10);
retry();
}
Currency/Unit Mismatches:
$weightKg = $weightLbs * 0.453592;
Missing Required Fields:
shipmentType).dd($rate->toArray()) to inspect the full response structure.Enable Verbose Logging:
Shipping::setLogLevel(\Monolog\Logger::DEBUG);
Check logs at storage/logs/laravel.log.
Inspect Raw API Responses:
Rather than hand-rolling a HandlerStack, attach a middleware to the client you bound in the container — this is the pattern most likely to survive the Guzzle 8 change:
use GuzzleHttp\Middleware;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
$stack = \GuzzleHttp\HandlerStack::create();
$stack->push(Middleware::tap(
fn (RequestInterface $req) => \Log::debug('API Request', [$req->getMethod(), (string) $req->getUri()]),
fn (RequestInterface $req, $opts, ResponseInterface $res) => \Log::debug('API Response', [$res->getStatusCode()]),
));
Then hand that client to the provider via Shipping::extend() (see Extension Points). Verify the middleware closure arity against Guzzle 8's docs — signatures are the usual casualty of a major bump.
Common Errors:
InvalidCredentialsException: Double-check .env values (e.g., UPS_ACCOUNT_NUMBER format).ValidationException: Use ->validate() to check inputs before API calls.ConnectionException: Ensure SHIPPING_SANDBOX matches your endpoint (e.g., sandbox.ups.com).Class not found / handler errors after upgrading: Almost always a stale composer.lock or a package still requiring Guzzle 7. Run composer why guzzlehttp/guzzle and composer why-not guzzlehttp/guzzle 8.0.Tests after the upgrade:
Swap the real client for a MockHandler in your test suite so provider tests stay offline and version-agnostic:
$mock = new \GuzzleHttp\Handler\MockHandler([new \GuzzleHttp\Psr7\Response(200, [], $fixtureJson)]);
$client = new \GuzzleHttp\Client(['handler' => \GuzzleHttp\HandlerStack::create($mock)]);
Custom Providers: Extend the package by creating a new provider class:
namespace App\Shipping\Providers;
use IvanMitrikeski\Shipping\Contracts\Provider;
class DHLService implements Provider {
public function getRates(array $data) {
// Custom logic
}
}
Register it in config/shipping.php:
'providers' => [
'dhl' => \App\Shipping\Providers\DHLService::class,
],
Modify Request/Response:
Override the default HTTP client or response parser. Use container resolution and ClientInterface so the override keeps working across Guzzle majors:
Shipping::extend('ups', function ($app) {
return new \IvanMitrikeski\Shipping\Providers\Ups\UpsService(
$app->make(\GuzzleHttp\ClientInterface::class),
new \App\Shipping\Parsers\CustomUpsParser()
);
});
The constructor signature you target must match the package's own — confirm it against the installed version after upgrading.
Add Custom Fields:
Extend the Shipment model or use accessors:
// app/Models/Shipment.php
public function getTrackingUrlAttribute() {
return $this->provider->getTrackingUrl($this->tracking_number);
}
Webhook Handling: For real-time updates (e.g., FedEx shipment status), create a controller:
public function handleWebhook(Request $request) {
$provider = $request->provider;
$data = $request->json()->all();
return Shipping::webhook($provider)->handle($data);
}
Pin your override points:
After the Guzzle 8 upgrade, add a smoke test that boots every registered provider (Shipping::rates($provider)) against sandbox credentials. Extension points are the surface area most likely to break on a dependency major, and a one-minute test catches it before deploy.
How can I help you explore Laravel packages today?