# UPS API

- Tool: Bubble
- Difficulty: Intermediate
- Time required: 3–4 hours
- Last updated: July 2026

## TL;DR

Connect Bubble to the UPS REST API using OAuth 2.0 client credentials — but the Client Secret must never appear in a client-side workflow. The clean Bubble solution uses a Backend Workflow (paid plan) that mints a Bearer token server-side, caches it to avoid per-request round-trips, and makes the downstream UPS tracking or rating call. Sandbox and production use different base URLs AND different credentials — a common source of 401 errors at launch.

## Why UPS Requires a Different Approach Than Other Shipping APIs

UPS's modern REST API (replacing the older XML API) uses OAuth 2.0 client credentials — a two-step flow where you first POST to a token endpoint with your Client ID and Secret, receive a time-limited Bearer token, and then use that token on downstream calls. This is more complex than a static API key but gives your Bubble app direct access to UPS tracking data, carrier rates, and address validation without a third-party shipping platform sitting in the middle.

The critical security constraint in Bubble: the Client Secret must never appear in a client-side workflow or API call. The solution is to handle the entire OAuth flow inside a Bubble Backend Workflow that runs server-side. The Backend Workflow stores the Client Secret in a Bubble App Constant marked Private, mints the token, caches it (to avoid 300-500ms round-trip latency on every request), and makes the downstream UPS call. The frontend triggers the Backend Workflow and receives the shipping data back.

UPS maintains separate sandbox (`wwwcie.ups.com`) and production (`onlinetools.ups.com`) environments with entirely separate credentials. Sandbox tracking numbers only work against the sandbox URL. This separation is a common source of silent failures at launch — verify both the base URL and credential set when switching environments.

## Before you start

- A UPS developer account at developer.ups.com — free to create, linked to a UPS My Choice or UPS account
- A registered UPS developer app with a Client ID and Client Secret (created in the developer portal after account registration)
- A paid Bubble plan — Backend Workflows are required to keep the Client Secret server-side and are not available on the Free plan
- Bubble's API Connector plugin installed (Plugins tab → Add plugins → search 'API Connector' → Install)
- Basic familiarity with Bubble's Backend Workflows tab and App Constants (both are under the Settings and left-sidebar panels)

## Step-by-step guide

### 1. Register a UPS Developer App and Get Credentials

Go to developer.ups.com and sign in with your UPS account credentials (or create a new UPS developer account — it is free). Once logged in, click 'My Apps' in the navigation and then 'Create App'.

When creating the app, you will be asked to select API products. For a package tracking integration, select 'Track API'. For shipping rate quotes, select 'Rating API'. For address validation, select 'Address Validation API'. You can add multiple products to a single app. Give the app a name like 'Bubble Integration'.

After creating the app, you will see your **Client ID** and **Client Secret** in the app detail page. Copy both immediately — the Client Secret may not be shown again in full. Store them in a password manager.

IMPORTANT: UPS issues separate credentials for sandbox and production. The app dashboard shows both. Use the sandbox credentials while building — they connect to `wwwcie.ups.com`. Use the production credentials for your live app — they connect to `onlinetools.ups.com`. Do not mix them.

Also note which API products you have added. The UPS REST API is modular — you need to explicitly add each product (Tracking, Rating, Address Validation) to your app before you can call it.

```
# UPS API base URLs
Sandbox:    https://wwwcie.ups.com
Production: https://onlinetools.ups.com

# Token endpoint (same path, different base URL)
Sandbox:    https://wwwcie.ups.com/security/v1/oauth/token
Production: https://onlinetools.ups.com/security/v1/oauth/token

# Key API endpoints
Tracking:   /track/v1/details/{inquiryNumber}
Rating:     /rating/v2409/Rate
Address:    /addressvalidation/v1/1
```

**Expected result:** You have a UPS developer app with sandbox Client ID and Client Secret. You have noted which API products are enabled. You understand the sandbox vs production URL separation.

### 2. Store Credentials as Bubble App Constants and Create a Token Cache Data Type

In your Bubble app, go to Settings → App Constants (this may be under Settings → General → App constants in your Bubble version). Add the following constants:
- `ups_client_id` — your UPS Client ID (value: visible to all, no Private needed since Client ID is less sensitive)
- `ups_client_secret` — your UPS Client Secret (mark this Private)
- `ups_base_url` — `https://wwwcie.ups.com` for sandbox; change to `https://onlinetools.ups.com` for production

Next, create a Bubble Data Type called 'UPSToken' with these fields:
- `access_token` (text) — the Bearer token string returned by the UPS OAuth endpoint
- `expiry_time` (date) — the calculated expiry datetime (current time + token TTL in seconds)
- `is_active` (yes/no) — a flag you can use to quickly find the current valid token

You only ever need one active UPS token record at a time. When you mint a new token, set `is_active` to 'No' on any existing records first, then create a new record with the new token and `is_active` set to 'Yes'. This gives you a simple 'Do a search for UPSToken where is_active is Yes → first item' lookup pattern at the start of each Backend Workflow execution.

```
// Bubble App Constants to create:
// ups_client_id       → "your_client_id_here"
// ups_client_secret   → "your_client_secret_here" (Private)
// ups_base_url        → "https://wwwcie.ups.com" (sandbox)

// UPSToken Data Type fields:
// access_token  (text)
// expiry_time   (date)
// is_active     (yes/no)
```

**Expected result:** App Constants for Client ID, Client Secret, and base URL are saved in Bubble Settings. A UPSToken Data Type exists with the three fields for caching the access token.

### 3. Build a Backend Workflow That Mints and Caches the UPS Access Token

Go to the Backend Workflows tab in Bubble (visible only on paid plans). Click 'New API Workflow'. Name it 'UPS Get Token'. This workflow will be called internally by other Backend Workflows — it does not need to be exposed as a public API endpoint.

Inside the workflow, add these steps:

**Step 1 — Check for a valid cached token:**
Add a 'Do when condition is true' check: 'Search for UPSToken where is_active is Yes and expiry_time > Current date/time → count > 0'. If a valid token exists, the main tracking/rating workflow can use it directly. Design the parent workflow to check this before calling the token workflow.

**Step 2 — Call the UPS token endpoint:**
Add an API Connector call. In the API Connector, create a new API group called 'UPS OAuth'. Add a single call: Method POST, URL = `[ups_base_url App Constant]/security/v1/oauth/token`. Set Content-Type to `application/x-www-form-urlencoded` (not JSON — the token endpoint uses form encoding, not a JSON body).

In the request body, add:
- `grant_type` = `client_credentials`
- `client_id` = `[ups_client_id App Constant]`
- `client_secret` = `[ups_client_secret App Constant — Private]`

**Step 3 — Cache the token:**
After the API call, add a workflow action: 'Make changes to all UPSToken' where is_active is Yes → set `is_active` to No. Then 'Create a new UPSToken' with:
- `access_token` = the `access_token` field from the API response
- `expiry_time` = Current date/time + [token TTL in seconds from the `expires_in` response field]
- `is_active` = Yes

The UPS token endpoint returns `expires_in` as a number of seconds. If this value varies, store it dynamically; otherwise use a conservative value like 3600 seconds (1 hour) as a cache duration.

```
// POST /security/v1/oauth/token — request
// Content-Type: application/x-www-form-urlencoded
// Body (form-encoded):
grant_type=client_credentials&client_id=YOUR_CLIENT_ID&client_secret=YOUR_CLIENT_SECRET

// Success response (200)
{
  "token_type": "Bearer",
  "issued_at": "1720483200000",
  "client_id": "YOUR_CLIENT_ID",
  "access_token": "eyJraWQiOiJ...",
  "expires_in": "14399",
  "status": "approved"
}

// Cache logic:
// expiry_time = Current time + (expires_in seconds)
// If current time < expiry_time → use cached token
// If current time >= expiry_time → mint new token
```

**Expected result:** The UPS OAuth Backend Workflow successfully calls the token endpoint, receives an access_token, and creates a UPSToken record in Bubble's database with the token and its expiry time. Subsequent calls that find a valid cached token skip the minting step.

### 4. Build a Backend Workflow for Package Tracking

Create a second Backend Workflow. Name it 'UPS Track Package'. Add an input parameter: `tracking_number` (text). This is the workflow the frontend will trigger.

Inside the workflow:

**Step 1 — Get a valid token:**
Check if a valid UPSToken record exists (is_active = Yes, expiry_time > Current date/time). If yes, use the cached token. If no, trigger the 'UPS Get Token' Backend Workflow from Step 3 first, wait for it to complete, then fetch the new UPSToken.

**Step 2 — Call the UPS Tracking endpoint:**
In the API Connector, add a new call to the UPS OAuth API group (or create a separate 'UPS API' group for the downstream calls). Name it 'Track Shipment'. Method: GET. URL: `[ups_base_url]/track/v1/details/[tracking_number]`.

Add a call-level header: `Authorization: Bearer [access_token from cached UPSToken]`. This header is dynamic — it references the UPSToken Data Type, not a static App Constant — so it does not need to be Private in the traditional sense, but it is never sent to the browser because it is inside a Backend Workflow.

Add query parameters:
- `locale` = `en_US`
- `returnSignature` = `false`

Initialize the call using a real UPS sandbox tracking number (UPS provides test tracking numbers in the developer portal documentation). Set 'Use as' to 'Data'.

**Step 3 — Parse the response and return data:**
The UPS tracking response has a nested structure: `trackResponse.shipment[0].package[0].activity[0]` holds the most recent status update with `status.description` and `location.address`. Use Bubble's 'Extract JSON body value' steps to surface the key fields. Store them in a Bubble Data Type called 'UPSTrackingResult' or return them to the frontend via a Custom State.

RapidDev's team has built UPS tracking integrations with Bubble for e-commerce clients, including token refresh logic and multi-package dashboards. If your use case is complex, reach out at rapidevelopers.com/contact for a free scoping call.

```
// GET /track/v1/details/{trackingNumber} — query params
// ?locale=en_US&returnSignature=false
// Headers: Authorization: Bearer eyJraWQiOiJ...

// Sample response (simplified)
{
  "trackResponse": {
    "shipment": [
      {
        "inquiryNumber": "1Z12345E0205271688",
        "package": [
          {
            "trackingNumber": "1Z12345E0205271688",
            "currentStatus": {
              "description": "In Transit",
              "code": "I"
            },
            "deliveryDate": [
              { "date": "20260714", "type": "DEL" }
            ],
            "activity": [
              {
                "date": "20260709",
                "time": "143000",
                "location": {
                  "address": {
                    "city": "Louisville",
                    "stateProvince": "KY",
                    "country": "US"
                  }
                },
                "status": {
                  "description": "Package transferred to destination facility",
                  "code": "I"
                }
              }
            ]
          }
        ]
      }
    ]
  }
}
```

**Expected result:** The UPS Track Package Backend Workflow retrieves or reuses a cached Bearer token, calls the UPS Tracking endpoint with the provided tracking number, and returns the current shipment status, location, and estimated delivery date.

### 5. Build a Rating Call for Shipping Cost Estimates

Add a second downstream call to the UPS API. Name it 'Get Rate'. Method: POST. URL: `[ups_base_url]/rating/v2409/Rate`. Add `Content-Type: application/json` and `transactionSrc: Bubble` headers at the call level, plus the dynamic `Authorization: Bearer [token]` header.

The UPS Rating API accepts a detailed JSON body with shipper, destination, and package information. Create a Backend Workflow called 'UPS Get Rate' with parameters for the destination address and package dimensions.

In the workflow, first check for a valid cached token (same pattern as the tracking workflow). Then call the Get Rate action with a dynamically constructed JSON body.

The response contains a `RateResponse.RatedShipment` array with each service level (Ground, 2-Day Air, Next Day Air) and its `TotalCharges.MonetaryValue`. Surface this array to the frontend so the user can select a shipping method at checkout.

For the initialize call in the API Connector, use the UPS developer portal's sample request JSON — the Rating API body is complex and all fields must be present for a valid test response. Bubble's initialize step needs a real successful response to detect the schema correctly.

```
// POST /rating/v2409/Rate — simplified request body
{
  "RateRequest": {
    "Shipment": {
      "Shipper": {
        "Address": {
          "PostalCode": "78701",
          "CountryCode": "US"
        }
      },
      "ShipTo": {
        "Address": {
          "City": "<dynamic: to_city>",
          "StateProvinceCode": "<dynamic: to_state>",
          "PostalCode": "<dynamic: to_postal>",
          "CountryCode": "US"
        }
      },
      "ShipFrom": {
        "Address": {
          "PostalCode": "78701",
          "CountryCode": "US"
        }
      },
      "Package": {
        "PackagingType": { "Code": "02" },
        "PackageWeight": {
          "UnitOfMeasurement": { "Code": "LBS" },
          "Weight": "<dynamic: weight_lbs>"
        }
      }
    }
  }
}

// Response — RatedShipment array
{
  "RateResponse": {
    "RatedShipment": [
      {
        "Service": { "Code": "03", "Description": "UPS Ground" },
        "TotalCharges": { "CurrencyCode": "USD", "MonetaryValue": "12.45" }
      },
      {
        "Service": { "Code": "02", "Description": "UPS 2nd Day Air" },
        "TotalCharges": { "CurrencyCode": "USD", "MonetaryValue": "28.99" }
      }
    ]
  }
}
```

**Expected result:** The UPS Get Rate Backend Workflow returns an array of shipping service options with prices. The checkout page in Bubble displays these options in a Repeating Group for the customer to select.

### 6. Trigger Workflows from the Frontend and Switch to Production

From the Bubble frontend (a button click, page load, or form submission), trigger the Backend Workflow using 'Schedule API Workflow' → select your Backend Workflow → pass the required parameters (tracking number, address fields, etc.).

For tracking: add a 'Track' button to your order detail page. On click, trigger 'UPS Track Package' with the stored tracking number. After the Backend Workflow completes, the result is stored in a UPSTrackingResult Data Type or a Custom State — bind the tracking status display to this data.

Before switching to production:
1. In UPS developer portal → My Apps → your app, get the PRODUCTION Client ID and Client Secret (separate from sandbox credentials)
2. Update the `ups_client_id` App Constant to the production Client ID
3. Update the `ups_client_secret` App Constant to the production Client Secret
4. Update the `ups_base_url` App Constant to `https://onlinetools.ups.com`
5. Clear any cached UPSToken records so the first production call mints a fresh production token

Test with a real UPS tracking number from a recent shipment to verify the production credentials and base URL work together. The UPS tracking numbers in Sandbox documentation are test-only and will not resolve against `onlinetools.ups.com`.

```
# Steps for switching from Sandbox to Production

# 1. Update App Constant: ups_base_url
# Sandbox:    https://wwwcie.ups.com
# Production: https://onlinetools.ups.com

# 2. Update App Constants with production credentials:
# ups_client_id     → production Client ID
# ups_client_secret → production Client Secret (Private)

# 3. Clear cached tokens:
# In Bubble, run a one-time workflow:
# 'Make changes to all UPSToken where is_active = Yes'
# → set is_active = No

# 4. Test with a real tracking number from a recent real UPS shipment
# Production tracking numbers: 1Z + 16 characters (e.g., 1Z12345E0205271688)
# Sandbox test numbers: see UPS developer portal Track API documentation
```

**Expected result:** The tracking and rating flows work end-to-end from the Bubble frontend. Switching to production requires only App Constant updates — no workflow changes needed. A real UPS tracking number resolves successfully against the production API.

## Best practices

- Store the UPS Client Secret as a Private App Constant in Bubble. Private constants are inaccessible from client-side workflows and never appear in Bubble's debugger — equivalent to a server-side environment variable.
- Cache the UPS Bearer token in a Bubble Data Type with an expiry timestamp. Minting a new token on every API call adds 300-500ms latency and risks unnecessary load on UPS's token endpoint. One token can serve hundreds of calls before expiring.
- Keep sandbox and production credentials and base URLs clearly separated. Use App Constants (not hardcoded values in workflows) for both, so switching to production is a settings change rather than a workflow rebuild.
- Design the Backend Workflow to check token validity before every UPS call: if the cached token is still valid, use it; if it has expired, mint a new one first. This pattern handles token expiry gracefully without requiring manual intervention.
- Only request the UPS API products your app actually needs. Adding all products to your developer app does not grant access — each product must be explicitly enabled in the UPS developer portal.
- Test the token refresh flow explicitly: let a cached token expire (or manually set its `expiry_time` to the past in Bubble's database) and verify the Backend Workflow correctly mints a new token and completes the API call.
- In Bubble's Logs tab → Server logs, monitor the token endpoint calls during development to catch credential errors early. The `invalid_client` error from UPS is the most common first-run failure.
- Set up Bubble Privacy rules on the UPSToken Data Type. The access token is a sensitive credential — restrict database read access to server-side (Backend Workflows) only, preventing logged-in users from querying it through the database.

## Use cases

### Package Tracking Status in a Bubble Order Dashboard

Display real-time UPS shipment status, location history, and estimated delivery date directly in a Bubble admin panel or customer-facing order page. Users enter a tracking number and the Bubble workflow calls the UPS Tracking API via a Backend Workflow, returning status and activity checkpoints.

Prompt example:

```
How do I build a Bubble page where users can enter a UPS tracking number and see the current shipment status, location, and estimated delivery date returned from the UPS Tracking API?
```

### Carrier Rate Comparison at Checkout

At the shipping step of a Bubble checkout flow, call the UPS Rating API with the package dimensions, weight, and destination address to retrieve service options and prices. Display the results so the customer can choose between UPS Ground, 2-Day Air, and Next Day Air options.

Prompt example:

```
How do I call UPS's Rating API from a Bubble checkout workflow to get shipping rate options for a package, and display them as a selectable list for the customer?
```

### Address Validation Before Shipping

Before creating a shipment or handing off to ShipStation/another fulfillment system, validate the customer's entered shipping address against UPS's Address Validation API to catch typos, incorrect zip codes, and undeliverable addresses before they cause failed deliveries.

Prompt example:

```
How do I validate a customer's shipping address using the UPS Address Validation API from a Bubble workflow, and surface the corrected address suggestion if the original is invalid?
```

## Troubleshooting

### 401 'invalid_client' error from the UPS token endpoint

Cause: This error typically means the Client ID or Client Secret is incorrect, or you are using sandbox credentials against the production base URL (or vice versa). UPS issues separate credentials for sandbox (`wwwcie.ups.com`) and production (`onlinetools.ups.com`).

Solution: Verify that your App Constants match the environment: sandbox credentials + `wwwcie.ups.com`, or production credentials + `onlinetools.ups.com`. Do not mix. Regenerate credentials in the UPS developer portal if you suspect the secret was compromised. Also confirm the token endpoint body uses `application/x-www-form-urlencoded` encoding, not JSON.

### UPS tracking endpoint returns data in sandbox but not in production

Cause: Sandbox tracking numbers (from UPS test documentation) only resolve against `wwwcie.ups.com`. They are not real packages and cannot be tracked on the production API. This is a common confusion at launch.

Solution: Use a real UPS tracking number from an actual shipment to test against the production API (`onlinetools.ups.com`). Sandbox test numbers from UPS documentation will return 404 or 'tracking number not found' on the production endpoint.

### Backend Workflows tab is not visible in Bubble

Cause: Backend Workflows are only available on Bubble's paid plans. On the Free plan, the Backend Workflows tab does not appear in the Bubble editor.

Solution: Upgrade to a paid Bubble plan to unlock Backend Workflows. The UPS integration requires Backend Workflows to safely handle the Client Secret server-side. There is no secure alternative for this OAuth flow on the Free plan.

### Token caching is not working — a new token is minted on every API call

Cause: The token validity check in the Backend Workflow is not correctly comparing `expiry_time` against the current time, or the UPSToken record is not being found because `is_active` is not set to Yes when the token is created.

Solution: In the Backend Workflow, add a condition at the start: 'Only when: Do a search for UPSToken where is_active is Yes and expiry_time > Current date/time → count is 0'. This condition makes the token-minting steps only run when no valid token exists. Ensure the UPSToken creation step sets `is_active` to Yes.

### 'There was an issue setting up your call' when initializing the Rating API call

Cause: The UPS Rating API requires a complete, valid JSON body with all required fields to return a successful test response for initialization. Missing or placeholder values in the body result in a 400 error from UPS, blocking Bubble's initialization.

Solution: Use the full sample request JSON from UPS developer documentation as your initialization body. Replace address fields with real postal codes (even if test ones) and set a valid package weight. The Rating API is strict about the request structure — incomplete bodies return cryptic 400 errors.

## Frequently asked questions

### Why does the UPS API require a Backend Workflow in Bubble?

UPS uses OAuth 2.0 client credentials — you POST a Client Secret to get a Bearer token. The Client Secret must never appear in client-side code or network requests. Bubble's Backend Workflows run server-side, so storing the Client Secret as a Private App Constant and making the token call from a Backend Workflow keeps the secret completely off the browser.

### Do I need a paid UPS account to use the API?

No — a UPS developer account at developer.ups.com is free. The API provides access to tracking and rating data at no charge. Actual shipping rates are determined by your UPS account contract, and label creation charges carrier fees, but API access itself is free.

### Why does the UPS API work in development but not in production?

This almost always means you are still using sandbox credentials or the sandbox base URL (`wwwcie.ups.com`) in your production app. UPS has entirely separate credentials and base URLs for sandbox vs production. Update your Bubble App Constants: production credentials + `https://onlinetools.ups.com` as the base URL.

### How often does the UPS Bearer token expire?

UPS tokens expire based on the `expires_in` value in the token response (returned in seconds). In practice this is often around 4 hours (14,400 seconds), but you should not hardcode an expiry duration. Instead, store the `expires_in` value from the response, calculate the expiry timestamp, and check it before each API call. If the token has expired, mint a new one.

### Can I get UPS shipping rates without a paid UPS shipper account?

Yes — you can call the Rating API with your developer credentials to get retail UPS rates. However, negotiated (discounted) rates that a business receives through a UPS account contract are only available when the API calls are made with credentials linked to that business account. The rates returned will be retail/list prices without a linked business account.

### Is the old UPS XML API still usable from Bubble?

The legacy UPS XML API still exists but is not recommended for new integrations. The modern REST API at developer.ups.com is the current standard with OAuth 2.0. If you find UPS documentation links pointing to different endpoint structures or WSDL files, those are the legacy XML endpoints — confirm you are on the REST API before building.

---

Source: https://www.rapidevelopers.com/bubble-integrations/ups-api
© RapidDev — https://www.rapidevelopers.com/bubble-integrations/ups-api
