Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

70 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Laravel Courier

Latest Version on Packagist Total Downloads License

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

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

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

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

Package Carrier
laraditz/courier-sfexpress SF Express
laraditz/courier-lalamove Lalamove
laraditz/courier-jt-express J&T Express (MY)

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

Method Parameters Returns Description
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&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

DTO Key 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

DTO Properties
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

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

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

$result->waybillNumber; // 'MYIU1234715622'

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

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

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

// 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

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

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

Every outbound API call made through CourierHttpClient and every inbound webhook request is recorded (see 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();
Model Scopes Notes
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

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

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

Status Meaning
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 recognized

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 section for details.

License

MIT

About

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

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages