Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Couriers Laravel Package

nextbyte/laravel-couriers

Laravel Couriers provides a unified, configurable API for integrating multiple courier services into Laravel applications. Manage shipments, labels, tracking, and rate quotes through consistent drivers, with support for provider-specific settings, webhook handling, and extensible workflows.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and publish.

composer require nextbyte/laravel-couriers
php artisan vendor:publish --provider="Nextbyte\Couriers\CouriersServiceProvider"

Pin the version rather than floating: composer require nextbyte/laravel-couriers:1.5.0 --update-with-dependencies, then commit the lockfile. A package in the shipping path is not one to drift on ^1.5.

Where to look first, in this order.

  1. config/couriers.php — the default driver, per-carrier credential blocks, and any defaults (service code, package type, label format). This file is your real API surface; most "the package is doing something weird" reports resolve here.
  2. src/Contracts/ — the driver interface. Everything the package can do is enumerated there. Read it before reading any driver; it tells you which capabilities are actually uniform across carriers versus which are driver-specific bolt-ons.
  3. src/Drivers/ — one directory per carrier. Skim the driver you intend to use for its request/response mapping and any @throws.
  4. The service provider — how drivers are registered and how you'd add your own.

Credentials in .env, never in code.

COURIER_DRIVER=acme
COURIER_ACME_KEY=
COURIER_ACME_SECRET=
COURIER_ACME_ACCOUNT=
COURIER_ACME_SANDBOX=true

Confirm the config file reads env() at the top level (not inside a function or array merge), otherwise config:cache will bake in stale values and your rotation will silently no-op.

First use case: one shipment, end to end, in Tinker.

Do not start with an integration. Start with a single call:

$courier = app(\Nextbyte\Couriers\Contracts\Courier::class); // or the facade

$shipment = $courier->createShipment([
    'reference'   => 'ORD-1001',
    'service'     => 'ground',
    'from'        => [...],
    'to'          => [...],
    'parcels'     => [['weight' => 1.2, 'length' => 20, 'width' => 15, 'height' => 10]],
]);

$shipment->trackingNumber();
$shipment->label(); // binary PDF or a URL, depending on driver

Run that against the sandbox account before writing a single line of domain code. The vocabulary you discover here — service codes, package types, whether weight is grams or kilograms, whether the label comes back as bytes or a URL — is the actual work. Method and key names above are the conventional shape; verify them against src/Contracts/ rather than trusting this snippet.

Implementation Patterns

Put your own interface in front of it. This is the single most important pattern.

namespace App\Shipping;

interface ShippingGateway
{
    public function createLabel(Order $order): LabelResult;
    public function track(string $trackingNumber): TrackingStatus;
    public function cancel(string $trackingNumber): void;
}

Two implementations: CourierGateway (wraps the package) and StubGateway (returns fixture data, used in tests and local dev). Bind in a service provider:

$this->app->bind(ShippingGateway::class, fn ($app) => match (config('shipping.driver')) {
    'stub'    => new StubGateway(),
    default   => new CourierGateway($app->make(\Nextbyte\Couriers\Contracts\Courier::class)),
});

Nothing outside CourierGateway should mention the package. No Courier:: in controllers, no package DTOs on your models, no package exception classes in your Handler. This costs about a day and buys you testability without carrier credentials, carrier swaps without touching order flows, and a bounded exit if the package stalls. Score and dependents are both zero here — assume you may be the only consumer and plan the exit accordingly.

Map models to payloads in one place. A ShipmentMapper that converts Order/Address/OrderItem into the package's array or DTO shape keeps dimensional-weight rounding, country-code normalisation, and phone-format rules out of your checkout code. This is where 80% of the elapsed time goes, and it is where the bugs live.

Push label generation to a queue. Carrier calls run multi-second and occasionally multi-minute. A synchronous call inside the post-payment request will time out your web workers and strand orders in a half-committed state.

OrderPaid::dispatch($order); // listener queues GenerateLabel::dispatch($order)->onQueue('shipping')

Give the order an explicit label_pending state so the UI can say "preparing" instead of showing a broken link, and so a retry has something to write back to.

Idempotency guard, client-side. Retries without a carrier idempotency key create real labels and real charges. Before calling the gateway, check your own shipments table for an existing row keyed on order_id; write the row in the same transaction that dispatches the job. If the package does pass a reference/request-id through to the carrier, use it — but verify that in the driver source rather than assuming.

Carrier/service selection by config, not code. Model your lanes as a lookup:

// config/shipping.php
'lanes' => [
    'domestic-standard' => ['driver' => 'acme', 'service' => 'ground'],
    'eu-express'        => ['driver' => 'beta', 'service' => 'expedited'],
],

Then Courier resolves the manager with the lane's driver name if the package supports runtime driver selection (most manager-based packages do). If it only supports a single configured driver, run one gateway instance per lane via a small factory — do not hardcode carrier choices into Order logic.

Tracking: prefer webhooks, fall back to scheduled polling. If the driver exposes a webhook handler, register an unauthenticated (or signature-verified) route and translate the payload into your own TrackingStatus enum. If not, poll a TrackShipment job on a schedule with backoff that widens as the shipment ages, and stop polling once terminal. Either way, the terminal states in your enum are the contract — carrier state names vary wildly and change without notice.

Testing. Make StubGateway the default in phpunit.xml. For the real gateway, use fixture-based tests that assert your mapper's output shape, and one optional sandbox-gated integration test behind an env flag (SHIPPING_SANDBOX=1). Never let CI touch a live carrier.

Gotchas and Tips

Read the diff before you upgrade anything. The v1.5.0 release notes are a bare compare link. Semver says minor means additive, but that's an assumption. A minor bump can still change driver interfaces, config keys, or response object shapes — the three places that break consumers. Open the v1.4.3→v1.5.0 compare, classify the change, write a sentence in the ticket, then upgrade. Budget this as quarterly recurring work, not a one-off.

Treat parsed responses as untrusted. When a carrier silently nulls a field, a thin wrapper happily passes the null through and you persist a shipment with no tracking number. Validate the parsed response explicitly before writing it to your database, and fail loudly with context.

Build an exception taxonomy early. Errors arrive as carrier-specific exceptions, possibly wrapped. Your Handler should classify them into three buckets: retry (transient/5xx/timeout — requeue with backoff), fix the data (invalid address, unsupported service, oversized parcel — surface to the operator), and stop and call the carrier (auth failure, account suspended). Getting this wrong is how a nightly job hammers a carrier with the same bad address 500 times.

Check what gets logged before you go to production. Shipping payloads contain names, addresses, phone numbers, and sometimes customs contents. Confirm at LOG_LEVEL=debug that the package doesn't dump request bodies. If it does, either raise the log level for that channel or wrap the client. This is a one-minute check that is expensive to discover late.

Credential rotation shows up as a total outage. Every shipment fails at once with an auth error. Schedule a synthetic canary shipment in staging — one cheap label, weekly — so you learn about expiry from a dashboard and not from a warehouse.

Labels are audit documents. Store the PDF in object storage with a retention policy that satisfies your finance/audit people, not a blob column. You will be asked to reprint a label from eight months ago.

Duplicate-shipment failure mode. The nastiest incident in this domain isn't an exception, it's two identical labels on one order and two carrier charges. Guard with a unique index on shipments.order_id and a state machine that refuses to start a second shipment from label_created.

Sandbox and production do not behave identically. Sandbox carriers are lenient about addresses and slow to reject malformed parcels. Treat a green sandbox run as necessary, not sufficient — and validate against a small set of real orders in a low-risk lane first.

Extension points. If you need a carrier the package doesn't cover, the cleanest route is a custom driver implementing the package's contract, registered in the service provider, rather than a second parallel integration. Read one bundled driver end to end first — it is the best documentation the package has, given the release notes.

Version-drift tax. Confirm the package's composer.json PHP and Laravel constraints bracket your runtime before you build on it, and re-check at every Laravel major. A package that lags a framework major becomes a forced-upgrade project with no upstream support.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk
martialbe/flysystem-oss
chondal/3312client
jeroen-g/flickr
opscale-co/nova-genbi
itsjustvita/laravel-retention
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator
equipc/laravel-email-database-log
adedsec/nova-translated-labels