FedEx SOAP to REST API Migration, Step by Step
A step-by-step FedEx REST API migration guide: OAuth2 setup, field mapping, idempotent cutover, and dual-run verification for multi-tenant middleware.
FedEx gave the industry a hard stop this year: Compatible Providers had to complete upgrades by 31 March 2026, and end customers by 1 June 2026. If you're running a multi-tenant middleware platform with FedEx Rate and Ship calls still going through Web Services, this FedEx REST API migration is not optional homework anymore, it's the thing standing between your tenants and working label generation. This is the build log we wish we'd had: OAuth2 token handling, WSDL-to-JSON field remapping, dual-run verification, and a cutover sequence that doesn't create duplicate shipments across a thousand tenants.
Before You Start
You need three things before touching code: a project on the FedEx Developer Portal with sandbox credentials, a full inventory of every SOAP call site in your codebase, and a rollback plan that doesn't involve praying. This walkthrough covers Rate and Ship operations specifically. Track and Address Validation follow the same OAuth2-and-remap pattern, but the payload shapes differ enough that we're not covering them step by step here.
One scoping note before you start: FedEx Address Validation was migrated separately in 2025, so if your platform already handled that cutover, don't assume it tells you anything about Rate and Ship timing.
Implement the OAuth2 client-credentials exchange
The FedEx APIs support the OAuth 2.0 bearer token authentication method to authorize your application API requests with FedEx resources. The token request is a plain POST:
POST /oauth/token HTTP/1.1
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials&client_id={CLIENT_ID}&client_secret={CLIENT_SECRET}The response gives you a bearer token, a type, and a lifetime:
{
"access_token": "eyJhbGciOiJIUzI1NiIs...",
"token_type": "bearer",
"expires_in": 3600,
"scope": "CXS"
}This OAuth access token needs to be regenerated after every 60 minutes and provided with each API transaction to authenticate and authorize your access to the FedEx resources. Don't fetch a token per request. Cache it against the tenant's client credentials pair, keyed by tenant ID plus grant type, and refresh five minutes before the 3,600-second expiry rather than waiting for a 401. That five-minute margin is what saves you from the failure mode in the section below.
Register a project per API product on the FedEx Developer Portal
Create separate projects for Rate and Ship rather than one monolithic project, so scopes and rate limits are visible per capability. FedEx issues a Client ID and Client Secret at project creation, which you view on the Project Overview page. If your middleware serves FedEx accounts on behalf of tenants rather than under your own contract, note that Compatible Providers, now called Integrator Providers on the new platform, use a csp_credentials grant type with additional child_key and child_secret parameters to manage access on behalf of their customers' FedEx accounts. That's a different token flow from plain client_credentials, and it changes your token cache key design (see Step 3).
Audit every SOAP call site, per tenant and per adapter
Grep your codebase for RateRequest and ProcessShipmentRequest constructors. In a multi-tenant platform, the same logical operation often has three or four code paths depending on which tenant onboarding era wrote it. This is the step that gets skipped, and it's the step that bites you in week three when a tenant on a two-year-old adapter starts throwing SOAP faults nobody remembers how to read. Build a spreadsheet: tenant ID, adapter version, which SOAP operations it calls, and which fields it currently sends. You'll use this same list as your test matrix later.
Mapping Fields from WSDL to JSON
The field remap is mechanical but not trivial, because FedEx's REST schema flattens and renames a lot of what SOAP nested. For rate quotes, your legacy RateRequest becomes a POST to /rate/v1/rates/quotes. A minimal before/after for the shipper block:
// SOAP: RequestedShipment.Shipper
<Shipper>
<Address>
<StreetLines>123 Prinsengracht</StreetLines>
<City>Amsterdam</City>
<PostalCode>1015LB</PostalCode>
<CountryCode>NL</CountryCode>
</Address>
</Shipper>
// REST: requestedShipment.shipper
"shipper": {
"address": {
"streetLines": ["123 Prinsengracht"],
"city": "Amsterdam",
"postalCode": "1015LB",
"countryCode": "NL"
}
}Package line items follow the same pattern, moving from RequestedPackageLineItems nodes to a requestedPackageLineItems array with weight.value, weight.units, and dimensions as plain JSON objects instead of typed XML elements. The shipment creation call, POST /ship/v1/shipments, adds a labelResponseOptions and shippingChargesPayment block that has no direct SOAP equivalent; you're not just renaming fields, you're adding ones your old wrapper never had to think about.
Here's the caveat that catches teams out: the Rate API returns rate for the origin and destination for the requested service and will not validate whether that service is available for your ship date as well as origin and destination. If your SOAP wrapper quietly did that validation for you (some do, via side-channel logic), that check needs a new home, either in your adapter layer or as an explicit call to a service-availability endpoint before you trust a quote.
Building Client-Side Idempotency Before You Touch Ship
FedEx's transaction ID exists for tracing a request through their systems, not for deduplicating shipment creation on your side. If a tenant's retry logic or a flaky connection resends POST /ship/v1/shipments, FedEx will happily generate a second label and a second tracking number. Before you wire up REST shipment creation for any tenant, build an idempotency table keyed on tenant_id + order_id, check it before every ship call, and store the resulting tracking number and label reference against that key. This is the same envelope-versus-payload discipline we've written about elsewhere on this blog: the idempotency key lives in your envelope, not in FedEx's payload, because FedEx's schema was never designed to carry your deduplication semantics.
Dual-Run, Then Cut Over Tenant by Tenant
Don't flip write traffic in one move. Run REST in shadow mode first.
- Shadow mode. Add a per-tenant feature flag that mirrors live SOAP rate and ship traffic into the equivalent REST call, without letting REST results reach the tenant. Diff rate quotes for variance beyond your existing tolerance band, and diff label metadata (service code, tracking number format, dimensional weight) field by field.
- Cutover. Once a tenant's shadow diff is clean for a representative volume, flip write traffic for that tenant only. Keep the SOAP credentials live and untouched for a defined rollback window, don't decommission them the day you cut over. Only retire SOAP credentials once you've confirmed a full billing cycle of REST-only traffic for that tenant.
You're not alone in running this sequence simultaneously across carriers. Platforms serving many carriers, including nShift, ShipEngine, EasyPost, and Cargoson, all had to run some version of this exact shadow-then-cutover playbook this year, because FedEx's REST push landed in the same window as UPS's own move away from access-key authentication.
How You Know It Worked
Check these before declaring a tenant migrated, not after:
- The Ship response returns a valid label URL or base64-encoded label image, not just a 200 status.
- The tracking number format matches what your downstream tracking adapter expects (FedEx REST tracking numbers follow the same structure as SOAP, but your parsing regex may be stricter than it needs to be).
- Rate quotes for the same shipment sit within your defined tolerance across SOAP and REST for at least a week of live shadow traffic.
- Every FedEx REST error code (400s, 401s, 5xxs) is mapped into your existing retry and dead-letter queue logic, not left to bubble up as an unhandled exception.
Failure Mode: Token Refresh Races Under Multi-Tenant Load
Here's what happens if you get the caching wrong: dozens of tenants sharing a code path hit an expired or near-expired token simultaneously, all fire a refresh request against the same client secret at once, and you get a 401 storm that looks like a FedEx outage but is entirely self-inflicted. The fix is a single-flight refresh: one in-flight token request per tenant credential pair, guarded by a distributed lock, with every other caller blocking on that result rather than firing its own. Add jitter to your pre-expiry refresh window, don't refresh every token at exactly minute 55, and circuit-break on repeated 401s instead of retrying blindly into a carrier that's already telling you your credentials are stale.
The compressed FedEx timeline made this worse than it needed to be. Compatible Providers had to complete upgrades by March 31, 2026, while customers had until June 1, 2026, which gave middleware vendors a two-month window to load-test token refresh under realistic multi-tenant concurrency before their own customers were forced to follow. If you're a Compatible Provider reading this after the fact, that window is gone, so budget extra time for load-testing the refresh path specifically, not just the happy path of a single successful token exchange.
Reusing This Playbook for the Next Carrier
This isn't a one-off FedEx problem. UPS ran essentially the same migration a year and a half earlier: UPS required OAuth API credentials from the new UPS Developer Portal and updated security protocols to OAuth 2.0 prior to June 3, 2024, and any legacy UPS API integrations using XML, SOAP, or legacy JSON payloads were also required to convert to the RESTful APIs. DHL had already standardised on REST earlier still. The pattern is consistent enough across carriers that the adapter shape you build for FedEx today, audit call sites, isolate the OAuth2 exchange behind a cached single-flight refresh, remap fields through a versioned schema layer, shadow-run before cutover, is the same shape you'll reuse for the next carrier deprecation notice that lands in your inbox. Build it once as a reusable pattern in your middleware, not as a FedEx-specific patch, and the next migration costs you a sprint instead of a quarter.