dinas / shipping-sdk-laravel
Laravel client for Dinas Shipping API
Requires
- php: ^8.2
- dinas/shipping-sdk-php: ^1.1
- illuminate/contracts: ^11.0||^12.0
- php-http/guzzle7-adapter: ^1.1
- spatie/laravel-package-tools: ^1.16
- spatie/laravel-webhook-client: ^3.4
Requires (Dev)
- larastan/larastan: ^3.0
- laravel/pint: ^1.14
- nunomaduro/collision: ^8.8
- orchestra/testbench: ^10.0.0||^9.0.0
- pestphp/pest: ^3.0
- pestphp/pest-plugin-arch: ^3.0
- pestphp/pest-plugin-laravel: ^3.0
- phpstan/extension-installer: ^1.4
- phpstan/phpstan-deprecation-rules: ^2.0
- phpstan/phpstan-phpunit: ^2.0
README
This package is two-way bridge between Laravel applications and the Dinas Shipping API. You can send data to REST endpoints and handle incoming webhooks with ease.
Incoming events are automatically verified, logged, and dispatched as Laravel jobs, enabling smooth asynchronous updates such as shipment status changes or document availability.
You can find the full documentation here.
Installation
You can install the package via composer:
composer require dinas/shipping-sdk-laravel
The service provider will automatically register itself.
Add the following environment variables to your .env file:
DINAS_SHIPPING_TOKEN=your-api-token-here DINAS_SHIPPING_SECRET=your-random-webhook-secret-here
You can obtain your API token from the dashboard https://shipping.dinas.jp/settings/tokens.
Webhook Setup
First, add the webhook route to your api or web route file:
Route::dinasShippingWebhooks('dinas-shipping/webhook');
You can customize the endpoint path as needed, it will be automatically discovered on setup command.
Behind the scenes, by default this will register a POST route to a controller provided by this package.
Because the app that sends webhooks to you has no way of getting a csrf-token for web, you must exclude the route from csrf token validation.
Here is how you can do that in recent versions of Laravel.
use Illuminate\Foundation\Application; use Illuminate\Foundation\Configuration\Middleware; return Application::configure(basePath: dirname(__DIR__)) ->withRouting( // ... ) ->withMiddleware(function (Middleware $middleware) { $middleware->validateCsrfTokens(except: [ 'dinas-shipping/webhook' ]); })->create();
Next, publish the webhook migration and run it:
php artisan vendor:publish --provider="Spatie\WebhookClient\WebhookClientServiceProvider" --tag="webhook-client-migrations" php artisan vendor:publish --tag="shipping-sdk-laravel-migrations" php artisan migrate
Now, register the webhook with Dinas Shipping API by running:
php artisan webhook:dinas-shipping -i
You can check status of all webhooks with:
php artisan webhook:dinas-shipping
To remove the webhook registration, run:
php artisan webhook:dinas-shipping -r
More information is accessible with:
php artisan webhook:dinas-shipping --help
You can publish the config files with:
php artisan vendor:publish --tag="shipping-sdk-laravel-config"
Usage
Cars API
use Dinas\Shipping\Facades\Shipping; // Get cars with filters $cars = Shipping::getCars([ 'status' => \Dinas\ShippingSdk\Model\StockStatus::PENDING, 'search' => 'Toyota', 'port_code' => 'OSA', 'voyage' => 'VES0001PORT', 'vehicle_state' => 'dis', // whole, unknown, dmg 'vehicle_type' => \Dinas\ShippingSdk\Model\VehicleType::TRACTOR, 'photos' => true, // Only cars with photos 'docs' => true, // Only cars with documents 'on_yard' => true, // Only cars on yard 'price_terms' => \Dinas\ShippingSdk\Model\PriceTerms::FOB, 'sort' => '-id', 'per_page' => 100, 'page' => 1, ]); // Sync cars (create or update) $result = Shipping::syncCars([ [ 'chassis' => 'ABC123', 'make' => 'Toyota', 'model' => 'Camry', 'year' => 2020, 'color' => 'White', // ... other car fields ], [ 'chassis' => 'DEF456', 'make' => 'Honda', 'model' => 'Civic', // ... other car fields ], ]); // Hold cars from shipping Shipping::holdCars(['ABC123', 'DEF456'], [ 'date' => '2026-03-15', 'after' => true, // true = after date, false = before date ]); // Hold without date limit Shipping::holdCars(['ABC123', 'DEF456']); // Release cars for shipping Shipping::releaseCars(['ABC123', 'DEF456']); // Withhold cars upon arrival Shipping::withholdCars(['ABC123'], 'Payment pending'); // Withhold without reason Shipping::withholdCars(['ABC123']); // Grant cars (clear withhold status) Shipping::grantCars(['ABC123']); // Set yard ETA for cars Shipping::setYardEta([ ['chassis' => 'ABC123', 'eta' => '2026-02-15'], ['chassis' => 'DEF456', 'eta' => '2026-02-16'], ]);
Photos API
use Dinas\Shipping\Facades\Shipping; // Get car photos with filters $photos = Shipping::getCarPhotos([ 'chassis' => 'ABC123', 'voyage' => 'VES0001PORT', 'status' => 'pending', 'photos' => true, // false - for list without photo uploaded yet 'per_page' => 100, ]); // Store car photos from URLs // Data is automatically chunked (default: 50 items per chunk, configurable). // Returns a StoreResult with structured errors and job IDs. $result = Shipping::storeCarPhotos([ [ 'chassis' => 'ABC123', 'album' => \Dinas\ShippingSdk\Model\AlbumType::YARD_CARGO, 'urls' => [ 'https://example.com/photo1.jpg', 'https://example.com/photo2.jpg', ], ], [ 'chassis' => 'DEF456', 'album' => 'auction', 'urls' => [ 'https://example.com/photo3.jpg', ], ], ]); // Check results if (!$result->ok) { // Validation errors (422) as structured arrays foreach ($result->validationErrors as $field => $messages) { // e.g. ['0.chassis' => ['The chassis field is required.']] } } // API-level errors (e.g. "Car not found") as arrays foreach ($result->errors as $error) { echo $error['chassis'] . ': ' . $error['error']; } // All error messages as a flat array of strings $messages = $result->allErrorMessages(); // Job IDs returned from the API $jobIds = $result->jobIds; // Store car photos from files $result = Shipping::storeCarPhotoFiles([ [ 'chassis' => 'ABC123', 'album' => \Dinas\ShippingSdk\Model\AlbumType::YARD_NOTE, 'files' => [ // File objects or paths ], ], ]);
Documents API
use Dinas\Shipping\Facades\Shipping; // Store car documents from URLs $result = Shipping::storeCarDocuments([ [ 'chassis' => 'ABC123', 'type' => \Dinas\ShippingSdk\Model\DocumentType::EXPORT_CERTIFICATE, 'url' => 'https://example.com/cert-tohon.pdf', 'valid_until' => '2027-01-30', // optional, but recommended for better tracking of document validity ], [ 'chassis' => 'ABC123', 'type' => \Dinas\ShippingSdk\Model\DocumentType::VEHICLE_INVOICE, 'url' => 'https://example.com/invoice.pdf', ], [ 'chassis' => 'DEF456', 'type' => \Dinas\ShippingSdk\Model\DocumentType::EXPORT_CERTIFICATE, 'url' => 'https://example.com/title.pdf', ], ]); // Store car documents from files $result = Shipping::storeCarDocumentFiles([ [ 'chassis' => 'ABC123', 'type' => \Dinas\ShippingSdk\Model\DocumentType::EXPORT_CERTIFICATE, 'file' => $uploadedFile, 'valid_until' => '2027-01-30', ], ]);
Async Job Callbacks (onResolve)
When the API processes your photos/documents asynchronously, it returns a jobId and later sends a webhook
(api.job event) when the job is complete. You can register a callback that will be automatically executed
when that webhook arrives:
use Dinas\Shipping\Facades\Shipping; use Dinas\Shipping\DTOs\WebhookJobContext; $result = Shipping::storeCarDocuments($documents, onResolve: function (WebhookJobContext $context) { // This runs automatically when the API job finishes and the webhook is received. // $context->jobId — API job ID // $context->userId — User who initiated the request // $context->method — 'storeCarDocuments' // $context->status — 'finished' or 'failed' // $context->message — Optional message from API (can be null) // $context->errors — Errors from the initial API response if ($context->isFailed()) { Log::warning("Job {$context->jobId} failed: {$context->message}"); } // Notify user, update records, etc. }); // You can also use any callable: $result = Shipping::storeCarPhotos($photos, onResolve: [MyService::class, 'handleResult']); $result = Shipping::storeCarPhotos($photos, onResolve: 'my_handler_function');
The callback is serialized and stored in the webhook_jobs table. When the webhook arrives,
the callback is deserialized and executed exactly once (duplicate webhooks are ignored via status tracking).
The user_id of the authenticated user at the time of the call is captured and passed to the callback context.
Broadcasting (Pusher)
When a webhook resolves a job, a ShippingJobResolved event will be broadcast on a private user's channel
to notify the frontend in real-time. This is opt-out.
Disable in your .env:
DINAS_SHIPPING_BROADCASTING=false
Or in config/dinas-shipping-sdk.php:
'broadcasting' => [ 'enabled' => true, ],
Listen on the frontend (e.g. Laravel Echo + Pusher):
Echo.private(`App.Models.User.${userId}`) .listen('.shipping.job.resolved', (e) => { console.log(e.jobId, e.method, e.status, e.message, e.errors); });
The broadcast payload contains: jobId, method, status, message, errors.
Deleting models
Whenever you call async method, this package will store callback as a WebhookJob model. After a while, you might want
to delete old models.
The WebhookJob model has Laravel's MassPrunable trait
applied on it. You can customize the cutoff date in the config file.
In this example all models will be deleted when older than 30 days.
return [ 'webhook_jobs' => [ 'delete_after_days' => 30, ], ];
After configuring the model, you should schedule the model:prune Artisan command in your
application's routes/console.php. Don't forget to explicitly mention the WebhookJob class.
You are free to choose the appropriate interval at which this command should be run:
use Illuminate\Support\Facades\Schedule; use Dinas\Shipping\Models\WebhookJob; use Spatie\WebhookClient\Models\WebhookCall; Schedule::command('model:prune', [ '--model' => [WebhookJob::class, WebhookCall::class], ])->daily(); // This will not work, as models in a package are not used by default // Schedule::command('model:prune')->daily();
Voyages API
use Dinas\Shipping\Facades\Shipping; // Get all voyages $voyages = Shipping::getVoyages([ 'per_page' => 25, 'page' => 1, ]); // Get a specific voyage $voyage = Shipping::getVoyage(123);
Direct API Access
For full control, you can access the underlying API instances directly:
use Dinas\Shipping\Facades\Shipping; // Access Cars API directly $carsApi = Shipping::cars(); $result = $carsApi->getCars(status: 'pending', perPage: 100); // Access Car Photos API directly $photosApi = Shipping::carPhotos(); $photos = $photosApi->getCarPhotos(chassis: 'ABC123'); // Access Car Documents API directly $docsApi = Shipping::carDocuments(); $docsApi->storeCarDocumentUrls($documentData); // Access Voyages API directly $voyagesApi = Shipping::voyages(); $voyages = $voyagesApi->getVoyages(); // Access Webhooks API directly $webhooksApi = Shipping::webhooks(); $webhooks = $webhooksApi->getWebhooks(); // Get the SDK configuration $config = Shipping::getConfiguration(); // Set a custom HTTP client Shipping::setHttpClient($customClient);
Dependency Injection
You can also inject the Shipping class directly:
use Dinas\Shipping\Shipping; class CarController extends Controller { public function __construct( protected Shipping $shipping ) {} public function index() { return $this->shipping->getCars(['status' => 'pending']); } }
Handling webhook requests using jobs
If you want to do something when a specific event type comes in you can define a job that does the work. Here's an example of such a job:
<?php namespace App\Jobs\DinasWebhooks; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; use Spatie\WebhookClient\Models\WebhookCall; class TestJob implements ShouldQueue { use InteractsWithQueue, Queueable, SerializesModels; public function __construct(public WebhookCall $webhookCall) { } public function handle(): void { $payload = $this->webhookCall->payload; // do your work here } }
We highly recommend that you make this job queueable, because this will minimize the response time of the webhook requests. This allows you to handle more webhook requests and avoid timeouts.
After having created your job you must register it at the jobs array in the dinas-shipping-sdk.php config file.
// config/dinas-shipping-sdk.php 'jobs' => [ 'car.updated' => \App\Jobs\DinasWebhooks\HandleCarUpdated::class, ],
You can find the full list of events types in the documentation.
In case you want to configure one job as default to process all undefined event, you may set the job at default_job in
the dinas-shipping-sdk.php config file. The value should be the fully qualified classname.
Advanced usage
Retry handling a webhook
All incoming webhook requests are written to the database. This is incredibly valuable when something goes wrong while handling a webhook call. You can easily retry processing the webhook call, after you've investigated and fixed the cause of failure, like this:
use Spatie\WebhookClient\Models\WebhookCall; use Spatie\WebhookClient\Jobs\ProcessWebhookJob; dispatch(new ProcessWebhookJob(WebhookCall::find($id)));
More Information
For more information on how to use the underlying SDK, please refer to the spatie webhook client
Testing
composer test
Changelog
Please see CHANGELOG for more information on what has changed recently.
Contributing
Please see CONTRIBUTING for details.
Security Vulnerabilities
Please review our security policy on how to report security vulnerabilities.
Credits
License
The MIT License (MIT). Please see License File for more information.