Why This Setup Comes First
Every UPS to Salesforce integration starts in the same place, and it is not Salesforce. Before a single Apex callout can request a rate, generate a label, or poll a tracking number, UPS needs to know which application is calling, which shipping account it bills, and which APIs it may touch. All of that is negotiated in the UPS developer portal, and if you get it wrong there, every downstream failure will look like a Salesforce problem when it is not.
UPS forced this migration. New access keys stopped being issued on June 5, 2023, all API transactions required OAuth 2.0 from June 3, 2024, and account cleanup deadlines ran as late as December 31, 2025. Legacy XML and SOAP integrations built on an Access License Number are dead. There is one path now: an OAuth app, a client ID and secret, and the client credentials grant posted to /security/v1/oauth/token.
This is the zoomed-in version of the first step in our full technical guide at cloudsheer.com/integrations/ups/technical-guide. Cloudsheer has shipped this integration across 15 client projects, and credential setup is where roughly half the lost time happens.
What You Need Before You Start
- A ups.com login with admin control of the shipping account. There is no separate developer account: developer.ups.com uses the same identity you use to print labels.
- A 6-character UPS shipper account number already attached to that ups.com profile. Without it, the app wizard's account dropdown is empty.
- A UPS invoice if you need negotiated rates, typically dated within the last 30 to 90 days. You will need its Invoice Number, Invoice Date, Invoice Total, and Control ID.
- A shared group inbox for the app's primary contact email. That value cannot be changed later without recreating the app.
- A Salesforce org on Winter '24 or later, where External Credentials natively support the client credentials flow. No Auth Provider and no custom Apex are needed.
- A password manager open in another tab before you click Save.
Step 1: Attach the Shipping Account to Your ups.com Profile
The developer portal reads shipper accounts from your ups.com profile. With none attached you can still create an app, but the account dropdown is blank and Shipping calls fail later on an invalid shipper number.
- Log into https://www.ups.com and go to Profile, then Accounts and Payment, then Add Existing Account.
- Enter the 6-character account number. For negotiated rates UPS asks for invoice verification: Invoice Number, Invoice Date, Invoice Total (no dollar sign, no commas), and Control ID, copied character for character from the top right of the invoice.
- Skip the invoice step and Rating still works, but returns published rates only. Every quote your users see will be wrong by the size of your contract discount, and nobody notices until finance does.
Step 2: Create the App at developer.ups.com
- Go to https://developer.ups.com and click Login in the top right, using the same ups.com credentials from Step 1.
- Click the user profile circle in the top-right corner and select Apps, or go straight to https://developer.ups.com/apps.
- Click Add Apps. First-time users land on the form directly; otherwise the link sits at the bottom left of the app list.
- Use-case dropdown: select 'I want to integrate UPS technology into my business'. Any other option provisions a reseller or vendor shaped app that will not work for a direct Salesforce integration, and you cannot convert it. You start over.
- Account Number, sometimes labeled Billing Account Number: pick the account from Step 1. This is the account UPS bills and whose negotiated rates Rating returns.
- Primary Contact: First Name, Last Name, Country, Street Address, City, State, Zip Code, Email, Phone. Put the group inbox in Email.
- App Name: keep it under about 32 characters, since some downstream tools truncate.
- Callback URL: leave it blank, or paste your company website as a harmless placeholder. It is load-bearing only in the authorization-code flow, which you are not using.
UPS revises portal copy periodically, so a label may read slightly differently. The shape of the form has been stable across every project we have run, and the use-case dropdown is the first decision that actually matters.
Step 3: Add Products, Because There Are No Scopes
This is the step people are really searching for when they ask which OAuth scopes UPS needs. There are none. The client credentials flow has no user-selectable scope strings and you never send a scope parameter. Entitlement is defined entirely by which products are checked on the app. The token response echoes a scope field, but you do not control its value.
- In the Add Products panel on the right, click the yellow plus icon next to each API. It turns into a brown checkmark. The list is paginated, so page through it.
- Check at minimum Rating, Shipping, and Tracking. Authorization, which is OAuth itself, is auto-selected and cannot be removed.
- Commonly added alongside: Address Validation, Time in Transit, Locator, Pickup, Paperless Documents. Adding one now is cheaper than reopening the app later.
- Click Save. This is the most common lost-work moment in the whole flow: icons change state visually, but UPS does not persist selections until you press Save.
- Accept the UPS Technology Agreement when prompted. The app is created immediately, with no human review for the base set.
- Expect an approval wall on some products. Test and CIE access is instant, but production access for Shipping, Address Validation Street Level, Locator, Pickup, and Rating for LTL Freight needs a second request, where a dialog asks you to review and attest to UPS production requirements. Rating for small parcel and Tracking are generally instant.
Step 4: Copy the Client ID and Client Secret
- Open the new app from the Apps list. Client ID and Client Secret appear on the detail page. Click the eye icon to reveal, then the clipboard icon to copy. Never select and copy by hand: a trailing space causes a large share of 10401 errors.
- Put both into your secret store immediately, and treat the secret like a production password from this second onward: no email, no Slack, no spreadsheet, no Jira ticket.
- The portal currently lets you re-reveal the secret after navigating away. Integrator docs flag that as atypical and insecure, and it is expected to change. Do not build your runbook around reading it back later.
- One credential pair covers both environments. You change the host, not the credentials: https://wwwcie.ups.com for test and CIE, https://onlinetools.ups.com for production. Docs claiming separate sandbox credentials describe their own wrapper, not UPS.
Step 5: Prove the Token Works Before Touching Salesforce
Ten minutes with curl or Postman saves an afternoon debugging Salesforce config that was never broken. The request shape below is confirmed against OAuthClientCredentials.yaml in github.com/UPS-API/api-documentation.
- POST to https://wwwcie.ups.com/security/v1/oauth/token for test, or https://onlinetools.ups.com/security/v1/oauth/token for production.
- Headers: Authorization: Basic followed by base64 of client_id, a colon, then client_secret, with no newline inside the encoded value. Plus Content-Type: application/x-www-form-urlencoded.
- Optional header x-merchant-id set to your 6-character account number. Several integrators report stubborn 401s clearing the moment it is added, so send it in your manual test.
- Body: the literal string grant_type=client_credentials. That is the only body parameter in the entire flow.
- Success returns token_type, issued_at as epoch milliseconds, client_id, access_token, scope, expires_in, refresh_count, and status. There is no refresh token here: you re-POST for a new one.
- Read expires_in and never hardcode it. UPS cut token validity from 4 hours to 1 hour effective April 1, 2026, so responses that returned 14399 now return roughly 3599. Pre-2026 sample code caching for 4 hours starts throwing 401s.
- For a zero-code harness, UPS ships an official MCP server at github.com/UPS-API/ups-mcp taking CLIENT_ID, CLIENT_SECRET, and ENVIRONMENT of test or production.
Step 6: Create the External Credential in Salesforce
- Setup, then Named Credentials, then the External Credentials tab, then New.
- Label it UPS API with name UPS_API. Authentication Protocol: OAuth 2.0. Flow Type: Client Credentials with Client Secret Flow.
- Identity Provider URL: https://onlinetools.ups.com/security/v1/oauth/token. The field name is a trap. It wants the token endpoint, not an authorize endpoint.
- Leave the option that passes client credentials in the request body switched OFF. UPS specifies HTTP Basic, so Salesforce must send Authorization: Basic. Flipping it produces code 10400, Invalid/Missing Authorization Header, which looks exactly like a bad secret.
- Configure no callback URL anywhere. Salesforce posts server to server and never runs a browser redirect. A callback and an Auth Provider only matter in UPS's authorization-code flow, which exists for third parties acting on another merchant's account. There you save the Auth Provider first, copy the callback URL Salesforce generates, and paste it into the UPS app, where it must match byte for byte including trailing slash, case, port, and protocol.
- Under Principals, click New. Name it UPS_Principal, Sequence Number 1, Identity Type Named Principal, then paste the Client ID and Client Secret into the authentication parameters.
Step 7: Add the Named Credential and Grant Principal Access
- Named Credentials tab, then New. Label UPS, name UPS_Named_Credential, URL https://onlinetools.ups.com with no trailing path.
- Select the UPS_API External Credential and enable Generate Authorization Header so the bearer token is injected on every callout.
- Open a dedicated permission set, go to External Credential Principal Access, and add UPS_Principal.
- Assign that permission set to your integration user and to every admin who will run the smoke test. Missing this is the top cause of failures that read as auth problems but are access problems: 'You do not have access to the external credential'.
- No Remote Site Setting is needed when calling through callout:UPS_Named_Credential. You do need one if custom Apex hits onlinetools.ups.com directly.
Step 8: Smoke Test With a Real Rating Callout
Rating is the right first call: cheap, usually entitled instantly, and a 200 proves the whole chain at once.
- In Developer Console, run Execute Anonymous with an HttpRequest whose endpoint is callout:UPS_Named_Credential/api/rating/v2409/Rate, method POST, Content-Type application/json, and a minimal RateRequest body.
- Set ShipperNumber in the payload to the same 6-character account bound to the app. A mismatch returns code 120100, Missing or invalid shipper number.
- Do not set an Authorization header yourself. Salesforce injects it, and a manual header collides with the generated one.
- A 200 with a RatedShipment block means portal entitlement, credentials, External Credential, principal access, and Named Credential are all correct.
- Run it again after the token expires. A callout that succeeds first and returns 401 only on the second run is a token-refresh misconfiguration, and it never shows up on the first attempt. The native flow re-presents credentials on a 401 and retries, which is why the 1-hour TTL needs no custom handling.
- To test against CIE, point the Named Credential URL at https://wwwcie.ups.com. CIE returns canned sample data for many operations and its labels are non-billable, so never validate business logic on CIE alone.
Common Errors and How to Fix Them
- HTTP 400, code 10400, Unsupported grant type: you sent grant_type as a header, as JSON, or as a query parameter. Fix: send an application/x-www-form-urlencoded body containing the literal grant_type=client_credentials.
- HTTP 400, code 10400, Invalid/Missing Authorization Header: malformed Basic header, or the credentials-in-body toggle is on. Fix: turn the toggle off and check for a newline inside your base64.
- HTTP 401, code 10401, ClientId is Invalid: wrong client ID, a secret copied with trailing whitespace, or credentials from a different app. Fix: re-copy both with the clipboard icon, and outside Salesforce add the x-merchant-id header.
- HTTP 401, code 250002, Invalid Authentication Information: the most reported UPS error. Fix: re-fetch the token, confirm you are not using a CIE token against onlinetools.ups.com or the reverse, and confirm the app's authorization type is client credentials.
- HTTP 401, code 250003, Invalid Access License number: the account is still wired to a legacy access key, or the ups.com login is not the one associated with the account number in the payload. Fix: point the integration at the new OAuth app.
- HTTP 429, code 10429, Too Many Requests: UPS publishes no limits and returns no Retry-After or quota headers, still an open issue on the UPS-API GitHub repo. Fix: exponential backoff plus client-side throttling in your queueables.
- System.CalloutException: Unable to fetch the OAuth token: almost always a wrong Identity Provider URL or the credential-placement toggle. Fix: re-paste the token endpoint exactly and recheck the toggle.
- You do not have access to the external credential: the running user lacks External Credential Principal Access. Fix: assign the permission set carrying the UPS_Principal mapping.
Security Checklist Before You Ship
- Keep the Client Secret only in your secret store and the External Credential principal. It should never appear in Apex, custom metadata, a static resource, a test class, or version control.
- Use a group inbox as the app's primary contact. The field is effectively irreversible, and a personal address becomes a lockout the day that person leaves.
- Grant External Credential Principal Access through a dedicated permission set, not a profile, and assign it only to the integration user and named admins.
- Never hardcode a token TTL. Anything written before April 2026 that caches for 4 hours will now 401. Read expires_in, or let the native flow handle refresh.
- Make the UPS host an environment-specific value so a sandbox refresh cannot leave test data pointed at production and generate billable labels.
- Rotate the secret on a schedule and after any team change, updating the External Credential principal in the same window.
- Log UPS error codes, not raw request bodies. Shipment payloads carry customer names and addresses.
Where This Fits in the Full UPS Build
Everything above is foundation. The build on top is where the product decisions live: which object triggers a rate request, whether labels are stored as Files or streamed to a printer, how UPS service codes map to what reps actually see, how tracking events flow back onto the Order record, and how you handle the void window. Those are covered in the full technical guide at cloudsheer.com/integrations/ups/technical-guide, including the Apex service layer and the retry patterns that keep 10429 away from users.
If you work in Claude Code, we publish a free skill encoding this setup, the request shapes, and the error catalog so your agent stops guessing at UPS payload structures. Install it with: npx skills add shivamgoel-cloudsheer/Claude-skills --skill ups-salesforce. One last note: UPS revises portal wording more often than it revises APIs, so anchor your runbook to endpoints and treat button labels as approximate.
How Cloudsheer Can Help
Cloudsheer has delivered UPS to Salesforce integrations across 15 client projects, including rescue engagements where a legacy access-key integration went dark after the 2024 retirement and shipping stopped the same morning. We know where the approval walls are, which products need a production attestation before go-live, when the native External Credential flow is enough, and the narrow cases where an x-merchant-id requirement means falling back to a custom Apex token fetch.
If you are planning a UPS build, migrating off access keys, or debugging a 250002 that will not clear, we can usually tell you in one call whether the problem is portal entitlement, credential placement, or permission set mapping. Book a free discovery call at cal.com/cloudsheer-consulting/30min.
