Migrating UPS API from XML to OAuth 2.0 REST

A step-by-step guide to migrating multi-tenant UPS integrations from XML/Access Key to OAuth 2.0 REST, with token-caching to avoid rate limits.

Migrating UPS API from XML to OAuth 2.0 REST

Why this migration can't wait

If your middleware still authenticates against UPS with an access key, a user ID, a password and an XML payload, you are running on borrowed time. On June 5, 2023 UPS began the sunset process for access key authentication in favor of the more secure OAuth 2.0 model for all APIs, a change that impacts all API integrations and requires updates to your existing application interface. Vendor documentation differs slightly on the final hard cut-off — DeftShip's integration guide states that starting June 3, 2024, all API transactions with UPS require OAuth 2.0 authentication, with the existing access-key authorization deprecated and any calls using the old method no longer working, while other trackers list access keys no longer being supported for authentication from August 5, 2024. Either way, the direction is the same: XML plus access key is dead weight.

The architectural framing matters more than the exact date. UPS has been winding down XML access in favor of REST APIs with OAuth 2.0, and the old model — Access Key, User ID, Password, and XML payloads over HTTP — is what most legacy carrier adapters were built against. The practical symptom is ugly and hard to diagnose in production: that stack is increasingly unreliable, and when it fails, checkout shows no UPS options with little useful error detail. In a single-tenant shop that's a bad afternoon. In a multi-tenant carrier middleware serving hundreds of shippers off one UPS adapter, it's an incident with a blast radius you'd rather not explain to a client at 9am.

This guide walks through the migration path for a multi-tenant platform specifically — where "one client ID and secret per integration" becomes "one client ID and secret per tenant, at scale," and where token handling has to be engineered, not bolted on.

What you need before starting

You need a UPS Developer Portal account with REST app credentials, not a legacy access key, plus a place to store per-tenant secrets and a staging environment to run old and new in parallel.

  • A UPS Developer Portal account per tenant contract, generating a Client ID and Client Secret through the new application flow rather than the old access-key request form.
  • Clarity on which UPS products each tenant actually calls. UPS now expects new integrations on the Developer Portal using REST endpoints for Rating, Shipping and Tracking as separate APIs, authenticated with OAuth 2.0 client credentials and short-lived access tokens — this is a structural change from one XML payload doing everything.
  • A secrets store (Vault, AWS Secrets Manager, or similar) addressable by tenant ID, because each shipper account now maps to its own Client ID/Secret pair rather than a shared access key.
  • A staging environment where you can run XML and REST calls against the same basket of test shipments before you touch production traffic for any tenant.

Step-by-step migration

The core of the work is mechanical but unforgiving of shortcuts, especially the token handling step. Treat each step as a gate — don't move a tenant cohort forward until the previous step is verified.

  1. Audit every tenant still calling XML endpoints. Tag by UPS account number, not by API key, since one tenant can hold multiple UPS accounts and one access key can (badly) span several.
  2. Register a REST app per tenant in the UPS Developer Portal, selecting the Rating, Shipping and Tracking products the tenant actually uses, and write the resulting Client ID and Client Secret into that tenant's secrets namespace immediately — never in a shared config file.
  3. Implement the client-credentials grant. Make a call to the OAuth endpoint with your client ID and client secret, and the OAuth server responds with a bearer token that you then place in the Authorization HTTP header as "Bearer <token>" on subsequent API calls. Do this against the sandbox token endpoint first, then the production equivalent, before any tenant traffic touches it.
  4. Cache the token per tenant, keyed by tenant ID and scope, with a TTL-aware refresh rather than requesting a fresh token on every rate or shipment call. This is the step most single-store migration guides skip, because they only have one credential pair to manage. At 500 tenants it's the difference between a working system and a self-inflicted outage (more on that below).
  5. Map legacy XML request/response fields to the new JSON schemas. RateRequest and ShipmentRequest structures don't translate one-to-one to the REST Rating and Shipping payloads — expect to rewrite your adapter's serialisation layer, not just swap the transport.
  6. Run a test matrix in staging — light parcel, heavy parcel, residential and commercial destination — against XML (while it still works) and REST in parallel, comparing rate quotes and label output field by field.
  7. Cut over tenants in cohorts. Start with low-volume, low-risk tenants and monitor 401 and 429 rates per tenant before expanding the cohort. A spike in either is your signal to pause, not push through.
  8. Decommission XML credentials per tenant only after a full billing cycle of clean REST traffic, so you catch month-end reconciliation edge cases before you burn the bridge back.

On the authentication model itself, it's worth noting what UPS is actually trying to fix. UPS describes OAuth as giving customers control over which of their shipping accounts are accessible to a given application — a scoping model that access keys never really had. For a multi-tenant platform this is a genuine improvement: you can request exactly the account access a tenant contract calls for, rather than a flat key that could theoretically reach further than intended.

Failure mode: token thrashing under multi-tenant load

The single most common way this migration goes wrong is treating the OAuth token endpoint like a stateless pass-through: fetch a fresh token on every rate-shopping call because it's simpler to code. It works fine in a demo with one tenant. It falls over once you have hundreds of tenants firing rate requests during a peak sales window, because every one of those "fresh token" calls counts against UPS's rate limits on the token endpoint itself, on top of the actual API calls.

The fix isn't exotic. Look for OAuth token caching with automatic refresh before expiry as a first-class feature of your adapter, not an afterthought. Concretely:

  • Cache tokens keyed by tenant_id + scope, not globally — a shared token cache across tenants breaks the account-scoping benefit OAuth was meant to deliver.
  • Refresh just before expiry rather than on 401, so a legitimate rate-shopping burst never has to wait on a synchronous token round-trip.
  • Add jitter to refresh timing across tenants sharing infrastructure, so you don't get a thundering herd of simultaneous refresh calls at the top of every hour.
  • Wrap the token endpoint itself in the same circuit-breaker and backoff-on-429 pattern you'd already apply to the Rating or Shipping API, so one noisy tenant can't exhaust a shared token budget and take every other tenant's checkout down with it.

This is the same idempotency-and-isolation discipline that applies to webhook delivery or retry queues elsewhere in your platform — the token endpoint is just another dependency that needs a blast-radius boundary per tenant.

How you know it worked

You'll know the migration is done, not just started, when three things are simultaneously true. Zero XML calls appear in your access logs for a full 30 days across all tenants. Your per-tenant OAuth error budget — the 401 and 429 rate on token and API calls — sits inside whatever SLO you've set for carrier connectivity. And the Rating, Shipping and Tracking parity tests you built for the cutover still pass when run against the same basket matrix used before migration, with no silent field drift between XML and REST responses.

If any of those three slips, don't decommission the next cohort's XML credentials yet. Roll back that cohort, not the whole platform — this is exactly why cohort-based cutover in step 7 matters more than it looks like on paper.

Where UPS sits among carrier integrations

UPS isn't alone in forcing this kind of rework — FedEx and DHL Paket have run comparable SOAP-to-REST and token-model transitions on their own timelines, and every carrier integration platform has had to absorb the churn somewhere. Platforms that sit between shippers and carriers document the same mechanical swap for their own customers. nShift's migration notes for its customers describe entering the Client Secret in the UPS Password field, with the old UPS Access Token field deprecated and legacy values simply removable. That's the same swap you're doing in your own adapter, just exposed through a different admin UI.

If you're building this yourself, you're re-implementing what several multi-carrier platforms already centralise: nShift, EasyPost, ShipEngine, ProShip, Shipmondo and Cargoson all abstract carrier auth churn behind their own adapter layer, so an individual shipper doesn't have to re-run a UPS OAuth migration themselves every time a carrier changes its security model. Whether that trade-off makes sense depends on how much of your value is the carrier connectivity layer itself versus what you build on top of it — but it's worth knowing the option exists before you commit engineering months to owning this surface area directly.

PlatformUPS auth abstractionMulti-tenant credential isolationSource
nShiftClient ID/Secret fields replace legacy access token in carrier configNot publishednShift Help Center
EasyPostNot publishedNot published
ShipEngineNot publishedNot published
ProShipNot publishedNot published
ShipmondoNot publishedNot published
CargosonNot publishedNot published

Whichever route you take, don't treat this as a credential swap. It's a rewrite of your UPS adapter's authentication and serialisation layers, and at multi-tenant scale the token caching design in step 4 is the part that decides whether your cutover is boring or an incident report.