DPD Tracking API integration in an enterprise shipping application is more than sending a parcel number to a carrier endpoint. A production tracking workflow must obtain authentication, construct the correct SOAP request, handle HTTP failures, parse the carrier XML, determine the meaningful current status, normalize carrier-specific wording and return a stable result to the application.
The live ShippingPortal implementation uses a dedicated DpdTrackingClient behind IDpdTrackingClient. It uses a typed HttpClient, IOptions<DpdSettings>, structured logging and a separate IDpdAuthClient. DPD tracking is performed through the ParcelLifeCycleService using the getTrackingData operation.
- Keep DPD authentication separate from tracking request construction.
- Normalize the tracking number before putting it into the SOAP request.
- Use the configured DPD
HttpClientand send the requiredSOAPAction. - Parse
statusInforecords and distinguish reached/dated status from the final display label. - Normalize DPD wording into standard application states.
- Return stable no-data and error results rather than leaking carrier exceptions into the UI.
- Keep account identifiers, authentication values and customer-specific details out of public documentation.
1. Where DPD Tracking Fits in a Shipping Platform
The application should ask a tracking service for shipment status rather than knowing how DPD SOAP calls work. The carrier layer owns authentication, SOAP construction and XML parsing.
ShippingPortal
↓
Tracking Service
↓
IDpdTrackingClient
↓
IDpdAuthClient
↓
DPD ParcelLifeCycleService
↓
DPD XML Response
↓
TrackingInfo
↓
Standard TrackingResult
↓
UI / API
2. Create a Dedicated DPD Tracking Client
The live implementation keeps DPD transport concerns inside DpdTrackingClient and receives its dependencies through constructor injection.
public class DpdTrackingClient : IDpdTrackingClient
{
private readonly HttpClient _httpClient;
private readonly DpdSettings _settings;
private readonly ILogger<DpdTrackingClient> _logger;
private readonly IDpdAuthClient _dpdAuthClient;
private const string LoginServiceUrl =
"https://public-ws.dpd.com/services/LoginService/V2_0/";
private const string ParcelLifeCycleServiceUrl =
"https://public-ws.dpd.com/services/ParcelLifeCycleService/V2_0";
}
3. Configure the Client with Typed Settings and Authentication
IOptions<DpdSettings> supplies typed DPD configuration and IDpdAuthClient supplies the authentication token. The tracking client therefore does not need to know how login credentials are stored or how the token is obtained.
public DpdTrackingClient(
HttpClient httpClient,
IOptions<DpdSettings> settings,
ILogger<DpdTrackingClient> logger,
IDpdAuthClient dpdAuthClient)
{
_httpClient = httpClient;
_settings = settings.Value;
_logger = logger;
_dpdAuthClient = dpdAuthClient;
}
4. Start the DPD Tracking Operation
The public method obtains the DPD authentication token, requests the tracking XML and maps the XML into TrackingInfo. The supplied implementation catches unexpected failures and returns a stable error result.
public async Task<TrackingInfo> GetTrackingAsync(
string trackingNumber,
CancellationToken cancellationToken = default)
{
try
{
cancellationToken = CancellationToken.None;
string authToken =
await _dpdAuthClient.GetAuthTokenAsync();
string trackingXml =
await FetchTrackingXmlAsync(
authToken,
trackingNumber,
cancellationToken)
.ConfigureAwait(false);
return ParseTrackingResponse(
trackingXml,
trackingNumber);
}
catch (Exception ex)
{
_logger.LogError(
ex,
"DPD tracking failed for {TrackingNumber}",
trackingNumber);
return CreateErrorTrackingInfo(
"Error fetching tracking data");
}
}
5. Normalize the Tracking Number Before the SOAP Request
The live code trims whitespace, removes a trailing comma and limits the value to 14 characters before inserting it into the DPD request.
string cleanTrackingNumber =
trackingNumber?.Trim().TrimEnd(',')
?? string.Empty;
if (cleanTrackingNumber.Length > 14)
{
cleanTrackingNumber =
cleanTrackingNumber.Substring(0, 14);
}
6. Obtain the DPD Authentication Token Separately
Authentication is delegated to IDpdAuthClient. This keeps the tracking method focused on tracking rather than credential management or login protocol details.
string authToken =
await _dpdAuthClient.GetAuthTokenAsync();
7. Build the DPD SOAP Tracking Envelope
The supplied integration calls DPD's Parcel Life Cycle Service and uses the getTrackingData operation. The SOAP header contains the configured DelisId, the authentication token and the message language; the body contains the normalized parcel label number.
string soapEnvelope = $@"<soapenv:Envelope
xmlns:soapenv=""http://schemas.xmlsoap.org/soap/envelope/""
xmlns:ns=""http://dpd.com/common/service/types/Authentication/2.0""
xmlns:ns1=""http://dpd.com/common/service/types/ParcelLifeCycleService/2.0"">
<soapenv:Header>
<ns:authentication>
<delisId>{_settings.DelisId}</delisId>
<authToken>{authToken}</authToken>
<messageLanguage>en_EN</messageLanguage>
</ns:authentication>
</soapenv:Header>
<soapenv:Body>
<ns1:getTrackingData>
<parcelLabelNumber>{cleanTrackingNumber}</parcelLabelNumber>
</ns1:getTrackingData>
</soapenv:Body>
</soapenv:Envelope>";
8. Send the SOAP Request with SOAPAction
The integration sends an HTTP POST with the XML body as text/xml and adds the exact DPD SOAPAction for getTrackingData.
using var request =
new HttpRequestMessage(
HttpMethod.Post,
ParcelLifeCycleServiceUrl)
{
Content =
new StringContent(
soapEnvelope,
Encoding.UTF8,
"text/xml")
};
request.Headers.Add(
"SOAPAction",
"http://dpd.com/common/service/types/ParcelLifeCycleService/2.0/getTrackingData");
HttpResponseMessage response =
await _httpClient.SendAsync(
request,
cancellationToken)
.ConfigureAwait(false);
9. Handle Non-Success HTTP Responses Explicitly
A completed HTTP call can still represent a carrier failure. The client reads the response body, logs the status and response, then throws an HttpRequestException when DPD returns a non-success HTTP status.
string responseBody =
await response.Content.ReadAsStringAsync()
.ConfigureAwait(false);
if (!response.IsSuccessStatusCode)
{
_logger.LogError(
"DPD tracking failed: {StatusCode} - {Response}",
response.StatusCode,
responseBody);
throw new HttpRequestException(
$"DPD tracking failed: {response.StatusCode}");
}
return responseBody;
10. Parse the DPD Response with XDocument
The parser uses XDocument.Parse and finds all descendants whose local name is statusInfo. Using Name.LocalName keeps the parser independent of the namespace prefix used in the XML response.
XDocument doc =
XDocument.Parse(xml);
var allStatusInfos =
doc.Descendants()
.Where(e =>
e.Name.LocalName == "statusInfo")
.ToList();
if (allStatusInfos.Count == 0)
return CreateNoTrackingDataInfo();
11. Select the Latest Meaningful DPD Status
The implementation first searches for the last statusInfo where statusHasBeenReached is true and a non-empty date exists. If that is unavailable, it falls back to the last status with a date.
var reachedWithDate =
allStatusInfos
.Where(si =>
si.Elements().Any(e =>
e.Name.LocalName == "statusHasBeenReached"
&& e.Value.Equals(
"true",
StringComparison.OrdinalIgnoreCase)))
.Where(si =>
!string.IsNullOrWhiteSpace(
GetNestedContentLocal(si, "date")))
.LastOrDefault();
var statusWithDate =
reachedWithDate
?? allStatusInfos.LastOrDefault(
si =>
!string.IsNullOrWhiteSpace(
GetNestedContentLocal(si, "date")));
12. Use the Final Status Label for Display
The code then uses the final statusInfo element overall for the display label, while the selected dated status supplies the location and date used for shipment details.
var displayStatusInfo =
allStatusInfos.Last();
string rawLabel =
GetNestedContentLocal(
displayStatusInfo,
"label");
string location =
statusWithDate != null
? GetNestedContentLocal(
statusWithDate,
"location")
: string.Empty;
string dateStr =
statusWithDate != null
? GetNestedContentLocal(
statusWithDate,
"date")
: string.Empty;
13. Read Nested XML Content Safely
DPD values are nested under a content child. The helper reads the requested child by local name and then returns the nested content value.
private static string GetNestedContentLocal(
XElement parent,
string tagName)
{
var element =
parent.Elements()
.FirstOrDefault(
e => e.Name.LocalName == tagName);
if (element == null)
return string.Empty;
var contentElement =
element.Elements()
.FirstOrDefault(
e => e.Name.LocalName == "content");
return contentElement?.Value.Trim()
?? string.Empty;
}
14. Handle No-Tracking Responses Explicitly
The implementation checks for labels such as no tracking, not found, no information, unavailable and no data. These responses become a dedicated no-tracking state rather than a misleading Pending status.
private bool IsNoTrackingDataMessage(
string statusMessage)
{
if (string.IsNullOrWhiteSpace(statusMessage))
return true;
string lower =
statusMessage.ToLowerInvariant();
return lower.Contains("no tracking")
|| lower.Contains("not found")
|| lower.Contains("no information")
|| lower.Contains("unavailable")
|| lower.Contains("no data");
}
private TrackingInfo CreateNoTrackingDataInfo()
{
return new TrackingInfo
{
Status = "No Tracking Data",
DisplayStatus = "No Tracking Data",
Progress = 0,
EstDelivery = "—",
LastUpdate = "—"
};
}
15. Build a Rich Status with Date and Location
The live parser preserves the raw DPD label and adds a formatted date and location when those values are available.
string richDetail = rawLabel;
if (!string.IsNullOrWhiteSpace(dateStr))
{
if (DateHelper.TryParseDate(
dateStr,
"dd/MM/yyyy, HH:mm",
out DateTime dt))
{
richDetail +=
$" on {dt:dd MMM yyyy HH:mm}";
}
else
{
richDetail +=
$" on {dateStr}";
}
}
if (!string.IsNullOrWhiteSpace(location))
richDetail += $" at {location}";
16. Normalize DPD Statuses into Standard States
The live implementation converts carrier wording into three application states: Delivered, In Transit and Pending.
| Application state | DPD wording | Progress |
|---|---|---|
| Delivered | Contains "delivered" | 100% |
| In Transit | Out for delivery, in transit, delivery centre, handed to DPD, sending depot | 50% |
| Pending | Fallback for other statuses | 20% |
17. Use Case-Insensitive Substring Matching
The status mapper deliberately uses substring matching. This lets descriptive carrier messages still match the application's standard states without requiring an exact label.
private string NormalizeStatus(
string rawStatus)
{
if (string.IsNullOrWhiteSpace(rawStatus))
return "Pending";
string lower =
rawStatus.ToLowerInvariant();
if (lower.Contains("delivered"))
return "Delivered";
if (lower.Contains("out for delivery"))
return "In Transit";
if (lower.Contains("in transit"))
return "In Transit";
if (lower.Contains("delivery centre"))
return "In Transit";
if (lower.Contains("handed to dpd")
|| lower.Contains("parcel handed to dpd"))
return "In Transit";
if (lower.Contains("sending depot"))
return "In Transit";
return "Pending";
}
18. Convert the Status into UI Progress
The supplied implementation maps Delivered to 100, In Transit to 50 and Pending to 20.
private int CalculateProgress(
string normalizedStatus)
{
return normalizedStatus
.ToLowerInvariant() switch
{
"delivered" => 100,
"in transit" => 50,
"pending" => 20,
_ => 0
};
}
19. Set the Delivery Value for Delivered Shipments
When the normalized status is Delivered, the implementation uses the formatted last-update value as the delivery value and logs that assignment.
string estDelivery = "—";
if (normalizedStatus.Equals(
"Delivered",
StringComparison.OrdinalIgnoreCase))
{
estDelivery = lastUpdate;
_logger.LogInformation(
"DPD delivery date set: {EstDelivery} for {TrackingNumber}",
estDelivery,
trackingNumber);
}
20. Format DPD Dates in One Place
The carrier date format handled by the implementation is dd/MM/yyyy, HH:mm. The helper returns a consistent display format and falls back to the original value when parsing fails.
private string FormatLastUpdate(
string dateStr)
{
if (string.IsNullOrWhiteSpace(dateStr))
return "—";
if (DateHelper.TryParseDate(
dateStr,
"dd/MM/yyyy, HH:mm",
out DateTime dt))
{
return dt.ToString(
"dd MMM yyyy, HH:mm");
}
return dateStr;
}
21. Return the TrackingInfo Result
After parsing, the live implementation returns normalized status, rich display text, progress, delivery value, last update and selected raw values in RawData.
return new TrackingInfo
{
Status = normalizedStatus,
DisplayStatus = richDetail,
Progress = progress,
EstDelivery = estDelivery,
LastUpdate = lastUpdate,
RawData = new
{
RawLabel = rawLabel,
Location = location,
RawDate = dateStr
}
};
22. Return Stable Error Results
Remote failures are converted into a predictable tracking result so the application does not have to expose carrier exceptions directly to the user interface.
private TrackingInfo CreateErrorTrackingInfo(
string errorMessage)
{
return new TrackingInfo
{
Status = "Exception",
DisplayStatus = "Error",
Progress = 0,
EstDelivery = "—",
LastUpdate = errorMessage
};
}
23. Keep Authentication, SOAP and Parsing Separate
The live design has clear responsibilities: IDpdAuthClient provides authentication, DpdTrackingClient builds and sends the DPD request, and ParseTrackingResponse interprets the carrier XML.
IDpdAuthClient
↓
Authentication Token
↓
DpdTrackingClient
↓
SOAP Request
↓
DPD ParcelLifeCycleService
↓
XML Response
↓
ParseTrackingResponse(...)
↓
TrackingInfo
24. Use DPD as One Carrier in a Multi-Carrier Platform
DPD should remain one implementation in a broader tracking architecture. UPS, DHL, DPD and Asendia can each retain their carrier-specific authentication and response parsing while the application works with a common tracking workflow.
ITrackingClient
│
├── UPS Tracking Client
├── DHL Tracking Client
├── DPD Tracking Client
└── Asendia Tracking Client
25. Protect DPD Credentials and Customer Information
The live code uses account-specific settings such as DelisId and an authentication token. This article intentionally does not reproduce customer identifiers, credentials, account values or other environment-specific secrets. Those values should remain in secure application configuration and authentication components.
The same rule applies to logs. Structured logging should carry enough information to diagnose failures without unnecessarily storing credentials or sensitive shipment data.
26. Test the DPD Integration at Multiple Levels
- Validate trimming, trailing-comma removal and the 14-character limit.
- Test authentication independently from tracking.
- Test SOAP request generation with safe test settings.
- Test unsuccessful HTTP responses.
- Test XML with no
statusInfoelements. - Test reached-status selection and fallback-to-dated-status behavior.
- Test every known status phrase plus unknown wording.
- Test DPD date parsing and fallback behavior.
- Test no-data and error-result mapping.
27. Measure Authentication, SOAP and Parsing Separately
A slow tracking screen can have several causes. Measure authentication, the DPD HTTP request, XML parsing, status normalization and application mapping separately rather than treating the whole tracking operation as one black box.
Authentication: measure separately
DPD SOAP request: measure separately
XML parsing: measure separately
Status normalization: measure separately
Application mapping: measure separately
28. The Complete DPD Tracking Flow
The live implementation can be summarized as the following sequence.
1. Receive tracking number
↓
2. Obtain DPD authentication token
↓
3. Trim and normalize tracking number
↓
4. Limit tracking number to 14 characters
↓
5. Build DPD SOAP envelope
↓
6. POST to ParcelLifeCycleService
↓
7. Add SOAPAction for getTrackingData
↓
8. Validate HTTP response
↓
9. Parse XML with XDocument
↓
10. Locate statusInfo records
↓
11. Select latest reached/dated status
↓
12. Read final display label
↓
13. Normalize DPD status
↓
14. Calculate progress
↓
15. Format last update
↓
16. Return TrackingInfo
↓
17. Map to the application's standard tracking result
29. DPD Tracking API Integration Checklist
- Is DPD tracking isolated behind
IDpdTrackingClient? - Is authentication handled by
IDpdAuthClient? - Are DPD settings provided through typed configuration?
- Is the tracking number normalized before the SOAP request?
- Is the 14-character carrier limit applied consistently?
- Is the correct SOAPAction used?
- Are HTTP failures handled explicitly?
- Are
statusInforecords parsed by local name? - Are reached and dated statuses selected correctly?
- Are DPD labels normalized into application-level states?
- Are no-data and exception results handled separately?
- Are account identifiers and secrets excluded from public source?
- Can the application reuse the same tracking workflow for UPS, DHL, DPD and Asendia?
30. Conclusion
A production DPD Tracking API integration in C# requires careful handling of authentication, SOAP request construction, tracking-number normalization, XML parsing, status selection, date formatting and application-level failures.
The ShippingPortal implementation demonstrates a clean carrier boundary: authentication is delegated to a separate client, DPD-specific HTTP and SOAP behavior stays inside DpdTrackingClient, XML is parsed into TrackingInfo, and carrier wording is normalized before the result reaches the application layer.
That separation also makes DPD easier to maintain alongside UPS, DHL and Asendia integrations in an enterprise shipping platform.