The generalised UPS integration code is available in the WellSpr.ing Forgejo instance at
git.wellspr.ing. It is published under the WellSpr.ing Covenant License (WCL-1.0): freely given, so freely given.
What the integration does
The UPSDirectClient is a Node.js/TypeScript class that wraps the UPS REST API v2205. It handles OAuth2 token acquisition and caching, rate shopping across all available service levels, dimensional weight calculation, and residential delivery classification. It returns structured rate results with per-service totals and itemised charges.
It does not use the Shopify Shipping API, ShipStation, EasyPost, Shippo, or any other aggregator layer. It talks directly to UPS. That's the point.
Prerequisites
- A UPS account (free to create at ups.com)
- UPS Developer credentials — create at developer.ups.com (free tier available)
- Your UPS account number (found in account settings)
- Node.js 18+ or Bun
Setup
export UPS_CLIENT_ID=your_client_id
export UPS_CLIENT_SECRET=your_client_secret
export UPS_ACCOUNT_ID=your_account_number
git clone https://git.wellspr.ing/WellBuilder/ups-direct-client
cd ups-direct-client
npm install
Basic usage
import { UPSDirectClient } from './ups-direct-client';
const ups = new UPSDirectClient({
clientId: process.env.UPS_CLIENT_ID,
clientSecret: process.env.UPS_CLIENT_SECRET,
accountId: process.env.UPS_ACCOUNT_ID,
origin: {
name: 'My Business',
addressLine: '123 Main St',
city: 'Seattle',
state: 'WA',
zip: '98101',
country: 'US',
}
});
const rates = await ups.getRates({
originZip: '98101',
destZip: '10001',
weightLb: 3,
dimsIn: { l: 12, w: 9, h: 6 },
destIsResidential: true,
});
console.log(rates[0]);
How it works — key implementation
Two methods do the work. getToken() acquires and caches the OAuth bearer token; getRates() builds the rate-shop payload and returns structured results sorted cheapest-first.
const creds = Buffer
.from(this.clientId + ":" + this.clientSecret)
.toString("base64");
const tokenRes = await fetch(
"https://onlinetools.ups.com/security/v1/oauth/token",
{
method: "POST",
headers: {
"Authorization": "Basic " + creds,
"Content-Type": "application/x-www-form-urlencoded",
"x-merchant-id": this.accountId,
},
body: "grant_type=client_credentials",
}
);
const { access_token, expires_in } = await tokenRes.json();
const payload = {
RateRequest: {
Request: { RequestOption: "Shop" },
Shipment: {
Shipper: {
ShipperNumber: this.accountId,
Address: { PostalCode: originZip, CountryCode: "US" },
},
ShipTo: {
Address: {
PostalCode: destZip,
CountryCode: "US",
ResidentialAddressIndicator: destIsResidential ? "" : undefined,
},
},
Package: {
PackagingType: { Code: "02" },
Dimensions: dimsIn
? { UnitOfMeasurement: { Code: "IN" },
Length: String(dimsIn.l), Width: String(dimsIn.w), Height: String(dimsIn.h) }
: undefined,
PackageWeight: {
UnitOfMeasurement: { Code: "LBS" },
Weight: String(Math.max(0.1, weightLb)),
},
},
},
},
};
const rateRes = await fetch(
"https://onlinetools.ups.com/api/rating/v2205/shop",
{
method: "POST",
headers: {
"Authorization": "Bearer " + access_token,
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
}
);
const data = await rateRes.json();
The full source with itemised charge parsing, DIM-weight handling, address-validation hooks, and multi-tenant instance management is in the repository. The above captures the OAuth handshake and rate-shop payload pattern.
Multi-tenant usage
Each UPSDirectClient instance is independent: it holds its own OAuth token cache and origin parameters. Construct separate instances for different merchant accounts. This is how ShipFair supports multi-tenant city covenant deployment.
Why carrier-direct
Aggregators like EasyPost and ShipStation add a margin on top of the rates they access on your behalf. The margin is disclosed in their pricing, but the net result is that the rate you see through an aggregator is higher than what you'd get through a direct negotiated account. ShipFair's benchmark uses our own direct account precisely because the direct rate is the honest reference — it reflects what the carrier actually charges when the information asymmetry is removed.