DPD Tracking & .NET Shipping Integration

DPD Tracking API Integration with C#:
Building a Scalable .NET Tracking Solution

A production-oriented approach to DPD Tracking API integration in C#, ASP.NET Core and multi-carrier shipping software.

September 2026 • 16 min read • By Kshitij Mehta

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.

Key Takeaways
  • Keep DPD authentication separate from tracking request construction.
  • Normalize the tracking number before putting it into the SOAP request.
  • Use the configured DPD HttpClient and send the required SOAPAction.
  • Parse statusInfo records 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 statusInfo elements.
  • 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 statusInfo records 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.


31. About the Author

I'm Kshitij Mehta, a Senior .NET Developer and ASP.NET Core Consultant with experience developing enterprise software, shipping platforms, integrations and database-driven applications.

My work includes C#, ASP.NET Core, SQL Server, Dapper, ADO.NET, REST APIs, shipping software, carrier API integrations and enterprise application architecture.