Skip to content

Repository files navigation

dpd-de-php

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.

Latest Version Total Downloads CI PHPStan PHP License


Requirements

  • PHP 8.2+
  • ext-curl
  • ext-dom
  • ext-json

Installation

composer require very-code-com/dpd-de-php

Quick Start

use 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-decoded

See examples/ for complete, runnable scripts.


Configuration

// 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.

Debug mode

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.


API Reference

createShipment(OrderItem $item, ?OrderSettings $settings = null): SetOrderResult

createShipments(array $items, ?OrderSettings $settings = null): SetOrderResult

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 OrderItem with its own shipAddress.

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).

checkOrderData(array $items, ?OrderSettings $settings = null): void

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.

validateLocally(array $items): string[]

Runs the same local field/weight checks createShipment() performs, with no network call. Returns an empty array when the items are locally valid.

fetchParcelLifeCycle(string $parcelNo): TrackingResult

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).

fetchOrderStatus(string $parcelNo, ?string $deliveryZipCode = null): OrderStatus

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.

findParcelShops(ParcelShopQuery $query): ParcelShop[]

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.

fetchZipCodeRules(): ZipCodeRules

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.


Local validation

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.


Exceptions

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.


REST caveats

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:

  1. 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.
  2. The REST contract was reconstructed from the official PDF documentation and empirical sandbox probing (no real production credentials were available during development). Version/Language headers and the setOrder JSON body shape are confirmed working; however, the exact HTTP header names for PartnerCredentials/UserCredentials could 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.
  3. fetchParcelLifeCycle() always uses SOAP, regardless of TransportMode - its deeply nested, UI-oriented JSON response shape was never empirically confirmed over REST, so no REST parser was implemented for it.

Known DPD Cloud Service API quirks

  1. 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/StatusInfo vs. OrderStatus/StatusInfoDetail).
  2. ParcelLifeCycleService/2.0 namespace quirk - this schema declares elementFormDefault="qualified" but every individual element inside TrackingResult (shipmentInfo, statusInfo, status, label, content, etc.) explicitly overrides this with form="unqualified". In practice, these elements carry no namespace at all in the real SOAP response - not even the schema's own ParcelLifeCycleService/2.0 namespace. Internal\Soap\ResponseParser queries them with unprefixed XPath expressions, not a registered prefix.
  3. Document/literal SOAP style - unlike some other German carrier APIs, DPD Cloud Service is plain document/literal: no xsi:type attributes, and the default namespace https://cloud.dpd.com/ is declared once on the outer request element and inherited by every descendant.
  4. No multi-parcel shipment (MPS) support in this API - every physical package needs its own OrderItem / ship address (see the DPD Cloud Service FAQ).
  5. Cash-on-delivery is deprecated - DPD fully discontinued "Nachnahme" (COD) on 2020-05-11. The Cod DTO, PaymentType enum, and *_COD ShipService variants remain 1:1 with the WSDL for completeness but should not be relied on for new integrations.
  6. Separate credentials per environment - sandbox and production each require their own PartnerCredentials/UserCredentials pair; they are not interchangeable.

Dependency Injection & Testing

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).


Running Tests

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=512M

License

Apache 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.

About

PHP client library for the DPD Cloud Service Webservice (DPD Deutschland) — create shipments (setOrder), track parcels (getParcelLifeCycle, getOrderStatus), find ParcelShop pickup points and fetch pickup zip-code rules, over SOAP or REST

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages