Acumatica · Integration

Acumatica UPS Tracking Integration — A Complete Guide

Acumatica UPS Tracking Integration — A Complete Guide is the kind of integration that pays for itself the first time it runs without intervention.

John Kihiu12 min read

UPS's developer platform (UPS Ready / the newer UPS API suite) is the one I've had the most credential-related support tickets on, because UPS's auth model genuinely changed shape between the legacy XML API and the current OAuth-based REST API, and a lot of documentation still floating around online describes the old one. Here's what actually works today, and the shipment/tracking wiring I use on top of it.

OAuth token plus a separate UPS account number on every call

UPS uses standard OAuth 2.0 client credentials for the bearer token, but unlike FedEx, most UPS endpoints also require your UPS Account Number passed explicitly in a header or the request body, the token alone identifies your app, not which shipper account you're acting on behalf of. Miss this and you get a cryptic 401 that looks like a bad token when the token is actually fine.

C# · UPS OAuth + account header
var tokenReq = new HttpRequestMessage(HttpMethod.Post, "https://onlinetools.ups.com/security/v1/oauth/token")
{
    Headers = { { "x-merchant-id", _settings.MerchantId } },
    Content = new FormUrlEncodedContent(new Dictionary<string, string>
    {
        ["grant_type"] = "client_credentials",
    })
};
tokenReq.Headers.Authorization = new AuthenticationHeaderValue("Basic",
    Convert.ToBase64String(Encoding.UTF8.GetBytes($"{_settings.ClientId}:{_settings.ClientSecret}")));

// Downstream tracking/shipping calls then need BOTH:
trackReq.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
trackReq.Headers.Add("transId", Guid.NewGuid().ToString("N"));   // UPS wants a per-request trace id
trackReq.Headers.Add("transactionSrc", "AcumaticaIntegration");

Two very different integration scopes: label generation vs tracking-only

Clients ask for "UPS integration" meaning two genuinely different things, and I always clarify which up front because the scope differs enormously. Tracking-only means Acumatica never talks to UPS until a shipment already has a tracking number (entered manually or imported from a WMS) and you're just pulling status. Full shipment integration means Acumatica calls UPS's Shipping API to generate the label and tracking number itself, which pulls in package dimensions, weight, service level selection, and, critically, UPS's own rating API if the client wants live freight cost back on the Sales Order before confirming the shipment.

Start with tracking-only even when the client wants full label generation eventually

I default new UPS integrations to tracking-only first, ship it, then layer label generation on top once tracking is proven stable. Label generation involves package dimension data that's often missing or wrong in a client's item master (nobody maintains box dimensions carefully until something forces them to), and debugging "why did UPS reject this shipment request" is much easier once you already trust the auth and tracking plumbing underneath it.

UPS does offer real tracking webhooks, use them over polling

Unlike FedEx, UPS's Tracking API has a genuinely usable webhook subscription (Quantum View / Tracking Notifications) that pushes status changes to a registered HTTPS endpoint. I expose a lightweight ASP.NET endpoint outside the main Acumatica screen pipeline, a simple controller, not a graph action, that receives the UPS payload, verifies it, and writes into a staging table, with a separate scheduled process reconciling staged events into the actual Shipment DAC. Keeping ingestion and reconciliation as two steps means a malformed or unexpected UPS payload never has a chance to throw inside a live PXGraph save.

C# · webhook receiver, minimal and defensive
[HttpPost, Route("api/ups/tracking-webhook")]
public IHttpActionResult ReceiveTrackingEvent([FromBody] UpsTrackingPayload payload)
{
    if (payload?.TrackingNumber == null)
        return BadRequest(); // never let a malformed payload bubble further

    using (var scope = new PXTransactionScope())
    {
        var staging = new UsrUpsTrackingStaging
        {
            TrackingNbr = payload.TrackingNumber,
            StatusCode = payload.Activity?.Status?.Code,
            RawPayload = JsonSerializer.Serialize(payload),
            ReceivedDateTime = DateTime.UtcNow,
        };
        PXCache<UsrUpsTrackingStaging>.Insert(staging);
        scope.Complete();
    }
    return Ok(); // ack fast, UPS retries on non-2xx, don't make it wait on our processing
}

Idempotency: UPS will redeliver the same event

Like most webhook providers, UPS retries on anything but a clean 2xx, and even successful deliveries occasionally duplicate. I dedupe on tracking number plus status code plus event timestamp before writing an update into the actual Shipment record, a unique constraint on the staging table's natural key catches this cheaply rather than needing application-level dedup logic on every insert.

Wrapping up

UPS's OAuth flow needs both a bearer token and an explicit account number on every call, which trips up more integrations than the token itself does. Scope the project honestly as tracking-only versus full label generation before writing code, prefer UPS's real webhook subscription over polling since it actually works well, and always land webhook payloads in a staging table with idempotent dedup before they touch a live Shipment record.

John Kihiu
Acumatica ERP Developer · Laravel Engineer

Independent software engineer in Nairobi specialising in Acumatica customisations, Laravel backends, and tax fiscalisation integrations across East and Southern Africa.