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.
- Keep Asendia transport and authentication inside a dedicated carrier client.
- Use typed credentials and a configured
HttpClientrather 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
EventTimeinstead 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_foundresponses and empty responses. - Test latest-event selection by
EventTime. - Test status normalization for every known phrase and unknown values.
- Test valid and invalid
EventTimevalues. - Test the mapping from
TrackingInfointo 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-keyand Bearer authentication sent correctly? - Are unsuccessful HTTP responses handled explicitly?
- Are
dataanddata_not_founddistinguished? - 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.