very-code_
← Back to homepage

DPD Germany Logistics API Integration

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.

PHP 8.2+ SOAP + REST DPD Cloud Service Logistics PHPStan level 8 Packagist Open Source

What is dpd-de-php?

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.

What the API covers

Every operation of the DPD Cloud Service, each available over SOAP (the default, following the live WSDL) and over REST.

createShipment()
createShipments()
Books one parcel, or up to 30 in a single call, and returns the parcel numbers together with the label PDF - already Base64-decoded and ready to write to disk.
checkOrderData()
A server-side dry run: DPD validates the exact order data without creating a shipment or consuming a parcel number.
validateLocally()
The same field and business-rule checks the client runs before every booking, exposed on demand with no network call at all.
fetchOrderStatus()
Structured tracking (Parcel Life Cycle 3.1): order information, ship address, the latest status and five named milestones - start, on the road, delivery depot, car load, delivered.
fetchParcelLifeCycle()
The older UI-oriented tracking model (Parcel Life Cycle 2.0): pre-formatted text blocks with bold and paragraph flags, meant to be rendered straight into a tracking page.
findParcelShops()
ParcelShop and parcel-locker search by address or by geo-coordinates, returning opening hours, holidays, distance, offered services and a isParcelLocker() check.
fetchZipCodeRules()
Pickup rules for your own account address: no-pickup days as parsed dates, Express and Classic cut-off times, pickup depot and state.
lastSystemInformation()
DPD's free-text service announcements - planned maintenance, upcoming API changes - carried on every response and logged at notice level.

What it does

Shipments and labels

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.

Eighteen shipping products

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.

Two tracking models

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.

ParcelShop finder

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.

Local pre-flight validation

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.

SOAP or REST, one API

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.

Rate limiting as its own error

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.

PHPStan level 8

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.

Validation that mirrors DPD

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 optional
  • State is required for the USA and Canada and forbidden everywhere else - matched across every spelling DPD accepts, from US to United States
  • Predict products need an e-mail address or a phone number for the delivery notification
  • Classic Return needs a phone number and refuses to be batched with other items
  • Shop delivery needs a real ParcelShop ID - look one up with findParcelShops()
  • the Express 8:30-18:00 products are Germany-domestic only; abroad the product is Express International
  • a batch may hold at most 30 orders, and DPD Cloud Service has no multi-parcel shipments: each physical package is its own order item

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.

Credentials, environments and the sandbox

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.

Built to stay working

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.

Who is this for?

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.