DHL Tracking & .NET Shipping Integration

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

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

September 2026 • 16 min read • By Kshitij Mehta

Shipment tracking becomes much harder once it moves beyond a single carrier page. An enterprise shipping application has to retrieve carrier data, authenticate correctly, tolerate API failures, normalize carrier-specific statuses, preserve shipment history and return a stable model to the user interface.

The DHL implementation used in a ShippingPortal-style .NET architecture follows that pattern. The tracking workflow is separated into a dedicated DHLTrackingClient, uses IHttpClientFactory for outbound HTTP calls, stores recent results in IMemoryCache, parses DHL XML responses into application models and maps DHL status text into a small set of standard shipment states.

Security note The original implementation contains environment-specific DHL credentials and account settings. Those values are intentionally not reproduced here. Production applications should load API credentials from secure configuration or a secret-management system rather than embedding them in source code.

1. DHL Tracking Integration Architecture

The useful part of this design is that the rest of the application does not need to know how DHL authentication, XML construction or response parsing works. It calls one tracking abstraction and receives a normalized result.

Tracking UI
Application Service
IDhlTrackingClient
DHL API
Normalized Result

The client itself depends on IHttpClientFactory, IMemoryCache, ILogger<DHLTrackingClient>, IConfiguration and a DHL settings repository. This keeps carrier access inside a dedicated infrastructure component instead of spreading HTTP and XML logic across controllers or Razor pages.

2. The Application-Level Tracking Method

The application service can stay very small. It calls the injected DHL tracking client, converts the carrier result into the application's common TrackingResult, and catches unexpected failures so a carrier outage does not crash the tracking page.

private async Task<TrackingResult> TrackDHLAsync(
    string trackingNumber,
    CancellationToken cancellationToken)
{
    try
    {
        var dhlResult = await _dhlTrackingClient
            .GetTrackingAsync(trackingNumber, cancellationToken);

        return MapFromDhlResult(dhlResult);
    }
    catch (Exception ex)
    {
        _logger.LogError(
            ex,
            "DHL tracking failed for {TrackingNumber}",
            trackingNumber);

        return CreateDefaultResult(
            "Exception",
            "Error",
            0,
            "—",
            "Error");
    }
}

This wrapper is valuable in a multi-carrier system because the user-facing contract can remain the same even when the underlying carrier APIs behave very differently.

3. Building the DHL Tracking Client

The carrier client implements IDhlTrackingClient and receives its infrastructure dependencies through constructor injection.

public class DHLTrackingClient : IDhlTrackingClient
{
    private readonly IHttpClientFactory _httpClientFactory;
    private readonly IMemoryCache _cache;
    private readonly ILogger<DHLTrackingClient> _logger;
    private readonly IConfiguration _configuration;
    private readonly IDhlSettingsRepository _dhlSettingsRepository;

    private const string DhlApiBaseUrl =
        "https://api-eu.dhl.com/parcel/de/tracking/v0/shipments";
}

There are two important architectural choices here. First, outbound HTTP is created through IHttpClientFactory rather than constructing a new HttpClient for every request. Second, carrier settings are accessed through a repository abstraction, which keeps account-specific data outside the parsing and mapping logic.

4. Validate Tracking Numbers and Cache Recent Results

The implementation rejects empty tracking numbers before making a remote call. It then trims whitespace and removes a trailing comma before generating a cache key.

if (string.IsNullOrWhiteSpace(trackingNumber))
    return CreateErrorResult("Tracking number is required.");

string cleanTrackingNumber = trackingNumber?.Trim().TrimEnd(',') ?? string.Empty;

var cacheKey = $"DHL_Track_{cleanTrackingNumber}";

if (_cache.TryGetValue(cacheKey, out DhlTrackingResult cached))
    return cached;

var result = await FetchTrackingFromApiAsync(
    cleanTrackingNumber,
    cancellationToken);

_cache.Set(
    cacheKey,
    result,
    TimeSpan.FromMinutes(15));

A 15-minute in-memory cache can reduce repeated requests when a tracking page is refreshed frequently. The correct cache duration depends on business requirements, shipment volume and the freshness expected by the user interface.

5. Calling the DHL Tracking API

The implementation retrieves carrier settings, builds the DHL XML request, URL-encodes the XML, sends a GET request and parses the XML response.

var dhlApiSetting = await _dhlSettingsRepository
    .GetSettingsAsyncLive("Others", "<setting-key>", "<app-name>");

string billingNumber = dhlApiSetting.AccountNumber;

string apiKey = _configuration["DHL:ApiKey"] ?? string.Empty;
string apiSecret = _configuration["DHL:ApiSecret"] ?? string.Empty;
string username = _configuration["DHL:AppName"] ?? string.Empty;
string password = dhlApiSetting.Signature;
string zipCode = _configuration["DHL:ZipCode"] ?? string.Empty;

string xml = $@"<?xml version=""1.0"" encoding=""UTF-8""?>
<data appname=""{username}""
      password=""{password}""
      request=""d-get-piece-detail""
      language-code=""en""
      zip-code=""{zipCode}""
      piece-code=""{trackingNumber}"">
</data>";

string encodedXml = HttpUtility.UrlEncode(xml);
string requestUrl = $"{DhlApiBaseUrl}?xml={encodedXml}";

var client = _httpClientFactory.CreateClient("DHL");

using var request = new HttpRequestMessage(
    HttpMethod.Get,
    requestUrl);

string credentials = Convert.ToBase64String(
    Encoding.UTF8.GetBytes($"{apiKey}:{apiSecret}"));

request.Headers.Authorization =
    new AuthenticationHeaderValue("Basic", credentials);

request.Headers.Add("dhl-api-key", apiKey);

var response = await client.SendAsync(
    request,
    cancellationToken);

The source implementation uses DHL's parcel tracking endpoint at api-eu.dhl.com, constructs the request using the d-get-piece-detail operation and sends credentials using both Basic authentication and the DHL API key header.

Do not copy secrets into source code

The source code has fallback values for configuration. That is useful to identify what settings the component expects, but real deployments should not keep live secrets in a repository. Prefer environment variables, managed configuration or a dedicated secret store.

6. Handling HTTP Failures Explicitly

Carrier APIs can fail because of authentication problems, invalid requests, transient network conditions or upstream service issues. The client logs non-success responses and turns them into an exception that the outer tracking method can handle consistently.

string responseBody =
    await response.Content.ReadAsStringAsync();

if (!response.IsSuccessStatusCode)
{
    _logger.LogWarning(
        "DHL API returned {StatusCode}: {Response}",
        response.StatusCode,
        responseBody);

    throw new HttpRequestException(
        $"DHL API error: {response.StatusCode}");
}

return ParseTrackingResponse(
    responseBody,
    trackingNumber);

Logging the status code and response body can make production diagnosis substantially easier. In a live environment, however, response bodies should be reviewed for sensitive shipment or credential-related data before being logged in full.

7. Parsing DHL XML by Element Attributes

The DHL response is XML rather than the JSON commonly used by many modern REST APIs. The implementation uses XDocument and searches for elements according to their name attribute.

XDocument doc = XDocument.Parse(xmlResponse);

var shipmentElement = doc.Descendants()
    .FirstOrDefault(e =>
        (string)e.Attribute("name") == "piece-shipment");

var eventElements = doc.Descendants()
    .Where(e =>
        (string)e.Attribute("name") == "piece-event")
    .ToList();

This is an important detail of the implementation. The parser is not assuming that the XML element name itself is piece-shipment. It looks for elements whose name attribute contains that value. The same approach is used for piece-event elements.

8. Building a Tracking Event Timeline

Each event is converted into a DhlTrackingEvent. The source implementation uses the short status when present and falls back to the full event status.

List<DhlTrackingEvent> events = new List<DhlTrackingEvent>();

foreach (var ev in eventElements)
{
    events.Add(new DhlTrackingEvent
    {
        Status = ev.Attribute("event-short-status")?.Value
              ?? ev.Attribute("event-status")?.Value
              ?? string.Empty,
        Location = ev.Attribute("event-location")?.Value
                 ?? string.Empty,
        Timestamp = ev.Attribute("event-timestamp")?.Value
                 ?? string.Empty
    });
}

Keeping the event list separate from the current shipment status allows the UI to present a history timeline while still exposing a single standard status for filtering, reporting and dashboards.

9. Mapping DHL Status Text to Standard States

One of the most important pieces of a multi-carrier tracking system is status normalization. DHL can return different phrases that mean roughly the same business state. The implementation maps those phrases into four application-level states:

Application status Examples handled by the implementation Progress
Delivered Delivered, delivery successful, successfully delivered, parcel delivered 100%
In Transit Out for delivery, in transit, delivery in progress, at delivery centre, destination parcel center 50%
Pending Label created, posted by the sender, dropped at retail, pick-up was successful, handed over 10%
Exception Customs information required, more information required, returned, address problem, shipment refused 0%

The code first tries an exact, case-insensitive match and then performs a substring match. That second stage is useful for longer DHL status messages where a known phrase appears inside a more descriptive response.

if (statusMap.TryGetValue(key, out string? standard))
    return standard;

foreach (var kvp in statusMap)
{
    if (key.Contains(
        kvp.Key,
        StringComparison.OrdinalIgnoreCase))
    {
        _logger.LogInformation(
            "DHL status matched: '{RawStatus}' -> '{StandardStatus}'",
            dhlStatus,
            kvp.Value);

        return kvp.Value;
    }
}

_logger.LogWarning(
    "DHL status not mapped: '{RawStatus}' for tracking number",
    dhlStatus);

return "Pending";

Logging an unmapped status is particularly useful. Carrier wording evolves, and a warning gives the development team evidence for extending the mapping dictionary instead of silently losing information.

10. Extracting the Current Status and Shipment Details

The parser takes the current state from the top-level piece-shipment element. It reads the short status first, falls back to the full status, and builds a richer display value from the status, timestamp and location.

string rawShortStatus =
    shipmentElement.Attribute("short-status")?.Value;

string rawStatusFull =
    shipmentElement.Attribute("status")?.Value;

string rawStatus =
    rawShortStatus ?? rawStatusFull ?? "Pending";

string location =
    shipmentElement.Attribute("event-location")?.Value ?? "";

string timestamp =
    shipmentElement.Attribute("status-timestamp")?.Value ?? "";

string richDetail = rawStatus;

if (!string.IsNullOrWhiteSpace(timestamp))
    richDetail += $" on {timestamp}";

if (!string.IsNullOrWhiteSpace(location))
    richDetail += $" at {location}";

The result is then populated with the normalized status, progress percentage, formatted last update, last location and the complete event list.

11. Estimated Delivery and Delivered Timestamps

The implementation looks for the DHL delivery-date attribute on the shipment element. When that value is unavailable but the shipment is already delivered, it falls back to the status timestamp.

string deliveryDate =
    shipmentElement.Attribute("delivery-date")?.Value;

if (!string.IsNullOrWhiteSpace(deliveryDate))
{
    result.EstDelivery = FormatDate(deliveryDate);
}
else if (result.Status == "Delivered" &&
         !string.IsNullOrWhiteSpace(timestamp))
{
    result.EstDelivery = FormatTimestamp(timestamp);
}

This keeps the application model stable even when a particular response does not contain an estimated delivery date.

12. Date Formatting

The source implementation delegates date parsing to a shared DateHelper. The tracking client then formats timestamps for display.

private string FormatTimestamp(string timestamp)
{
    if (DateHelper.TryParseDateTime(
        timestamp,
        out DateTime dt))
    {
        return dt.ToString("dd MMM yyyy, HH:mm");
    }

    return timestamp;
}

private string FormatDate(string dateStr)
{
    if (DateHelper.TryParseDateTime(
        dateStr,
        out DateTime dt))
    {
        return dt.ToString("dd MMM yyyy");
    }

    return dateStr;
}

Keeping the parser tolerant is useful when upstream formats differ from the most common value. Returning the original value as a fallback also avoids turning an otherwise valid shipment response into a blank UI field.

13. Returning Stable Error Results

Parsing failures are caught and represented as an Exception result instead of escaping through the application boundary.

catch (Exception ex)
{
    _logger.LogError(
        ex,
        "Error parsing DHL tracking response for {TrackingNumber}",
        trackingNumber);

    result.Status = "Exception";
    result.DisplayStatus = "Parse error";
    result.Progress = 0;
}

The same principle is used when the remote service itself fails:

private DhlTrackingResult CreateErrorResult(
    string message = null)
{
    return new DhlTrackingResult
    {
        Status = "Exception",
        DisplayStatus = message ?? "Error",
        Progress = 0,
        EstDelivery = "—",
        LastUpdate = "—",
        LastLocation = "—"
    };
}

14. Production Improvements for Enterprise Shipping Systems

The source implementation is already structured around clear responsibilities. For a larger carrier-integration platform, several additional practices can make the pattern more robust.

  • Move secrets completely out of code: use environment-specific configuration or a secret manager for API keys, application credentials and account information.
  • Configure the named HTTP client: give the DHL client explicit timeouts, default headers where appropriate, and resilient transport settings.
  • Use cancellation consistently: propagate the supplied CancellationToken through every downstream operation.
  • Expand observability: log correlation identifiers, response timings, carrier status and failure categories without exposing sensitive shipment data.
  • Protect status mapping from regressions: add automated tests for every known status phrase plus representative unknown values.
  • Separate carrier models from application models: keep DhlTrackingResult and DhlTrackingEvent inside the carrier integration boundary, then map them into a shared tracking contract.
  • Review cache strategy: in-memory caching is simple and effective for one application instance, while distributed caching can make more sense across multiple nodes.

15. Using the Same Pattern for UPS, DPD and Asendia

The most important architectural lesson is not the XML itself. It is the boundary around the carrier.

A multi-carrier shipping platform can expose a consistent application-level contract while each integration handles its own transport and response format:

public interface ITrackingClient
{
    Task<TrackingResult> GetTrackingAsync(
        string trackingNumber,
        CancellationToken cancellationToken = default);
}

Under that boundary, DHL can use its XML response parsing, while UPS, DPD and Asendia can use their own authentication and response models. The business layer does not need to know those carrier-specific implementation details.

This approach makes it easier to add carrier-specific features without turning one service class into a large conditional statement full of API-specific rules.

16. Why This Architecture Works for ShippingPortal-Style Applications

Tracking is usually only one step in an enterprise shipping workflow. The same application may create labels, store shipment records, send tracking numbers to an ERP, process carrier responses and expose tracking information to customer-service users.

Keeping DHL communication in a dedicated client means those workflows can remain independent from the carrier protocol. The application can request a normalized shipment status while the integration layer handles credentials, HTTP, XML parsing, caching, logging and DHL-specific status phrases.

That separation is particularly useful when an organization supports several carriers. It allows a single tracking experience to sit above several different APIs.

17. DHL Tracking API Integration Checklist

  • Validate and normalize the tracking number before making the API call.
  • Use a dedicated IDhlTrackingClient abstraction.
  • Use IHttpClientFactory for outbound requests.
  • Keep API keys, secrets and account settings in secure configuration.
  • Construct the DHL XML request in a dedicated carrier method.
  • Handle non-success HTTP responses explicitly.
  • Parse shipment and event information separately.
  • Normalize DHL statuses into application-level states.
  • Store the event timeline for shipment-history views.
  • Cache recent tracking responses where the business permits it.
  • Log unmapped carrier statuses so the mapping can evolve.
  • Return stable error models to the application layer.
  • Keep the carrier implementation independent from the rest of the shipping workflow.

18. Conclusion

A production-grade DHL tracking integration is more than a single HTTP request. The useful design is the complete flow: validate the tracking number, retrieve the correct carrier settings, construct the XML request, authenticate safely, call DHL through IHttpClientFactory, cache appropriate responses, parse shipment events, normalize status values and return a stable result to the application.

The implementation described here follows that approach and fits naturally into a broader multi-carrier architecture. The same application-level tracking contract can sit above UPS, DHL, DPD and Asendia integrations while each carrier remains isolated behind its own client and mapping logic.

For enterprise shipping software, that separation is what keeps carrier integrations maintainable as shipment volume, carrier count and business requirements grow.