PHPackages                             laraditz/courier - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [API Development](/categories/api)
4. /
5. laraditz/courier

ActiveLibrary[API Development](/categories/api)

laraditz/courier
================

A unified interface for multiple courier and shipping carrier services in Laravel.

v1.1.0(2w ago)0192MITPHPPHP ^8.1

Since Jun 18Pushed 1w agoCompare

[ Source](https://github.com/laraditz/courier)[ Packagist](https://packagist.org/packages/laraditz/courier)[ RSS](/packages/laraditz-courier/feed)WikiDiscussions main Synced 2w ago

READMEChangelogDependencies (8)Versions (6)Used By (2)

Laravel Courier
===============

[](#laravel-courier)

[![Latest Version on Packagist](https://camo.githubusercontent.com/e3ea7d94894c006d164f0edafb0e0d971fcbe683f7495711fe078708140da592/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f6c6172616469747a2f636f75726965722e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/laraditz/courier)[![Total Downloads](https://camo.githubusercontent.com/a926ab3e2a7b3da8cf21cc420082d949487765c6b876d57b2e996152437c43b3/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f6c6172616469747a2f636f75726965722e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/laraditz/courier)[![License](https://camo.githubusercontent.com/98bb987092bbeba1bfed502566d93a3c3eac777a6258cc784ce08553bdba5780/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f6c2f6c6172616469747a2f636f75726965722e7376673f7374796c653d666c61742d737175617265)](./LICENSE.md)

A unified interface for multiple courier and shipping carrier services in Laravel.

Overview
--------

[](#overview)

This package provides a driver-based abstraction layer for courier integrations. Define your shipments once using strongly-typed DTOs — the driver handles the carrier-specific API calls and returns normalized results.

Each carrier ships as a separate Composer package. Install only what you need.

Requirements
------------

[](#requirements)

- PHP 8.1+
- Laravel 10, 11, 12, or 13

Installation
------------

[](#installation)

```
composer require laraditz/courier
```

The service provider is auto-discovered. Publish the config:

```
php artisan vendor:publish --tag=courier-config
```

Publish and run the migrations to create the API/webhook log tables:

```
php artisan vendor:publish --tag=courier-migrations
php artisan migrate
```

Available Drivers
-----------------

[](#available-drivers)

PackageCarrier[laraditz/courier-sfexpress](https://github.com/laraditz/courier-sfexpress)SF Express[laraditz/courier-lalamove](https://github.com/laraditz/courier-lalamove)Lalamove[laraditz/courier-jt-express](https://github.com/laraditz/courier-jt-express)J&amp;T Express (MY)Configuration
-------------

[](#configuration)

`config/courier.php`:

```
return [
    'default' => env('COURIER_DRIVER', 'sfexpress'),

    'drivers' => [
        'sfexpress' => [
            'key'              => env('SFEXPRESS_KEY'),
            'secret'           => env('SFEXPRESS_SECRET'),
            'customer_code'    => env('SFEXPRESS_CUSTOMER_CODE'),
            'encoding_aes_key' => env('SFEXPRESS_AES_KEY'),
            'pay_month_card'   => env('SFEXPRESS_PAY_MONTH_CARD'),
            'country'          => env('SFEXPRESS_COUNTRY', 'MY'),
            'scope_name'       => env('SFEXPRESS_SCOPE', 'OSMY'),
            'sandbox'          => env('SFEXPRESS_SANDBOX', false),
        ],
    ],
];
```

Available Methods
-----------------

[](#available-methods)

MethodParametersReturnsDescription`createShipment``ShipmentPayload $payload``ShipmentResult`Book a new shipment and get a waybill number`getShipment``string $reference``ShipmentResult`Look up a shipment by your own order reference`track``string $trackingNumber``TrackingResult`Get current status and full tracking history`getRates``RatePayload $payload``RateCollection`Fetch available service options and prices for a route`cancelShipment``string $waybillNumber, ?string $reference = null``CancelResult`Cancel an existing shipment`getLabel``string $waybillNumber, ?string $reference = null``LabelResult`Retrieve the shipping label (PDF bytes or ZPL)`getAvailability``AvailabilityPayload $payload``ServiceCollection`List services available between two locations> **Driver support:** Not all drivers implement every method. Calling an unsupported method throws `Laraditz\Courier\Exceptions\UnsupportedOperationException`. Check the driver's documentation for which methods are available.
>
> **`$reference`:** your own order reference, if one was supplied (or generated by the driver) when the shipment was created — see `ShipmentResult::$reference` below. Some carriers (e.g. J&amp;T Express) require it to cancel a shipment or fetch its label, since their APIs key off your reference rather than the waybill number. Pass it back in from wherever you persisted the original `ShipmentResult`.

### Result DTOs

[](#result-dtos)

DTOKey Properties`ShipmentResult``waybillNumber`, `status`, `estimatedDelivery`, `reference: ?string`, `meta()``TrackingResult``waybillNumber`, `status`, `estimatedDelivery`, `events[]`, `meta()``TrackingEvent``timestamp`, `location`, `description`, `status``RateCollection``items[]` → `RateOption``RateOption``serviceCode`, `serviceName`, `price`, `currency`, `estimatedDays`, `meta()``CancelResult``success`, `message`, `meta()``LabelResult``waybillNumber`, `format`, `content`, `meta()``ServiceCollection``items[]` → `ServiceOption``ServiceOption``code`, `name`, `description`, `estimatedDays`### Payload DTOs

[](#payload-dtos)

DTOProperties`ShipmentPayload``sender: Address`, `recipient: Address`, `parcel: Parcel`, `serviceCode: string`, `remarks: ?string`, `scheduledAt: ?Carbon`, `reference: ?string``RatePayload``origin: Location`, `destination: Location`, `parcel: Parcel`, `serviceCode: string``AvailabilityPayload``origin: Location`, `destination: Location``Address``name`, `phone`, `email`, `line1`, `line2`, `line3`, `city`, `state`, `postcode`, `country`, `lat`, `lng``Location``postcode`, `city`, `state`, `country`, `lat`, `lng``Parcel``weight`, `length`, `width`, `height`, `declaredValue`, `description`, `quantity`---

Usage
-----

[](#usage)

### Create a Shipment

[](#create-a-shipment)

```
use Laraditz\Courier\Facades\Courier;
use Laraditz\Courier\DTOs\Shared\Address;
use Laraditz\Courier\DTOs\Shared\Parcel;
use Laraditz\Courier\DTOs\Payloads\ShipmentPayload;

$result = Courier::createShipment(new ShipmentPayload(
    sender: new Address(
        name: 'Raditz Farhan',
        phone: '+60123456789',
        email: null,
        line1: 'No 1 Jalan Test',
        line2: null,
        line3: null,
        city: 'Kuala Lumpur',
        state: 'Wilayah Persekutuan',
        postcode: '50000',
        country: 'MY',
    ),
    recipient: new Address(/* ... */),
    parcel: new Parcel(
        weight: 1.5,
        length: 20.0,
        width: 15.0,
        height: 10.0,
        declaredValue: 100.0,
        description: 'Goods',
        quantity: 1,
    ),
    serviceCode: 'M102',
));

$result->waybillNumber; // 'MYIU1234715622'
$result->status;        // 'pending'
$result->reference;     // your $reference (or a generated one) — persist this if the driver needs it later
```

### Look Up a Shipment by Reference

[](#look-up-a-shipment-by-reference)

```
$result = Courier::getShipment('ORDER-001');

$result->waybillNumber; // 'MYIU1234715622'
```

Not every driver supports order inquiry — check the driver's documentation.

### Track a Shipment

[](#track-a-shipment)

```
$result = Courier::track('MYIU1234715622');

$result->waybillNumber;  // 'MYIU1234715622'
$result->status;         // 'in_transit'
$result->events;         // TrackingEvent[]

foreach ($result->events as $event) {
    $event->timestamp;   // Carbon
    $event->location;    // 'Kuala Lumpur Hub'
    $event->description; // 'Package picked up'
    $event->status;      // 'picked_up'
}
```

### Get Rates

[](#get-rates)

```
use Laraditz\Courier\DTOs\Shared\Location;
use Laraditz\Courier\DTOs\Payloads\RatePayload;

$rates = Courier::getRates(new RatePayload(
    origin: new Location('50000', 'Kuala Lumpur', 'Wilayah Persekutuan', 'MY'),
    destination: new Location('10000', 'Georgetown', 'Pulau Pinang', 'MY'),
    parcel: new Parcel(1.5, 20, 15, 10, 100, 'Goods', 1),
));

foreach ($rates->items as $option) {
    $option->serviceCode;    // 'STANDARD'
    $option->serviceName;    // 'Standard Delivery'
    $option->price;          // 12.50
    $option->currency;       // 'MYR'
    $option->estimatedDays;  // 3
}
```

### Other Operations

[](#other-operations)

```
// Cancel
$result = Courier::cancelShipment('MYIU1234715622');
$result->success;  // true

// Cancel — some drivers require the original reference too
$result = Courier::cancelShipment('MYIU1234715622', 'ORDER-001');

// Get label — content is raw bytes (PDF or ZPL depending on driver)
$result = Courier::getLabel('MYIU1234715622');
$result->format;   // 'pdf'
$result->content;  // raw bytes

// Get label — some drivers require the original reference too
$result = Courier::getLabel('MYIU1234715622', 'ORDER-001');

// Check service availability by route
$services = Courier::getAvailability(new AvailabilityPayload(
    origin: new Location('50000', 'Kuala Lumpur', 'Wilayah Persekutuan', 'MY'),
    destination: new Location('10000', 'Georgetown', 'Pulau Pinang', 'MY'),
));
```

### Switching Drivers

[](#switching-drivers)

```
// Use a specific driver explicitly
Courier::driver('sfexpress')->track('MYIU1234715622');
```

### Webhooks

[](#webhooks)

The package registers a `POST /courier/webhook/{driver}` route that drivers can use to receive carrier push notifications.

To enable webhook support in a driver, implement the `HandlesWebhooks` interface:

```
use Illuminate\Http\Request;
use Laraditz\Courier\Contracts\HandlesWebhooks;

class MyCarrierDriver implements CourierDriver, HandlesWebhooks
{
    public function verifyWebhook(Request $request): bool
    {
        // Validate the carrier's signature/token
        return $request->header('X-Carrier-Token') === config('courier.drivers.mycarrier.webhook_secret');
    }

    public function handleWebhook(Request $request): void
    {
        // Process the carrier-specific payload
    }
}
```

When a valid webhook is received, the package fires a `WebhookReceived` event:

```
use Laraditz\Courier\Events\WebhookReceived;

// In your EventServiceProvider
Event::listen(WebhookReceived::class, function (WebhookReceived $event) {
    $event->driver;  // 'mycarrier'
    $event->payload; // raw request data array
});
```

Drivers that do not implement `HandlesWebhooks` return `404`. Requests that fail `verifyWebhook` return `401`.

To associate an incoming webhook log with the shipment it relates to, a driver can also implement `ExtractsWebhookReference`:

```
use Illuminate\Http\Request;
use Laraditz\Courier\Contracts\ExtractsWebhookReference;

class MyCarrierDriver implements CourierDriver, HandlesWebhooks, ExtractsWebhookReference
{
    public function extractWebhookReference(Request $request): array
    {
        return [
            'reference' => $request->input('order_no'),
            'waybillNumber' => $request->input('waybill_no'),
        ];
    }
}
```

Extraction is best-effort — if it throws, the failure is swallowed and logging/processing continues without a reference.

### Logging

[](#logging)

Every outbound API call made through `CourierHttpClient` and every inbound webhook request is recorded (see [Installation](#installation) for the migrations), giving you a full audit trail per shipment.

Configure logging in `config/courier.php`:

```
'logging' => [
    'enabled' => env('COURIER_LOGGING_ENABLED', true),

    // Days to keep logs for `courier:prune-logs`. Set to null to disable pruning.
    'retention_days' => env('COURIER_LOGGING_RETENTION_DAYS', 90),

    // Request/response keys whose values are redacted before being stored.
    'redact' => [
        'authorization',
        'api_key',
        'apikey',
        'key',
        'secret',
        'token',
        'password',
    ],
],
```

Query the logs directly via their models:

```
use Laraditz\Courier\Models\CourierApiLog;
use Laraditz\Courier\Models\CourierWebhookLog;

CourierApiLog::forReference('ORDER-001')->get();
CourierApiLog::forDriver('sfexpress')->failed()->get();

CourierWebhookLog::forDriver('sfexpress')->processed()->get();
CourierWebhookLog::rejected()->get();
```

ModelScopesNotes`CourierApiLog``forReference`, `forDriver`, `successful`, `failed`One row per outbound HTTP call (request/response, duration, status)`CourierWebhookLog``forReference`, `forDriver`, `processed`, `rejected`, `failed`One row per inbound webhook (`status` is `processed`, `rejected`, or `failed`)A write failure while logging is caught and reported to the default log channel — it never breaks the underlying courier call or webhook request.

Prune logs older than `courier.logging.retention_days` (a no-op when it's `null`):

```
php artisan courier:prune-logs
```

Schedule it to run periodically in your app's console kernel or `routes/console.php`:

```
Schedule::command('courier:prune-logs')->daily();
```

### Testing

[](#testing)

Use `Courier::fake()` to mock courier calls in tests:

```
use Laraditz\Courier\Facades\Courier;

$fake = Courier::fake();

// Your code under test runs here...
$this->service->bookShipment($order);

$fake->assertShipmentCreated(1);
$fake->assertShipmentCreated(fn ($payload) => $payload->serviceCode === 'STANDARD');
$fake->assertTracked('SF1234567890');
$fake->assertCancelled('SF1234567890');
$fake->assertRatesFetched();
$fake->assertLabelFetched('SF1234567890');
$fake->assertNothingSent();

// getShipment() calls are recorded too — provide a preset response the same way as createShipment
$fake = Courier::fake(['getShipment' => new ShipmentResult('CUSTOM-002', 'pending', null)]);
```

Provide preset responses:

```
use Laraditz\Courier\DTOs\Results\ShipmentResult;

$fake = Courier::fake([
    'createShipment' => new ShipmentResult('CUSTOM-001', 'pending', null),
]);
```

Normalized Status Vocabulary
----------------------------

[](#normalized-status-vocabulary)

All drivers map their carrier-specific statuses to these values:

StatusMeaning`pending`Shipment created, not yet picked up`picked_up`Collected by courier`in_transit`Moving through the network`out_for_delivery`On the delivery vehicle`delivered`Successfully delivered`failed_delivery`Delivery attempt failed`returned`Returned to sender`cancelled`Shipment cancelled`unknown`Status not recognizedBuilding a Custom Driver
------------------------

[](#building-a-custom-driver)

Implement `Laraditz\Courier\Contracts\CourierDriver` and register it:

```
// In your ServiceProvider
$this->app->make('courier')->extend('mycarrier', function ($app, $config) {
    return new MyCarrierDriver($config);
});
```

To receive push notifications from the carrier, also implement `Laraditz\Courier\Contracts\HandlesWebhooks`. See the [Webhooks](#webhooks) section for details.

License
-------

[](#license)

MIT

###  Health Score

42

—

FairBetter than 88% of packages

Maintenance97

Actively maintained with recent releases

Popularity9

Limited adoption so far

Community10

Small or concentrated contributor base

Maturity46

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~9 days

Total

4

Last Release

19d ago

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/1203676?v=4)[Raditz Farhan](/maintainers/raditzfarhan)[@raditzfarhan](https://github.com/raditzfarhan)

---

Top Contributors

[![raditzfarhan](https://avatars.githubusercontent.com/u/1203676?v=4)](https://github.com/raditzfarhan "raditzfarhan (70 commits)")

###  Code Quality

TestsPHPUnit

### Embed Badge

![Health badge](/badges/laraditz-courier/health.svg)

```
[![Health](https://phpackages.com/badges/laraditz-courier/health.svg)](https://phpackages.com/packages/laraditz-courier)
```

###  Alternatives

[psalm/plugin-laravel

Psalm plugin for Laravel

3355.4M352](/packages/psalm-plugin-laravel)[laravel/ai

The official AI SDK for Laravel.

1.1k4.6M275](/packages/laravel-ai)[moonshine/moonshine

Laravel administration panel

1.3k268.2k86](/packages/moonshine-moonshine)[defstudio/telegraph

A laravel facade to interact with Telegram Bots

817336.8k3](/packages/defstudio-telegraph)[illuminate/pagination

The Illuminate Pagination package.

12234.6M1.1k](/packages/illuminate-pagination)[illuminate/pipeline

The Illuminate Pipeline package.

9350.2M302](/packages/illuminate-pipeline)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
