webit/dpd-client
webit/dpd-client is a PHP library that integrates your application with the DPD courier API, handling shipment creation, label generation, parcel tracking, and delivery service management so you can automate DPD shipping workflows from Laravel or any PHP project.
1. Install and inspect before you wire anything up.
composer require webit/dpd-client
The package is small (3 stars, last release May 2019) and predates Laravel's package auto-discovery era for many vendors. Assume there is no DpdServiceProvider and no published config until you've confirmed otherwise. Before writing a single line of application code, spend 15 minutes in the repo:
README.md — the only source of truth for credentials and entry-point class names.src/ — identify the top-level client/factory class and the per-feature service classes (typically package creation, label generation, pickup-point lookup, tracking).composer.json — check the declared PHP constraint. If it says ^7.x only, you'll need a fork or --ignore-platform-reqs plus a compatibility pass before running on PHP 8.2+.tests/ — if fixtures exist, they double as payload examples, which is worth more than the README.2. Get credentials and pick an environment first.
DPD Poland (this is a Polish DPD client) typically separates a sandbox from production and separates a master FID from per-contract FIDs. Get both, plus a dedicated test sender address, before your first call. Nothing in this package will fail helpfully if you pass the wrong FID — you'll get a server-side validation error at package-creation time.
3. First use case: pickup points, not labels.
The lowest-risk integration to prove the wiring is a pickup-point lookup (or a tracking call on a known waybill). It's read-only, has no side effects, and confirms credentials, environment, and transport in one shot. Label generation is the real goal but it creates billable, hard-to-undo shipments — don't make it your smoke test.
// Verify transport + auth before touching shipments
$client = new \Webit\Dpd\Client\Client($login, $password, $masterFid, $sandbox = true);
$points = $client->pickup()->findByPostalCode('00-001');
dump($points);
4. Abstract it immediately.
Wrap the package behind your own interface (App\Shipping\DpdGateway) on day one. Given the release date, the realistic medium-term plan is replacing or forking this package, and the difference between a two-hour swap and a two-week rewrite is whether Webit\... namespaces leaked into your controllers.
Bind the client as a singleton with config, not new at call sites.
// config/services.php
'dpd' => [
'login' => env('DPD_LOGIN'),
'password' => env('DPD_PASSWORD'),
'master_fid' => env('DPD_MASTER_FID'),
'sandbox' => env('DPD_SANDBOX', true),
],
// App\Providers\ShippingServiceProvider
$this->app->singleton(DpdGateway::class, fn ($app) => new DpdGateway(
$app['config']->get('dpd')
));
Then never reference Webit\ outside App\Shipping\Dpd. Your DpdGateway interface exposes domain verbs — createShipment(Order $order): Waybill, labelPdf(Waybill $w): string, points(Address $a): Collection — and one adapter implements it. When you fork the package, you write one new adapter.
Map domain objects to payloads explicitly.
Don't let Order or Address models flow into the client. A dedicated mapper makes DPD's unit and format expectations (weights in kg vs grams, dimensions in cm, phone format, ISO country codes, parcel count derived from item dimensions) visible and testable in one place:
final class OrderToDpdPackage
{
public function map(Order $order): array
{
return [
'reference' => $order->number, // keep it <= 20 chars; DPD truncates silently
'receiver' => $this->address($order->shippingAddress),
'parcels' => [[
'weight' => max(1, (int) ceil($order->totalWeightGrams() / 1000)),
'size' => $this->sizeCode($order), // often a template code, not raw dims
]],
'cod' => $order->isCashOnDelivery() ? $order->total : null,
'content' => $order->items->pluck('name')->take(3)->implode(', '),
];
}
}
Queue the mutations, keep the reads synchronous.
Label generation and shipment creation belong on a queue job with retries and a persisted result, because the round trip is slow and the failure modes are network-shaped. Tracking and pickup-point lookups are fine inline (but see caching below).
final class CreateDpdShipment implements ShouldQueue
{
public $tries = 3;
public $backoff = [10, 60, 300];
public function handle(DpdGateway $dpd): void
{
// Guard against re-processing: DPD will happily create a duplicate parcel
if ($this->order->dpd_waybill) {
return;
}
$waybill = $dpd->createShipment($this->order);
$this->order->update(['dpd_waybill' => $waybill->number]);
dispatch(new StoreDpdLabel($this->order, $waybill));
}
}
Persist every request and response.
The package almost certainly won't give you useful structured logs. Wrap the client in a decorator that dumps request/response pairs (redacting passwords) to a dpd_logs table or a dedicated channel, tagged with order ID. When DPD support asks "what did you send at 14:32", this is the difference between a five-minute answer and a lost afternoon.
Store label PDFs, don't regenerate them.
Labels are typically returned as base64 or raw binary in the response and are frequently re-downloadable for a limited window. Decode once, push to your media disk / S3, and store the path on the order. Shipment PDFs are also batch-printable — build a "print labels for today's shipments" screen early; ops will ask on day two.
Cache pickup points aggressively.
Point networks change on the order of weeks, not minutes. Cache per postal code for 24h with a tagged store, and refresh on a scheduled command. This also insulates checkout from a DPD outage.
The package is essentially unmaintained — plan for it. Last release 2019-05-29, zero dependents, three stars. Before committing:
ext-soap with WSDL caching — WSDL fetches over the network on every instantiation are a classic production latency bug. Cache the WSDL locally and point the client at the file."webit/dpd-client": "dev-master" under a repositories vcs entry pointing at your own fork) so you can patch without upstream cooperation. Don't ^-pin and hope.No auto-discovery means no auto-config. You will write the service provider, the config file, and the container binding yourself. Publish nothing; just bind. And don't resolve the client from the container in a model — inject it.
Retries on package creation can double-bill and double-ship. A timeout does not mean the parcel wasn't created. Always persist an idempotency signal before the call (a pending row with a client-side reference) and reconcile by querying the reference before retrying. This is the single most expensive mistake available in this domain.
Error handling is server-side and terse. DPD's validation errors come back as strings keyed by field or as a status code, not as typed exceptions. Normalize them in the adapter into something you can act on (DpdValidationException with fieldErrors), and log the raw response alongside. Never surface the raw message to a customer.
Units and codes are the top source of "why is my label wrong." Weight in kg (rounded, minimum 1), dimensions by template code rather than free numbers in most flows, country as ISO-3166 alpha-2, currency as ISO-4217, phone without the country prefix if the API expects it. Write unit tests on the mapper with realistic Polish addresses — ul., m., and postal format XX-XXX.
Credentials are not one-size-fits-all. Master FID creates/administers; contract FIDs generate parcels. Mixing them yields authorization errors that don't mention FIDs at all. Keep both in config with distinct names.
Test without the network. Because your DpdGateway interface is yours, bind a FakeDpdGateway in the test container and assert on the mapped payload. Only the adapter itself needs an integration test, and that should run against sandbox credentials gated behind a separate test suite / CI job that you can skip.
Extension point worth adding yourself: a DpdGateway::healthCheck() that does a cheap authenticated read. Wire it into your ops dashboard so a credential expiry or DPD outage is visible before your checkout is.
How can I help you explore Laravel packages today?