PHP client library for the DPD Cloud Service Webservice (DPD Deutschland).
An open-source PHP client for DPD Germany's Cloud Service - create shipments (with PDF/ZPL labels), pre-flight validate orders server-side, track parcels via two different tracking models, find pickup ParcelShops, and fetch account pickup rules. SOAP by default (fully verified against the live WSDL), with an optional REST transport.
- PHP 8.2+
ext-curlext-domext-json
composer require very-code-com/dpd-de-phpuse VeryCodeCom\DpdDe\DpdCloudClient;
use VeryCodeCom\DpdDe\Dto\{Address, OrderItem, Parcel};
use VeryCodeCom\DpdDe\Enum\ShipService;
$client = DpdCloudClient::sandbox('DPD Cloud Service Alpha2', 'partner-token', 123456, 'user-token');
$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, yourInternalId: 'ORDER-1'),
));
echo $result->firstParcelNo(); // e.g. 01234567890123
file_put_contents('label.pdf', $result->labelPdf); // already Base64-decodedSee examples/ for complete, runnable scripts.
// Named constructors
$client = DpdCloudClient::sandbox($partnerName, $partnerToken, $userId, $userToken);
$client = DpdCloudClient::production($partnerName, $partnerToken, $userId, $userToken);
// From environment variables (recommended)
$config = DpdCloudConfig::fromEnv();
$client = new DpdCloudClient($config);
// From array (framework config)
$config = DpdCloudConfig::fromArray([
'partner_name' => '...', 'partner_token' => '...',
'user_id' => 123456, 'user_token' => '...', 'env' => 'production',
]);DPD Cloud Service uses two credential pairs, both issued by DPD:
- PartnerCredentials (
Name+Token) - identifies the software/integration (your "Partner" slot, e.g.DPD Cloud Service Alpha2). - UserCredentials (
cloudUserID+Token) - identifies the DPD customer account (your own account).
| Env variable | Required | Default | Description |
|---|---|---|---|
DPD_CLOUD_PARTNER_NAME |
yes | - | PartnerCredentials.Name |
DPD_CLOUD_PARTNER_TOKEN |
yes | - | PartnerCredentials.Token |
DPD_CLOUD_USER_ID |
yes | - | UserCredentials.cloudUserID (integer) |
DPD_CLOUD_USER_TOKEN |
yes | - | UserCredentials.Token |
DPD_CLOUD_ENV |
no | production |
sandbox or production |
DPD_CLOUD_LANGUAGE |
no | de_DE |
Response language, e.g. de_DE, en_US |
DPD_CLOUD_TIMEOUT |
no | 30 |
Request timeout (seconds) |
DPD_CLOUD_CONNECT_TIMEOUT |
no | 10 |
Connection timeout (seconds) |
DPD_CLOUD_DEBUG |
no | 0 |
1/true to enable verbose debug output (see below) |
Sandbox vs. production endpoints:
| Environment | SOAP endpoint | REST base |
|---|---|---|
| Sandbox (Testsystem) | https://cloud-stage.dpd.com/services/v1/DPDCloudService.asmx |
https://cloud-stage.dpd.com/api/v1 |
| Production | https://cloud.dpd.com/services/v1/DPDCloudService.asmx |
https://cloud.dpd.com/api/v1 |
DPD issues separate credentials per environment - your production PartnerCredentials/UserCredentials will not work against the sandbox and vice versa.
Set the debug flag (constructor arg, DPD_CLOUD_DEBUG=1, or 'debug' => true in
fromArray) to make the client attach the raw DPD response to every thrown
exception and log a full debug report (message + raw XML/JSON + stack trace) at
error level via the injected PSR-3 logger:
$config = DpdCloudConfig::fromArray([...], debug: true);
$client = new DpdCloudClient($config, logger: $myPsrLogger);
try {
$client->createShipment($item);
} catch (\VeryCodeCom\DpdDe\Exception\DpdCloudException $e) {
echo $e->getRawResponse(); // exact XML/JSON DPD returned (or null)
echo $e->getDebugReport(); // class + message + raw response + stack trace
}Leave debug off in production to keep exceptions and logs concise.
Creates one shipment, or up to 30 in a single call (DPD method: setOrder,
OrderAction=startOrder). Runs local pre-flight validation first (see below).
Note: DPD Cloud Service does not support multi-parcel shipments (MPS) - each physical package needs its own
OrderItemwith its ownshipAddress.
SetOrderResult exposes labelPdf (already Base64-decoded document bytes - PDF, or ZPL
text when LabelSize::ZplA6 is requested) and items (one OrderResult per parcel,
yourInternalId + parcelNo); firstParcelNo() is a convenience shortcut.
OrderItem fields:
| Field | Type | Required | Notes |
|---|---|---|---|
shipAddress |
Address |
yes | Recipient address |
parcelShopId |
int |
yes | Pickup point ID; 0 for classic home delivery |
parcel |
Parcel |
yes | Shipping product, weight, references |
pudoId |
?string |
no | Newer PUDO/locker identifier (undocumented in the PDF, present in the live WSDL) |
Parcel fields: shipService (ShipService enum), weightKg (0-31.5), content,
yourInternalId, reference1, reference2 (all max 35 chars), and a deprecated cod
(DPD discontinued cash-on-delivery on 2020-05-11).
Server-side dry run (DPD method: setOrder, OrderAction=checkOrderData) - asks DPD to
validate the order data without creating a real shipment or consuming a parcel number.
Throws DpdCloudApiException if DPD rejects the data.
Runs the same local field/weight checks createShipment() performs, with no network
call. Returns an empty array when the items are locally valid.
Tracking via the older, UI-rendering-oriented "Parcel Life Cycle Service 2.0" (DPD
method: getParcelLifeCycle). Returns a TrackingResult with a shipmentInfo header, a
statusInfo list (one per milestone) and a contactInfo list - each built from
pre-formatted ContentLine/ContentItem text blocks (bold/paragraph flags included) meant
for direct UI rendering. Always uses SOAP, regardless of the configured transport mode
(see REST caveats).
Tracking via the newer, structured "Parcel Life Cycle Service 3.1" (DPD method:
getOrderStatus). Returns an OrderStatus with orderInformation (service/weight/
reference/receiver), shipAddress, lastStatusInfo and a statusInfoContainer with five
named milestones (start, onTheRoad, deliveryDepot, carLoad, delivered). Provide
deliveryZipCode to receive full (non-anonymised) tracking data, per DPD's privacy rules.
Searches for DPD ParcelShop pickup points (DPD method: getParcelShopFinder), either by
address or by geo-coordinates:
use VeryCodeCom\DpdDe\Dto\{ParcelShopQuery, SearchAddress, SearchGeoData};
$shops = $client->findParcelShops(
ParcelShopQuery::byAddress(new SearchAddress(zipCode: '10115', city: 'Berlin', country: 'DE'))
);
$shops = $client->findParcelShops(
ParcelShopQuery::byGeoData(new SearchGeoData(longitude: 13.405, latitude: 52.52))
);Each ParcelShop carries shopAddress, geoData (distance + coordinates),
openingHoursList, holidayList, shopServiceList (ShopService[]), and
isParcelLocker(): bool.
Fetches pickup rules for your own account's pickup address (DPD method:
getZipCodeRules; no parameters needed) - no-pickup days, Express/Classic cut-off times,
pickup depot, state. getNoPickupDaysList(): DateTimeImmutable[] parses the raw comma-
separated date list.
Every createShipment()/createShipments()/checkOrderData() call is pre-validated
locally (no network call) against the field constraints documented in the DPD Cloud
Service Webservice documentation (error-code appendix):
| Field | Constraint |
|---|---|
| Weight | 0 - 31.5 kg |
YourInternalID / Reference1 / Reference2 / Content |
max 35 chars |
COD.Purpose |
max 14 chars (deprecated) |
ShipAddress.Company |
2-50 chars, when set |
ShipAddress.Name (first+last combined) |
2-50 chars |
ShipAddress.Street |
1-50 chars, required |
ShipAddress.HouseNo |
1-8 chars, required |
ShipAddress.City |
1-50 chars, required |
ShipAddress.ZipCode / Country |
required |
ShipAddress.Phone |
5-20 chars, when set |
ShipAddress.State (ISO 3166-2) |
exactly 2 chars, when set |
| Order batch size | max 30 OrderItems per call |
This is a best-effort local check mirroring DPD's own validation - it does not replace
checkOrderData() for a full server-side dry run. Failures throw
DpdCloudValidationException before any network call is made.
All exceptions extend VeryCodeCom\DpdDe\Exception\DpdCloudException.
| Exception | Trigger |
|---|---|
DpdCloudValidationException |
Local pre-flight validation failed - $errors: string[] |
DpdCloudAuthException |
DPD rejects Partner/User credentials, or the API call rate limit was hit (CLOUD_API_PARTNERCREDENTIALS, CLOUD_API_USERCREDENTIALS, CLOUD_API_NOLOGIN, CLOUD_API_NOUSERACCESS, CLOUD_API_USERCALLLIMIT) - carries $errorCode |
DpdCloudApiException |
Other DPD business-logic errors (Ack=false) - carries $errors (structured ErrorID/ErrorCode/messages); hasCode(string): bool and getFormattedErrors(): string[] helpers |
DpdCloudTransportException |
Network error or non-2xx HTTP response |
DpdCloudResponseParseException |
DPD returned unparseable / unexpected-shape XML or JSON |
Every exception exposes getRawResponse(): ?string (the exact response DPD returned,
when captured) and getDebugReport(): string (message + raw response + stack trace) -
see Debug mode.
DPD Cloud Service exposes both SOAP and REST endpoints for the same five operations. This
library fully supports both via the TransportMode constructor argument:
use VeryCodeCom\DpdDe\Enum\TransportMode;
$client = new DpdCloudClient($config, TransportMode::Rest);However, SOAP is the recommended default and is used unless you explicitly opt into REST, for these reasons:
- The SOAP contract is fully verified against the live WSDL
(
https://cloud.dpd.com/services/v1/DPDCloudService.asmx?wsdl) - every field name, type and namespace behaviour is confirmed. - The REST contract was reconstructed from the official PDF documentation and empirical
sandbox probing (no real production credentials were available during development).
Version/Languageheaders and thesetOrderJSON body shape are confirmed working; however, the exact HTTP header names forPartnerCredentials/UserCredentialscould not be conclusively verified without a real account. The convention used (PartnerCredentials.Name,PartnerCredentials.Token,UserCredentials.cloudUserID,UserCredentials.Token) is the most likely candidate but you should verify it against your own sandbox account (enable debug mode to inspect the raw response) before relying on REST in production. fetchParcelLifeCycle()always uses SOAP, regardless ofTransportMode- its deeply nested, UI-oriented JSON response shape was never empirically confirmed over REST, so no REST parser was implemented for it.
- Two tracking models -
getParcelLifeCycle("Parcel Life Cycle Service 2.0") returns pre-formatted, UI-rendering-oriented text blocks (ContentLine/ContentItem, with bold/paragraph flags);getOrderStatus("Parcel Life Cycle Service 3.1") returns a newer, more structured model with named milestones. They are not interchangeable and have different DTOs (TrackingResult/StatusInfovs.OrderStatus/StatusInfoDetail). ParcelLifeCycleService/2.0namespace quirk - this schema declareselementFormDefault="qualified"but every individual element insideTrackingResult(shipmentInfo,statusInfo,status,label,content, etc.) explicitly overrides this withform="unqualified". In practice, these elements carry no namespace at all in the real SOAP response - not even the schema's ownParcelLifeCycleService/2.0namespace.Internal\Soap\ResponseParserqueries them with unprefixed XPath expressions, not a registered prefix.- Document/literal SOAP style - unlike some other German carrier APIs, DPD Cloud
Service is plain document/literal: no
xsi:typeattributes, and the default namespacehttps://cloud.dpd.com/is declared once on the outer request element and inherited by every descendant. - No multi-parcel shipment (MPS) support in this API - every physical package needs
its own
OrderItem/ ship address (see the DPD Cloud Service FAQ). - Cash-on-delivery is deprecated - DPD fully discontinued "Nachnahme" (COD) on
2020-05-11. The
CodDTO,PaymentTypeenum, and*_CODShipServicevariants remain 1:1 with the WSDL for completeness but should not be relied on for new integrations. - Separate credentials per environment - sandbox and production each require their own PartnerCredentials/UserCredentials pair; they are not interchangeable.
The client accepts a custom TransportInterface and PSR-3 logger:
new DpdCloudClient(
config: DpdCloudConfig,
transportMode: TransportMode = TransportMode::Soap,
transport: TransportInterface = new CurlTransport(),
logger: ?Psr\Log\LoggerInterface = null,
)Implement TransportInterface::send(TransportRequest): TransportResponse to swap in a
PSR-18 HTTP client adapter, or a scripted fake for tests - see
tests/Unit/DpdCloudClientTest.php for the pattern
used by this library's own test suite (no real network calls).
composer install
# Unit tests (no network required)
vendor/bin/phpunit --testsuite unit
# Integration tests against the real DPD Cloud Service sandbox (Testsystem)
DPD_CLOUD_SANDBOX=1 \
DPD_CLOUD_PARTNER_NAME="DPD Cloud Service Alpha2" \
DPD_CLOUD_PARTNER_TOKEN=xxx \
DPD_CLOUD_USER_ID=123456 \
DPD_CLOUD_USER_TOKEN=xxx \
vendor/bin/phpunit --testsuite integration
# Static analysis (PHPStan level 8)
vendor/bin/phpstan analyse --memory-limit=512MApache License 2.0 - see NOTICE for attribution requirements.
You may use, distribute, and modify this library freely. You must retain the NOTICE file and copyright notices in any redistribution or derivative work.
Built by Very Code. Contributions welcome - open an issue or PR.