Open-source PHP client for the DPD Cloud Service Webservice (DPD Deutschland): shipment creation and labels, server-side and local validation, two tracking models, ParcelShop finder and pickup rules, over both SOAP and REST.
DPD is one of the largest parcel networks in Europe, and DPD Germany exposes its shipping platform through the DPD Cloud Service Webservice - the interface behind label printing, parcel tracking, ParcelShop lookups and pickup scheduling for German business accounts.
dpd-de-php is an open-source PHP client for that service. It covers all five operations over both transports DPD offers, SOAP and REST, behind one identical API; it validates orders locally against DPD's own error-code appendix before anything is sent; and it is typed to PHPStan level 8 from the credentials down to the tracking milestones.
The work that is easy to underestimate in a DPD integration is not the happy path - it is the two credential pairs, the two entirely different tracking models, the fields the WSDL calls optional and the API rejects as missing, and the sandbox credentials that are not where the official documentation says they are. All of that is handled, and documented, in the package.
Every operation of the DPD Cloud Service, each available over SOAP (the default, following the live WSDL) and over REST.
isParcelLocker() check.A booking returns the parcel number and the label PDF in the same response. Label size, start position and printer format are configurable per order through typed settings.
Classic, Predict, Return, Shop delivery and return, the whole Express family from 8:30 to 18:00 including the Saturday variants, Express International, and the discontinued COD products kept for completeness - all as one enum.
DPD ships two unrelated tracking services and neither replaces the other. Both are implemented: the structured one for state machines and stored milestones, the text-block one for rendering a customer-facing page.
Pickup points by address or coordinates, with the data needed to actually display them: distance, opening hours, holiday closures and per-shop services such as prepaid returns or cash payment.
Field lengths, weight ceilings and DPD's business rules are checked before the call, in characters rather than bytes - exactly as DPD counts them - so a 35-character umlaut string is not falsely rejected.
A constructor argument switches transport. The REST contract is not in DPD's PDF at all; it was reconstructed from the portal code samples, including the two URL rules that quietly produce a 404 if you get them wrong.
DPD enforces a per-account call limit with a ten-minute cool-down. That gets its own exception type with retryAfterSeconds(), deliberately not an auth error - the right answer is to back off, not to go looking for new credentials.
Typed DTOs, backed enums and a documented exception hierarchy from end to end, with a transport interface that makes the whole client testable without a single network call.
Every booking is checked locally against the constraints in the DPD documentation's error-code appendix, and each check names the DPD error it prevents. Field rules cover weight (0-31.5 kg), the 35-character limits on content and references, and the address rules DPD applies to company, name, salutation, street, house number, city, post code, phone, e-mail and state.
The business rules are the ones that cost an afternoon when you meet them at runtime:
Content, YourInternalID and Reference1 are mandatory in practice, although the WSDL marks them optionalUS to United StatesfindParcelShops()Local validation is a best-effort mirror, not a replacement for DPD's own. It cannot know whether an address exists or whether a product is available on a given lane - which is what checkOrderData() is for, and why both are in the library.
DPD Cloud Service authenticates with two credential pairs: partner credentials identifying the integration itself, and user credentials identifying the DPD customer account. Both are issued per environment, and a production pair used against the test system fails exactly like an invalid one.
Getting sandbox access is the part where the official documentation actively misleads. The portal page that both the PDF and DPD support point at issues credentials for a different API - DPD Web Connect - whose format is not convertible to Cloud credentials. The real Cloud sandbox account is pre-filled inside the portal's code samples, on a page that is unreachable through normal navigation without a specific query parameter. The library's README documents the route, so nobody has to spend that day twice.
composer require very-code-com/dpd-de-php
$client = DpdCloudClient::sandbox('DPD Sandbox', $partnerToken, $userId, $userToken);
$result = $client->createShipment(new OrderItem(
shipAddress: new Address(
name: 'Max Mustermann', street: 'Musterstr.', houseNo: '1',
zipCode: '12345', city: 'Berlin', country: 'DE',
),
parcelShopId: 0,
parcel: new Parcel(
ShipService::Classic,
weightKg: 2.5,
content: 'Testware',
yourInternalId: 'ORDER-1',
reference1: 'ORDER-1',
),
));
echo $result->firstParcelNo(); // 01234567890123
file_put_contents('label.pdf', $result->labelPdf); // already decoded
Configuration can equally come from environment variables or a framework config array, and the client accepts a custom transport and a PSR-3 logger for dependency injection and testing.
Six exception types map to six different reactions: local validation, rejected credentials, the rate limit with its own retry hint, a DPD business error carrying structured error IDs and codes, a transport failure and an unparseable response. Every one exposes the raw response and a full debug report when debug mode is on.
Continuous integration runs the unit suite on PHP 8.2, 8.3 and 8.4 with PHPStan level 8 on every push and lints every example. A separate nightly workflow exercises all five operations over both transports against DPD's live test system, including a real booking that issues a label. It is kept out of the pull-request gate on purpose: DPD's per-account call limit would be hit within one matrix run.
One DPD limitation worth knowing before you design around it: there is no operation for re-downloading a label by parcel number. The PDF returned at booking time is the only copy the API will ever give you, so store it. The library documents this rather than pretending a workaround exists.
PHP teams shipping parcels through DPD Germany - an online shop printing labels at pack time, an ERP booking pickups in bulk, a marketplace showing customers where their parcel is, or a returns portal handing out ParcelShop drop-off points. Instead of a hand-rolled SOAP envelope and a folder of trial-and-error notes, the integration becomes a typed Composer package with its carrier quirks written down.