Asendia Tracking & .NET Shipping Integration

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

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

September 2026 • 16 min read • By Kshitij Mehta

Asendia Tracking API integration becomes important when a shipping platform needs to expose parcel status from Asendia alongside other carrier services. The difficult part is not simply sending a tracking number. A production integration must obtain an access token, build the correct JSON request, send the required headers, handle unsuccessful responses, identify the newest tracking event, normalize carrier wording and return a stable result to the application.

The live ShippingPortal implementation uses a dedicated AsendiaTrackingClient behind IAsendiaTrackingClient. It receives a configured HttpClient, typed AsendiaCredentials and an ILogger. The client obtains an OAuth access token, posts a bulk tracking request containing the requested tracking ID, deserializes the response and maps the result into the application's TrackingInfo model.

Key Takeaways
  • Keep Asendia transport and authentication inside a dedicated carrier client.
  • Use typed credentials and a configured HttpClient rather than embedding customer settings in the article.
  • Obtain OAuth tokens through the client-credentials flow before calling the tracking endpoint.
  • Send the tracking ID as a JSON array using the live bulk-tracking request model.
  • Map the newest event by EventTime instead of assuming the first event is current.
  • Normalize Delivered, In Transit and Pending states for the shared application workflow.
  • Return dedicated no-data and error results when tracking information is unavailable.
  • Keep customer-specific credentials, account identifiers and environment details out of public documentation.

1. Where Asendia Tracking Fits in a Shipping Platform

The application should ask a tracking service for shipment information rather than knowing how Asendia authentication or JSON requests work. The carrier client owns those details and returns an application-level result.


ShippingPortal
      ↓
Tracking Service
      ↓
IAsendiaTrackingClient
      ↓
OAuth Token
      ↓
Asendia Tracking API
      ↓
TrackingBulkResponse
      ↓
TrackingInfo
      ↓
Standard TrackingResult
      ↓
UI / API
                

2. Create a Dedicated Asendia Tracking Client

The live integration is implemented as AsendiaTrackingClient, which implements IAsendiaTrackingClient. Its dependencies are supplied through constructor injection.


public class AsendiaTrackingClient
    : IAsendiaTrackingClient
{
    private readonly HttpClient _httpClient;
    private readonly AsendiaCredentials _credentials;
    private readonly ILogger<AsendiaTrackingClient> _logger;

    public AsendiaTrackingClient(
        HttpClient httpClient,
        IOptions<AsendiaCredentials> credentials,
        ILogger<AsendiaTrackingClient> logger)
    {
        _httpClient = httpClient;
        _credentials = credentials.Value;
        _logger = logger;
    }
}
                

3. Start the Tracking Operation with Cancellation

The public tracking method trims the supplied tracking number, obtains an OAuth token, builds the bulk request, sends it to the configured Asendia endpoint and maps the response. Unexpected failures are logged and converted into an application error result.


public async Task<TrackingInfo> GetTrackingAsync(
    string trackingNumber,
    CancellationToken cancellationToken = default)
{
    try
    {
        string cleanTrackingNumber =
            trackingNumber?.Trim().TrimEnd(',')
            ?? string.Empty;

        string accessToken =
            await GetAccessTokenAsync(
                cancellationToken)
            .ConfigureAwait(false);

        var requestObj =
            new TrackingBulkRequest
            {
                trackingIds =
                    new[] { cleanTrackingNumber }
            };

        string json =
            JsonConvert.SerializeObject(
                requestObj);

        // Send the tracking request...

        return MapToTrackingInfo(
            bulkResponse,
            cleanTrackingNumber);
    }
    catch (Exception ex)
    {
        _logger.LogError(
            ex,
            "Asendia tracking failed for {TrackingNumber}",
            trackingNumber);

        return CreateErrorTrackingInfo(
            "Error fetching tracking data");
    }
}
                

4. Obtain an OAuth Access Token with Client Credentials

The live integration uses the OAuth client-credentials grant. The credentials are stored in the typed AsendiaCredentials object and are posted to the configured token URL as form-encoded values.


private async Task<string> GetAccessTokenAsync(
    CancellationToken cancellationToken)
{
    var formValues =
        new Dictionary<string, string>
        {
            { "client_id", _credentials.ClientId },
            { "client_secret", _credentials.ClientSecret },
            { "grant_type", "client_credentials" }
        };

    var content =
        new FormUrlEncodedContent(
            formValues);

    HttpResponseMessage response =
        await _httpClient.PostAsync(
            _credentials.TokenUrl,
            content,
            cancellationToken)
        .ConfigureAwait(false);

    response.EnsureSuccessStatusCode();

    string json =
        await response.Content
            .ReadAsStringAsync()
            .ConfigureAwait(false);

    var tokenResponse =
        JsonConvert.DeserializeObject<TokenResponse>(
            json);

    return tokenResponse.access_token;
}
                

5. Build the Asendia Bulk Tracking Request

The tracking endpoint receives a JSON object containing a trackingIds array. The live code serializes TrackingBulkRequest with the cleaned tracking number as its only ID.


var requestObj =
    new TrackingBulkRequest
    {
        trackingIds =
            new[] { cleanTrackingNumber }
    };

string json =
    JsonConvert.SerializeObject(
        requestObj);
                

6. Send the Required Asendia Authentication Headers

The live request sends the configured access key and API key as headers and places the OAuth token in a Bearer authorization header.


using var request =
    new HttpRequestMessage(
        HttpMethod.Post,
        _credentials.ApiEndpoint)
    {
        Content =
            new StringContent(
                json,
                Encoding.UTF8,
                "application/json")
    };

request.Headers.Add(
    "access_key",
    _credentials.AccessKey);

request.Headers.Add(
    "x-api-key",
    _credentials.ApiKey);

request.Headers.Authorization =
    new AuthenticationHeaderValue(
        "Bearer",
        accessToken);
                

7. Send the Tracking Request Through the Configured HttpClient

The integration uses the injected HttpClient and propagates the supplied CancellationToken to the network call.


HttpResponseMessage response =
    await _httpClient.SendAsync(
        request,
        cancellationToken)
    .ConfigureAwait(false);

string respJson =
    await response.Content
        .ReadAsStringAsync()
        .ConfigureAwait(false);
                

This keeps HTTP configuration outside the tracking logic while allowing the application to apply its normal networking and timeout policies.


8. Handle Unsuccessful Asendia Responses Explicitly

The client checks IsSuccessStatusCode. When Asendia returns an unsuccessful HTTP response, the integration logs the status code and response body and returns a stable error result.


if (!response.IsSuccessStatusCode)
{
    _logger.LogError(
        "Asendia API error: {StatusCode} - {Response}",
        response.StatusCode,
        respJson);

    return CreateErrorTrackingInfo(
        $"API error: {response.StatusCode}");
}
                

In production, response-body logging should be reviewed so credentials and unnecessary customer or shipment information are not written to logs.


9. Deserialize the Asendia Tracking Response

A successful response is deserialized into the carrier-specific TrackingBulkResponse model before application-level mapping occurs.


var bulkResponse =
    JsonConvert.DeserializeObject<TrackingBulkResponse>(
        respJson);

return MapToTrackingInfo(
    bulkResponse,
    cleanTrackingNumber);
                

This creates a clean boundary between the carrier's JSON contract and the application's normalized tracking model.


10. Distinguish Tracking Data from Not-Found Results

The mapper first checks whether the response contains a populated data collection. If so, it maps the first returned tracking detail. It separately handles the data_not_found collection and a fallback response that contains neither.


if (response.data != null &&
    response.data.Count > 0)
{
    var detail = response.data[0];

    return MapTrackingDetail(
        detail,
        trackingNumber);
}

if (response.data_not_found != null &&
    response.data_not_found.Count > 0)
{
    _logger.LogInformation(
        "Asendia tracking number not found: {TrackingNumber}",
        trackingNumber);

    return CreateNoTrackingDataInfo();
}

_logger.LogWarning(
    "Asendia response without data: {Message}",
    response.message);

return CreateNoTrackingDataInfo();
                

11. Find the Most Recent Event by EventTime

The live mapper does not assume that the first event in the collection is the newest event. It orders the events by EventTime descending and takes the first one.


EventDetail? latestEvent = null;

if (detail.events != null &&
    detail.events.Count > 0)
{
    latestEvent =
        detail.events
            .OrderByDescending(
                e => e.EventTime)
            .FirstOrDefault();
}
                

This is one of the most important source-specific details because the event collection order is not used as the definition of recency.


12. Build a Rich Display Status from the Latest Event

The current display value begins with the carrier-level Status. When an event is available, the implementation prefers the event's Label, falls back to Description, and appends a formatted timestamp and location.


string richDetail =
    detail.Status ?? "Unknown";

if (latestEvent != null)
{
    string eventLabel =
        latestEvent.Label
        ?? latestEvent.Description
        ?? string.Empty;

    string eventTime =
        latestEvent.EventTime ?? string.Empty;

    string location =
        latestEvent.LocationName ?? string.Empty;

    if (!string.IsNullOrWhiteSpace(eventLabel))
        richDetail = eventLabel;

    if (!string.IsNullOrWhiteSpace(eventTime))
    {
        if (DateHelper.TryParseDateTime(
            eventTime,
            out DateTime dt))
        {
            richDetail +=
                $" on {dt:dd MMM yyyy HH:mm}";
        }
        else
        {
            richDetail +=
                $" on {eventTime}";
        }
    }

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

13. Normalize Asendia Statuses into Standard States

The live normalization logic maps Asendia wording into Delivered, In Transit and Pending. It checks the combined rich status string so event labels can determine the application state.

Application state Examples handled by the implementation Progress
Delivered Delivered, delivery successful, successfully delivered 100%
In Transit In transit, check-in Asendia facility, arrived at facility, arrived at sorting/depot, departed, processed at, handed over, on its way, out for delivery, delivery in progress 50%
Pending Electronic notification, order data provided, shipment information received, label created, pre-advice, and unknown values 20%

14. Map Delivered, Transit and Pre-Shipment Wording

The source implementation uses case-insensitive substring matching. This is intentionally broad enough to recognize related phrases without requiring an exact status string.


private string NormalizeStatus(
    string rawStatus)
{
    if (string.IsNullOrWhiteSpace(rawStatus))
        return "Pending";

    string lower =
        rawStatus.Trim().ToLowerInvariant();

    if (lower.Contains("delivered") ||
        lower.Contains("delivery successful") ||
        lower.Contains("successfully delivered"))
    {
        return "Delivered";
    }

    if (lower.Contains("in transit") ||
        lower.Contains("check-in asendia facility") ||
        lower.Contains("arrived at facility") ||
        lower.Contains("arrived at sorting") ||
        lower.Contains("arrived at depot") ||
        lower.Contains("departed") ||
        lower.Contains("processed at") ||
        lower.Contains("handed over") ||
        lower.Contains("on its way") ||
        lower.Contains("out for delivery") ||
        lower.Contains("delivery in progress"))
    {
        return "In Transit";
    }

    if (lower.Contains("electronic notification") ||
        lower.Contains("order data provided") ||
        lower.Contains("shipment information received") ||
        lower.Contains("label created") ||
        lower.Contains("pre-advice"))
    {
        return "Pending";
    }

    return "Pending";
}
                

15. Convert the Normalized Status into UI Progress

The live integration maps Delivered to 100, In Transit to 50 and Pending to 20.


private int CalculateProgress(
    string status)
{
    return status
        .ToLowerInvariant() switch
    {
        "delivered" => 100,
        "in transit" => 50,
        "pending" => 20,
        _ => 0
    };
}
                

16. Format the Latest Event Date

The most recent event drives the tracking timestamp when it contains a usable EventTime. The implementation uses DateHelper.TryParseDateTime and falls back to the raw value when parsing is unsuccessful.


string lastUpdate = "—";

if (latestEvent != null &&
    !string.IsNullOrWhiteSpace(
        latestEvent.EventTime))
{
    if (DateHelper.TryParseDateTime(
        latestEvent.EventTime,
        out DateTime dt))
    {
        lastUpdate =
            dt.ToString(
                "dd MMM yyyy, HH:mm");
    }
    else
    {
        lastUpdate =
            latestEvent.EventTime;
    }
}
else if (!string.IsNullOrWhiteSpace(
    detail.LastStatusDate))
{
    if (DateHelper.TryParseDateTime(
        detail.LastStatusDate,
        out DateTime dt))
    {
        lastUpdate =
            dt.ToString(
                "dd MMM yyyy, HH:mm");
    }
    else
    {
        lastUpdate =
            detail.LastStatusDate;
    }
}
                

17. Use the Last Update for Delivered Shipments

For a shipment normalized as Delivered, the source implementation uses the formatted last-update value as EstDelivery. Other states receive the placeholder value —.


int progress =
    CalculateProgress(normalizedStatus);

string estDelivery =
    normalizedStatus == "Delivered"
        ? lastUpdate
        : "—";
                

This reflects the actual application model and should not be described as a separate future delivery estimate returned by the API.


18. Return a Stable TrackingInfo Model

Once the current event, status, progress and date have been resolved, the client returns a TrackingInfo object. The carrier-specific detail is preserved in RawData.


return new TrackingInfo
{
    Status = normalizedStatus,
    DisplayStatus = richDetail,
    Progress = progress,
    EstDelivery = estDelivery,
    LastUpdate = lastUpdate,
    RawData = detail
};
                

19. Handle Asendia No-Tracking Data Explicitly

The source has a dedicated result for situations where the response does not contain tracking information or the tracking number appears in data_not_found.


private TrackingInfo CreateNoTrackingDataInfo()
{
    return new TrackingInfo
    {
        Status = "No Tracking Data",
        DisplayStatus = "No Tracking Data",
        Progress = 0,
        EstDelivery = "—",
        LastUpdate = "—"
    };
}
                

This is intentionally different from an integration exception. The API responded, but the application did not receive usable tracking data.


20. Return a Predictable Error Result

Unexpected failures and explicit API errors are converted into a stable Exception result.


private TrackingInfo CreateErrorTrackingInfo(
    string errorMessage)
{
    return new TrackingInfo
    {
        Status = "Exception",
        DisplayStatus = errorMessage,
        Progress = 0,
        EstDelivery = "—",
        LastUpdate = errorMessage
    };
}
                

21. Keep Carrier DTOs Inside the Integration Boundary

The live client uses carrier-specific DTOs such as TrackingBulkRequest, TokenResponse, TrackingBulkResponse, TrackingDetail and EventDetail. These types keep the Asendia JSON contract isolated from the broader application model.


private class TrackingBulkRequest
{
    public string[] trackingIds { get; set; }
}

private class TokenResponse
{
    public string access_token { get; set; }
    public int expires_in { get; set; }
}

private class TrackingBulkResponse
{
    public List<TrackingDetail> data { get; set; }
    public List<string> data_not_found { get; set; }
    public string message { get; set; }
}
                

22. Protect Asendia Credentials and Customer Information

The live implementation depends on an AsendiaCredentials configuration object containing an API endpoint, token URL, access key, API key, client ID and client secret. Those values are intentionally not reproduced here. They should remain in secure configuration or a managed secret store.

The same rule applies to customer identifiers, carrier account information and environment-specific endpoint details. The article focuses on the integration design rather than exposing operational secrets.


23. Propagate Cancellation to Asendia Requests

The live code accepts a CancellationToken, passes it to the token request and passes it again to the tracking HTTP request. That gives the calling application a way to cancel network operations when the surrounding request is no longer needed.


await GetAccessTokenAsync(
    cancellationToken);

await _httpClient.SendAsync(
    request,
    cancellationToken)
    .ConfigureAwait(false);
                

24. Use Asendia as One Carrier in a Multi-Carrier Architecture

Asendia should remain isolated as one carrier implementation while the application layer provides a consistent tracking workflow for UPS, DHL, DPD and Asendia.


ITrackingClient
      │
      ├── UPS Tracking Client
      ├── DHL Tracking Client
      ├── DPD Tracking Client
      └── Asendia Tracking Client
                

The individual carrier implementations can use different protocols, authentication flows and response models while the rest of the application works with a normalized tracking result.


25. Measure OAuth, API and Response Mapping Separately

A slow tracking screen can have several causes. Measure token acquisition, the Asendia API call, JSON deserialization, event sorting and status mapping independently rather than treating the whole operation as a single timing.


OAuth token:          measure separately
Asendia API request:  measure separately
JSON deserialization: measure separately
Event ordering:       measure separately
Status mapping:       measure separately
                

26. Be Deliberate About Tracking Caching

Unlike the UPS and DHL examples in this content series, the supplied Asendia tracking client does not use IMemoryCache. It requests a fresh access token and tracking response through its current implementation. That is an important distinction: do not document caching as part of the live Asendia behavior unless it is actually added to the implementation.

A future caching layer could be introduced according to tracking-freshness requirements, but that would be an architectural enhancement rather than a description of the supplied code.


27. Test Asendia Tracking at Multiple Levels

  • Test tracking-number trimming and trailing-comma removal.
  • Test successful OAuth token acquisition and token failures.
  • Test JSON request creation and required request headers.
  • Test unsuccessful HTTP responses.
  • Test responses with populated data.
  • Test data_not_found responses and empty responses.
  • Test latest-event selection by EventTime.
  • Test status normalization for every known phrase and unknown values.
  • Test valid and invalid EventTime values.
  • Test the mapping from TrackingInfo into the application's common tracking model.

28. The Complete Asendia Tracking Flow

The live implementation can be summarized as the following request lifecycle:


1. Receive tracking number
        ↓
2. Trim and normalize tracking number
        ↓
3. Obtain OAuth access token
        ↓
4. Build TrackingBulkRequest
        ↓
5. Serialize JSON request
        ↓
6. Create POST HttpRequestMessage
        ↓
7. Add access_key and x-api-key
        ↓
8. Add Bearer authorization
        ↓
9. Send request with CancellationToken
        ↓
10. Read response JSON
        ↓
11. Validate HTTP status
        ↓
12. Deserialize TrackingBulkResponse
        ↓
13. Handle data / data_not_found
        ↓
14. Find latest event by EventTime
        ↓
15. Build rich display status
        ↓
16. Normalize status
        ↓
17. Calculate progress
        ↓
18. Format last update
        ↓
19. Return TrackingInfo
                

29. Asendia Tracking API Integration Checklist

  • Is the carrier integration isolated behind IAsendiaTrackingClient?
  • Are OAuth client credentials kept outside public source?
  • Is the client-credentials token flow handled separately?
  • Is the tracking number normalized before the request?
  • Is the bulk JSON request built from trackingIds?
  • Are access_key, x-api-key and Bearer authentication sent correctly?
  • Are unsuccessful HTTP responses handled explicitly?
  • Are data and data_not_found distinguished?
  • Is the latest event determined by EventTime?
  • Are event labels, descriptions, locations and timestamps handled safely?
  • Are statuses normalized into application-level states?
  • Are no-data and exception results kept separate?
  • Is the current implementation documented accurately, without claiming caching that is not present?
  • Can Asendia coexist with UPS, DHL and DPD behind the same tracking workflow?

30. Conclusion

A production Asendia Tracking API integration in C# requires careful handling of OAuth authentication, JSON request construction, carrier headers, HTTP failures, response deserialization, event selection, status normalization and stable application-level results.

The live ShippingPortal implementation keeps those concerns inside AsendiaTrackingClient, uses a separate credential configuration object, obtains an OAuth token through the client-credentials flow, sends a bulk tracking request, selects the most recent event by EventTime and maps Asendia-specific status wording into a small application-level state model.

That architecture allows Asendia to fit cleanly alongside UPS, DHL and DPD integrations without forcing the broader shipping application to understand every carrier's API contract.


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.