UPS Tracking & .NET Shipping Integration

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

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

August 2026 • 14 min read • By Kshitij Mehta

UPS Tracking API integration is a common requirement in shipping software, logistics platforms and enterprise order management systems. The difficult part is not sending one HTTP request. A production tracking service must authenticate securely, handle cancellation, normalize carrier responses, cache short-lived results, log failures and fit cleanly into a broader multi-carrier architecture.

The implementation behind ShippingPortal provides a practical example. The live UPS tracking client uses ASP.NET Core infrastructure such as IHttpClientFactory, IMemoryCache, ILogger, IConfiguration and an injected UPS token provider. It retrieves UPS API settings through a repository, obtains an OAuth token, calls the UPS Tracking API, maps the response into a standard application result and caches the result for 15 minutes.

That architecture is more useful than a simple code snippet because it shows where UPS API Integration belongs inside enterprise shipping software.

Key Takeaways
  • Keep UPS authentication separate from tracking logic.
  • Use HttpClientFactory for the outbound carrier request.
  • Cache short-lived tracking results to reduce repeat calls.
  • Map UPS-specific responses into a carrier-neutral model.
  • Handle API errors and parsing failures without leaking carrier details to users.
  • Design the UPS implementation so it can coexist with DHL, DPD and Asendia integrations.

1. Where UPS Tracking Fits in a Shipping Platform

A tracking request should pass through an application boundary rather than allowing a controller or UI component to construct a UPS HTTP request directly.


ShippingPortal
      ↓
Tracking Service
      ↓
IUpsTrackingClient
      ↓
UPS Authentication
      ↓
UPS Tracking API
      ↓
UPS Response DTO
      ↓
Standard TrackingResult
      ↓
Shipping Portal UI / API
                

The implementation uses this separation: the tracking client is injected with authentication, logging, configuration, caching and repository dependencies instead of constructing those services internally.


2. Create a Dedicated UPS Tracking Client

A dedicated client keeps carrier-specific HTTP behavior separate from shipment business logic.


public class UPSTrackingClient : IUpsTrackingClient
{
    private readonly IHttpClientFactory _httpClientFactory;
    private readonly IMemoryCache _cache;
    private readonly ILogger<UPSTrackingClient> _logger;
    private readonly IConfiguration _configuration;
    private readonly IUpsTokenProvider _authService;
}
                

The implementation follows this pattern and also injects a delivery-service repository for UPS settings.


3. Validate the Tracking Number Before Calling UPS

Do not make an external API request when the tracking number is empty. The live client checks the input immediately and returns a standard error result.


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

Early validation reduces unnecessary network traffic and makes the service behavior predictable.


4. Cache UPS Tracking Results for Short-Lived Lookups

Tracking pages are often refreshed repeatedly. Calling the carrier for exactly the same tracking number on every refresh can create unnecessary external traffic.


var cacheKey =
    $"UPS_Track_{trackingNumber}";

if (_cache.TryGetValue(
        cacheKey,
        out UpsTrackingResult cached))
{
    return cached;
}
                

The ShippingPortal client stores the UPS tracking result for 15 minutes after a successful API call.

The appropriate duration depends on the business need for freshness. Caching should never turn into a blanket rule where shipment status is stale for too long.


5. Keep UPS API Configuration Outside the Tracking Method

The live implementation retrieves UPS settings from the delivery-service repository and falls back to application configuration when a database value is unavailable.


var settings =
    await _deliveryServiceRepository
        .GetUpsApiSettingsByContractAsync("");

string merchantId =
    settings?.MerchantID
        ?? _configuration["UPS:MerchantID"];

string authorization =
    settings?.Authorization
        ?? _configuration["UPS:Authorization"];

string shipperNumber =
    settings?.ShipperNumber
        ?? _configuration["UPS:ShipperNumber"];
                

This keeps deployment-specific configuration outside the business workflow and allows enterprise environments to store carrier settings through a controlled configuration source.


6. Obtain the UPS OAuth Access Token Through a Separate Service

Authentication should not be embedded inside the method that builds the tracking request. ShippingPortal delegates token acquisition to IUpsTokenProvider.


string accessToken =
    await _authService.GetTokenAsync(
        merchantId,
        authorization);
                

This keeps OAuth concerns reusable and makes tracking logic easier to test independently.


7. Build the UPS Tracking API Request Explicitly

The production client uses the UPS Tracking API endpoint and passes the tracking number as part of the request URL.


var requestUrl =
    $"{UpsTrackingBaseUrl}" +
    $"{trackingNumber}" +
    "?locale=en_US" +
    "&returnSignature=false" +
    "&returnMilestones=false" +
    "&returnPOD=false";
                

The implementation defines the base UPS tracking endpoint separately and creates an HttpRequestMessage for the call.


8. Send Carrier Headers Through HttpRequestMessage

The live client adds the Bearer token and carrier-specific request headers before sending the request through the configured UPS HttpClient.


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

request.Headers.Add(
    "transId",
    Guid.NewGuid().ToString("N"));

request.Headers.Add(
    "transactionSrc",
    transactionSource);

request.Headers.Add(
    "ShipperNumber",
    shipperNumber);
                

Keeping request creation in one carrier client makes the integration easier to troubleshoot and keeps carrier details out of the rest of ShippingPortal.


9. Use IHttpClientFactory for UPS API Integration

The production implementation calls _httpClientFactory.CreateClient("UPS") rather than constructing a new HttpClient for every request.


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

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

This keeps HTTP client configuration in the application composition layer and allows the UPS client to share reliable HTTP infrastructure.


10. Propagate Cancellation to the UPS Tracking Request

The tracking method accepts a CancellationToken and passes it to the outbound HTTP operation.


var trackingData =
    await FetchTrackingFromApiAsync(
        trackingNumber,
        accessToken,
        shipperNumber,
        cancellationToken);
                

Cancellation is important for ASP.NET Core APIs and other user-facing applications where the caller may disconnect before the carrier response arrives.


11. Treat Non-Success UPS Responses as Integration Failures

An HTTP request can complete while the carrier still returns an unsuccessful status code. The live implementation checks IsSuccessStatusCode, logs the response and converts the condition into an exception handled by the outer workflow.


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

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

12. Deserialize the UPS Response into Strong DTOs

The live client deserializes the JSON response into a strongly typed TrackingResponseRoot before mapping carrier data into the application's result model.


var trackingResponse =
    JsonConvert.DeserializeObject<TrackingResponseRoot>(
        jsonResponse);

return MapToUpsTrackingResult(
    trackingResponse,
    trackingNumber);
                

Strong DTOs isolate the rest of the application from the exact JSON shape returned by UPS.


13. Normalize UPS Tracking Statuses into a Standard Model

A multi-carrier platform should not expose every carrier's wording directly to the user interface. ShippingPortal maps UPS status descriptions into standard application states.


private string MapUpsStatusToStandard(
    string upsStatus)
{
    if (string.IsNullOrEmpty(upsStatus))
        return "Pending";

    var s =
        upsStatus.ToLowerInvariant();

    if (s.Contains("delivered"))
        return "Delivered";

    if (s.Contains("out for delivery") ||
        s.Contains("in transit"))
        return "In Transit";

    if (s.Contains("exception") ||
        s.Contains("return"))
        return "Exception";

    return "Pending";
}
                

The implementation uses the same normalization concept so downstream workflows can reason about Delivered, In Transit, Pending and Exception without depending on carrier-specific strings.


14. Convert Tracking Status into UI-Friendly Progress

A shipping application can provide a normalized progress value for dashboards and shipment detail pages.


private int DetermineProgress(
    string status)
{
    return status switch
    {
        "Delivered" => 100,
        "In Transit" => 50,
        "Pending" => 10,
        _ => 0
    };
}
                

ShippingPortal uses this pattern to convert the standard tracking state into a simple progress indicator.


15. Use the Latest UPS Activity for Current Shipment State

The tracking response may contain a sequence of package activities. The implementation selects the first activity as the latest event and uses it to populate the current display status, normalized status, progress and last-update information.


var activities =
    package.activity;

var latestActivity =
    activities[0];

result.DisplayStatus =
    latestActivity.status?.description
        ?? "Unknown";

result.Status =
    MapUpsStatusToStandard(
        result.DisplayStatus);
                

16. Build a Tracking Event History

A tracking page is more useful when it exposes the shipment timeline instead of only the latest status.


foreach (var act in activities)
{
    var evt = new UpsTrackingEvent
    {
        Description =
            act.status?.description ?? "",

        Location =
            act.location?.address?.city ?? "",

        Timestamp =
            ParseDateTime(
                act.gmtDate,
                act.gmtTime),

        IsCurrent =
            act == latestActivity
    };

    result.Events.Add(evt);
}
                

The implementation builds this event history from the UPS activity collection.


17. Parse Carrier Dates in One Place

Carrier APIs frequently return date and time values in formats that do not match the application's display model. ShippingPortal isolates that conversion in helper methods.


private DateTime? ParseDateTime(
    string gmtDate,
    string gmtTime)
{
    if (string.IsNullOrEmpty(gmtDate) ||
        gmtDate.Length != 8)
        return null;

    // Parse the carrier date format
    // and combine it with the time.
}
                

Centralizing the conversion prevents date-format logic from spreading through the tracking workflow.


18. Extract Estimated Delivery Without Making It Mandatory

Tracking responses do not always contain a usable estimated delivery date. The live implementation checks the available delivery date and falls back to the default result when it cannot be parsed.

Optional carrier fields should therefore never cause the complete tracking operation to fail simply because the field is absent.


19. Normalize the Latest Tracking Location

UPS activity can contain address information. ShippingPortal converts that data into a simpler country-city representation for the application.


var country =
    location.address.country
    ?? location.address.countryCode
    ?? "";

var city =
    location.address.city
    ?? "";

result.LastLocation =
    string.IsNullOrEmpty(country)
        ? city
        : $"{country}-{city}".Trim('-');
                

The application can therefore display a consistent location without exposing the complete carrier-specific response structure.


20. Fail Safely When UPS Tracking Fails

A tracking service should return a predictable application result when the carrier is unavailable or an unexpected response is received.


catch (Exception ex)
{
    _logger.LogError(
        ex,
        "UPS tracking failed for {TrackingNumber}",
        trackingNumber);

    return CreateErrorResult(
        "Tracking service temporarily unavailable.");
}
                

The implementation logs the exception and returns a standard error result rather than allowing carrier-specific exceptions to leak through the application boundary.


21. Separate HTTP Failures from Response-Parsing Failures

A valid HTTP response can still contain unexpected data. ShippingPortal handles mapping/parsing errors separately and converts them into an Exception state with a safe display message.


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

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

22. Keep Shipment Data Access Separate from Carrier HTTP Calls

UPS tracking is only one part of a shipment workflow. The application may need to load a shipment from SQL Server, obtain the tracking number, call UPS and persist or present the resulting state.


SQL Server / Dapper
        ↓
Shipment
        ↓
Tracking Service
        ↓
UPS Tracking Client
        ↓
UPS API
        ↓
Standard Tracking Result
        ↓
UI / API
                

This separation prevents SQL Server concerns from leaking into the carrier client and makes each layer easier to benchmark.


23. Measure UPS API Latency Separately from SQL Server

A slow tracking screen does not necessarily mean that the database query is slow. The external UPS call may dominate the response time.


Shipment lookup:       40 ms
UPS OAuth:             90 ms
UPS tracking request: 420 ms
Response mapping:       3 ms

Total:                ~553 ms
                

Separate measurements make performance tuning much more precise and help decide whether caching, SQL optimization or carrier-side resilience is the appropriate improvement.


24. Add Resilience Without Hiding Persistent Carrier Failures

Timeouts, transient network failures and temporary carrier errors should be observable. Retry behavior can be useful for safe transient failures, but retries should be bounded and should not turn one request into a long blocking chain.

The production client already separates API failure logging from its standard error result, which provides a useful foundation for adding controlled resilience policies.


25. Build UPS Integration as Part of a Multi-Carrier Architecture

ShippingPortal is designed around multiple carrier integrations. UPS should therefore implement a common tracking capability instead of defining the entire tracking model.


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

This makes the same application-level tracking workflow reusable across UPS API Integration, DHL API Integration, DPD API Integration and Asendia API Integration.


26. Why UPS Tracking Works Well with Dapper-Based Shipping Software

Dapper can retrieve the small amount of shipment data needed to invoke the carrier, while the carrier client owns the external HTTP operation.


var shipment =
    await connection.QuerySingleOrDefaultAsync<ShipmentSummary>(
        """
        SELECT
            ID,
            TrackingNo,
            CourierID
        FROM tblOrders
        WHERE ID = @OrderId
        """,
        new
        {
            OrderId = orderId
        });

var tracking =
    await upsTrackingClient.GetTrackingAsync(
        shipment.TrackingNo,
        cancellationToken);
                

This keeps the database query focused and lets Dapper remain a lightweight data-access choice for measured high-performance shipping workloads.


27. Expose UPS Tracking Through an ASP.NET Core API

The public API should depend on the tracking service rather than directly constructing UPS requests.


[HttpGet("tracking/{trackingNumber}")]
public async Task<ActionResult<TrackingResult>> Track(
    string trackingNumber,
    CancellationToken cancellationToken)
{
    var result =
        await trackingService.TrackAsync(
            trackingNumber,
            cancellationToken);

    return Ok(result);
}
                

This separates the HTTP contract from carrier-specific implementation details.


28. Protect UPS Credentials and Sensitive Configuration

Carrier credentials, authorization values and other secrets should come from a secure configuration mechanism. They should not be hard-coded into controllers, source examples or public documentation.

The live implementation reads UPS configuration through its repository/configuration abstraction and passes only the required values to the token provider.


29. Use Structured Logging for UPS Integration Failures

Carrier integrations need enough information to diagnose a failed request without logging secrets or unnecessary customer information.


_logger.LogError(
    ex,
    "UPS tracking failed for {TrackingNumber}",
    trackingNumber);
                

The implementation follows this structured logging approach for both transport failures and response parsing failures.


30. Test UPS Tracking Integration at Multiple Levels

  • Unit-test UPS status normalization and date parsing.
  • Test the tracking service with a fake carrier client.
  • Test authentication failure behavior.
  • Test non-success HTTP responses.
  • Test malformed or incomplete carrier JSON.
  • Run controlled integration tests against the carrier environment.

The most important business behavior to protect is the conversion from carrier-specific data into the application's standard tracking result.


31. The Complete ShippingPortal UPS Tracking Flow

Putting the pieces together, the live implementation follows a clean sequence.


1. Validate tracking number
        ↓
2. Check 15-minute cache
        ↓
3. Load UPS settings
        ↓
4. Obtain OAuth token
        ↓
5. Build UPS Tracking API request
        ↓
6. Send through named HttpClient
        ↓
7. Validate HTTP response
        ↓
8. Deserialize UPS JSON
        ↓
9. Map package/activity data
        ↓
10. Normalize status and progress
        ↓
11. Build event history
        ↓
12. Cache result
        ↓
13. Return standard TrackingResult
                

That sequence is implemented directly in the client: cache lookup, configuration retrieval, token acquisition, API request, response mapping and caching.


UPS Tracking API Integration Checklist

  • Is UPS authentication isolated from shipment business logic?
  • Is the HTTP client configured through IHttpClientFactory?
  • Are tracking requests cancellable?
  • Are short-lived tracking results cached appropriately?
  • Are UPS-specific responses mapped into a standard model?
  • Are HTTP errors and JSON parsing failures handled separately?
  • Are carrier credentials protected and kept outside source code?
  • Are SQL Server and carrier API timings measured independently?
  • Can the UPS client coexist with DHL, DPD and Asendia integrations?
  • Have status mapping, date parsing and failure paths been tested?

Conclusion

A production UPS Tracking API integration in C# requires more than a successful HTTP request. Authentication, cancellation, caching, structured logging, response mapping, error handling and application boundaries all contribute to a reliable tracking service.

The ShippingPortal implementation demonstrates a practical pattern: keep UPS-specific HTTP and DTO logic inside a dedicated client, obtain authentication through a separate service, use ASP.NET Core infrastructure such as HttpClientFactory and MemoryCache, and return a carrier-neutral tracking result to the rest of the platform.

Most importantly, the same architecture can support a broader multi-carrier shipping platform. UPS API Integration can coexist with DHL API Integration, DPD API Integration and Asendia API Integration without forcing application workflows to understand each carrier's response format.


About the Author

I'm Kshitij Mehta, a Senior .NET Developer and ASP.NET Core Consultant with over 14 years of 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.