amdadulhaq/bd-courier-laravel

Create shipments and track parcels from Laravel via popular Bangladeshi courier services (Pathao, Steadfast, RedX, eCourier, Paperfly, Sundarban, SA Paribahan, Karatoa), behind one driver-based API.

Maintainers

Package info

github.com/amdad121/bd-courier-laravel

pkg:composer/amdadulhaq/bd-courier-laravel

Transparency log

Fund package maintenance!

amdad121

Statistics

Installs: 1

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.0.0 2026-08-23 09:16 UTC

This package is auto-updated.

Last update: 2026-08-23 09:19:16 UTC


README

Latest Version on Packagist GitHub Tests Action Status GitHub Code Style Action Status Total Downloads PHP Version Laravel Version Sponsor

Create shipments and track parcels from Laravel via popular Bangladeshi courier services (Pathao, Steadfast, RedX, eCourier, Paperfly, Sundarban, SA Paribahan, Karatoa), all behind one driver-based API.

Contents

Requirements

  • PHP 8.2, 8.3, 8.4, or 8.5
  • Laravel 11, 12, or 13

Installation

composer require amdadulhaq/bd-courier-laravel

The service provider and Courier facade are auto-discovered. Publish the config file:

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

Configuration

Set the default driver and credentials in your .env:

COURIER_DRIVER=steadfast

STEADFAST_API_KEY=your-api-key
STEADFAST_SECRET_KEY=your-secret-key

steadfast has the simplest signup of the bunch, so it's the default — start there if you don't have merchant credentials for anything else yet.

Or, for Pathao:

COURIER_DRIVER=pathao

PATHAO_CLIENT_ID=your-client-id
PATHAO_CLIENT_SECRET=your-client-secret
PATHAO_USERNAME=your-username
PATHAO_PASSWORD=your-password
PATHAO_STORE_ID=your-store-id

Or, for RedX:

COURIER_DRIVER=redx

REDX_API_TOKEN=your-api-token
REDX_PICKUP_STORE_ID=your-pickup-store-id

Or, for eCourier:

COURIER_DRIVER=ecourier

ECOURIER_USER_ID=your-user-id
ECOURIER_API_KEY=your-api-key

Or, for Paperfly:

COURIER_DRIVER=paperfly

PAPERFLY_MERCHANT_ID=your-merchant-id
PAPERFLY_USERNAME=your-username
PAPERFLY_PASSWORD=your-password

Or, for Sundarban:

COURIER_DRIVER=sundarban

SUNDARBAN_API_KEY=your-api-key
SUNDARBAN_BOOKING_USER_ID=your-booking-user-id

Or, for SA Paribahan:

COURIER_DRIVER=sa_paribahan

SA_PARIBAHAN_API_KEY=your-api-key
SA_PARIBAHAN_BOOKING_BRANCH=your-booking-branch

Or, for Karatoa:

COURIER_DRIVER=karatoa

KARATOA_API_TOKEN=your-api-token
KARATOA_MERCHANT_ID=your-merchant-id

See config/courier.php for every driver's options. COURIER_DRIVER defaults to steadfast.

Which driver do I need?

Driver Cancel shipment? Price calculator? Server-to-server webhook? Notes
pathao No Yes No (poll/track instead) Requires an issued OAuth token; auto-renewed and cached.
steadfast No No No (poll/track instead) Simplest signup, popular with small merchants.
redx Yes Yes No (poll/track instead)
ecourier No No No (poll/track instead)
paperfly No No No (poll/track instead)
sundarban No No No (poll/track instead)
sa_paribahan No No No (poll/track instead)
karatoa Yes No No (poll/track instead)

Every driver throws AmdadulHaq\BdCourier\Exceptions\CourierException for operations it doesn't support, so calling code can rely on the same contract regardless of which courier is active.

Booking a shipment

use AmdadulHaq\BdCourier\Facades\Courier;
use AmdadulHaq\BdCourier\DataTransferObjects\ShipmentRequest;

$response = Courier::createShipment(new ShipmentRequest(
    invoiceNumber: 'INV-1001',
    recipientName: 'John Doe',
    recipientPhone: '01700000000',
    recipientAddress: 'House 1, Road 2, Gulshan',
    recipientCity: 'Dhaka',
    recipientZone: 'Gulshan',
    codAmount: 1200.00,
    itemWeight: 0.5,
    itemDescription: 'T-Shirt',
));

$response->consignmentId; // the courier's own tracking/consignment ID — save this
$response->status;        // AmdadulHaq\BdCourier\Enums\ShipmentStatus

Track a shipment any time without mutating anything:

$status = Courier::track($response->consignmentId);

if ($status->status->isSuccessful()) {
    // mark the order delivered
}

Cancel a shipment (not every courier/driver supports this):

Courier::cancel($response->consignmentId);

Estimate the delivery fee before booking (not every courier/driver supports this):

$fee = Courier::calculatePrice($request);

Use a specific driver, or a driver other than the default, for one call — same as Storage::disk():

Courier::driver('pathao')->createShipment($request);

Handling webhooks

Some couriers notify your server directly with status updates instead of (or in addition to) you polling track(). Point that URL at a route in your app and hand the request straight to the driver:

use AmdadulHaq\BdCourier\Facades\Courier;

Route::post('/webhooks/redx', function (Request $request) {
    $result = Courier::driver('redx')->handleWebhook($request->all());

    if ($result->status->isSuccessful()) {
        // mark the order delivered
    }

    return response()->noContent();
})->withoutMiddleware([\Illuminate\Foundation\Http\Middleware\VerifyCsrfToken::class]);

None of the couriers supported out of the box sign their webhook payload, so every driver treats the incoming payload as a hint at most: it extracts the consignment/tracking ID and calls the courier's own track() endpoint to get the real, authoritative status rather than trusting the payload directly. Drivers with no webhook mechanism at all throw AmdadulHaq\BdCourier\Exceptions\CourierException.

API reference

ShipmentRequest

Everything a driver might need to book a shipment — pass what applies, ignore the rest (e.g. drivers without a price calculator ignore fields they don't read).

Property Type Notes
invoiceNumber string Required. Your own unique reference — becomes merchant_order_id (Pathao), invoice (Steadfast), merchant_invoice_id (RedX), etc.
recipientName string Required.
recipientPhone string Required.
recipientAddress string Required. Full delivery address.
recipientCity, recipientZone, recipientArea string District/thana/area — required by drivers that route by zone (Pathao, RedX). Defaults to '' for drivers that parse the full address instead.
codAmount float Cash-on-delivery amount to collect. Defaults to 0.0 for prepaid parcels.
itemWeight float In kilograms. Defaults to 0.5.
itemDescription string Defaults to ''.
itemQuantity int Defaults to 1.
specialInstruction ?string Delivery notes passed through to the courier where supported.
metadata array<string, mixed> Driver-specific extras not covered above.

ShipmentResponse

What every driver method returns, so calling code never branches on which courier answered.

Property Type Meaning
status ShipmentStatus Normalized status — see below.
invoiceNumber string Echoes back your invoice/reference.
consignmentId ?string The courier's own tracking/consignment ID — save this, you'll need it for track()/cancel().
trackingUrl ?string A public tracking link/code, when the courier returns one.
deliveryFee ?float The confirmed or quoted delivery charge, when the courier reports one.
message ?string Human-readable status text from the courier.
raw array<string, mixed> The untouched courier response — keep this for auditing/debugging, don't build logic on it directly.

ShipmentStatus

Case Meaning
Pending Booked, not yet picked up.
PickupRequested Pickup scheduled with the rider/hub.
PickedUp Rider has collected the parcel.
InTransit Parcel is moving between hubs / out for delivery.
Delivered Delivered and (if COD) collected — $status->isSuccessful() is true.
PartialDelivered Only part of the order was accepted/delivered.
Returned Parcel came back to the merchant.
Cancelled Shipment was cancelled before/after booking.
Failed Delivery attempt failed, or the courier reported an unrecognized status.

$status->isFinal() is true for every case except Pending, PickupRequested, PickedUp, and InTransit.

Adding your own courier

Register a custom driver:

use AmdadulHaq\BdCourier\Facades\Courier;

Courier::extend('my-courier', function ($app) {
    return new MyCourierDriver(/* ... */);
});

Any driver just needs to implement AmdadulHaq\BdCourier\Contracts\CourierDriver:

interface CourierDriver
{
    public function createShipment(ShipmentRequest $request): ShipmentResponse;

    public function track(string $consignmentId): ShipmentResponse;

    public function cancel(string $consignmentId): ShipmentResponse;

    public function calculatePrice(ShipmentRequest $request): float;

    public function handleWebhook(array $payload): ShipmentResponse;
}

Testing

Use CourierManager::fake() to swap the real courier with an in-memory fake and assert on what would have been booked, without dispatching anything or hitting the network:

use AmdadulHaq\BdCourier\CourierManager;
use AmdadulHaq\BdCourier\Facades\Courier;

$fake = CourierManager::fake();

// ... code under test that calls Courier::createShipment() ...

$fake->assertShipmentCreated('INV-1001');
$fake->assertNothingCreated();

Troubleshooting

Pathao: "Failed to issue an access token." Double-check PATHAO_USERNAME/PATHAO_PASSWORD (your Pathao Merchant Panel credentials, not a courier account PIN) and that PATHAO_BASE_URL matches your environment — the sandbox and production base URLs are different hosts, not just different credentials.

Steadfast / RedX / others: createShipment() throws a gateway error with no useful message Most of these couriers return a generic HTTP 4xx with the real reason buried in the JSON body. Catch AmdadulHaq\BdCourier\Exceptions\CourierException and inspect $exception->context — it holds the courier's raw decoded response.

A webhook route never fires handleWebhook() Make sure the route excludes Laravel's CSRF middleware (none of these couriers can obtain a CSRF token) and that you're passing the entire raw POST payload — $request->all() — since each driver looks for a specific ID field (consignment_id, tracking_id, tracking_number, etc.) that varies by courier.

"Nothing happens" in local development COURIER_DRIVER defaults to steadfast. If you're expecting a different courier, confirm .env actually sets COURIER_DRIVER and that you ran php artisan config:clear after changing it (cached config wins over .env).

Running the package's own test suite

composer install
composer test          # Pest
composer analyse        # Larastan
composer lint:check    # Pint

License

MIT. See LICENSE.md.