# How to Integrate Bolt.new with FedEx API

- Tool: Bolt.new
- Difficulty: Intermediate
- Time required: 30 minutes
- Last updated: April 2026

## TL;DR

Integrate FedEx's REST API (migrated from SOAP in 2023) into Bolt.new using OAuth 2.0 client credentials. All API calls must be server-side in Next.js API routes — the access token exchange exposes your secret key if done client-side. Bolt.new's WebContainer handles outbound calls for tracking, rate quotes, and label creation. Register at developer.fedex.com to get your Client ID and Secret.

## Integrate FedEx Tracking and Shipping Rates into Bolt.new

FedEx completed its migration from the legacy SOAP/XML API to a modern REST API in 2023, making integration significantly more approachable for web developers. The new API at developer.fedex.com covers tracking, rating, ship (label creation), address validation, pickup scheduling, and international documentation — all with standard HTTP and JSON instead of the old XML envelope format.

The most common use case for Bolt.new apps is package tracking: after a customer places an order, display real-time FedEx tracking status without redirecting them to fedex.com. The second most common is rate shopping: before finalizing shipping costs in your checkout, query FedEx for accurate real-time rates based on the shipment's actual dimensions, weight, and destination. Both use cases require server-side API routes since FedEx credentials must never be in client-side code.

OAuth 2.0 adds one layer of complexity compared to simple API key authentication. You first exchange your Client ID and Secret for a bearer token, then include that token in every API call. Tokens expire after one hour. A well-implemented integration caches the token and only re-exchanges when it expires — rather than making a token request before every single API call, which would double your request count and add latency. Bolt.new's WebContainer environment supports this token caching pattern in-memory during development, though cached tokens reset between server restarts.

## Before you start

- A FedEx Developer Portal account at developer.fedex.com — sign up with your business FedEx account or create a developer account
- A project created in the FedEx Developer Portal with API access enabled — provides your Client ID and Client Secret
- FedEx API access enabled for Tracking, Rating, and Ship APIs in your project settings
- A Bolt.new account with a new Next.js project open
- A Netlify account for deploying the production integration with real FedEx production credentials

## Step-by-step guide

### 1. Register on FedEx Developer Portal and get OAuth credentials

FedEx's new REST API requires registration at developer.fedex.com — a separate portal from your regular FedEx shipping account. The registration process creates an API project that is linked to your FedEx account and grants specific API access.

To register: go to developer.fedex.com and click 'Get API Access'. Sign in with your FedEx account credentials or create a new developer account. Once logged in, navigate to My Projects → Create a Project. Give your project a name (e.g., 'My App FedEx Integration') and agree to the API terms.

In your project settings, add the APIs you need. For the most common integration scenarios, enable: Tracking API (for package status), Rate API (for rate quotes), and Ship API (for label creation). FedEx creates a separate Client ID and Client Secret for each project. Both values are shown in the project detail page — copy them immediately and store them in your password manager.

FedEx provides two environments: Sandbox (sandbox.fedex.com APIs, uses test tracking numbers and simulated responses) and Production (apis.fedex.com). Always develop and test against the sandbox first. The sandbox base URL is `https://apis-sandbox.fedex.com` and the production URL is `https://apis.fedex.com`. Use different environment variables for each to make switching easy.

Add both credentials to your .env file. These are sensitive credentials that must never be exposed in client-side code or NEXT_PUBLIC_ environment variables. The Client Secret in particular is equivalent to an API password.

```
// lib/fedex.ts
interface TokenCache {
  token: string;
  expiresAt: number;
}

let tokenCache: TokenCache | null = null;

async function getFedExToken(): Promise<string> {
  const now = Date.now();
  // Return cached token if still valid (with 60s buffer)
  if (tokenCache && tokenCache.expiresAt > now + 60_000) {
    return tokenCache.token;
  }

  const clientId = process.env.FEDEX_CLIENT_ID;
  const clientSecret = process.env.FEDEX_CLIENT_SECRET;
  const baseUrl = process.env.FEDEX_API_BASE_URL ?? 'https://apis-sandbox.fedex.com';

  if (!clientId || !clientSecret) {
    throw new Error('FEDEX_CLIENT_ID and FEDEX_CLIENT_SECRET must be set in .env');
  }

  const response = await fetch(`${baseUrl}/oauth/token`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({
      grant_type: 'client_credentials',
      client_id: clientId,
      client_secret: clientSecret,
    }),
  });

  if (!response.ok) {
    const err = await response.text();
    throw new Error(`FedEx token exchange failed: ${err}`);
  }

  const data = await response.json() as { access_token: string; expires_in: number };
  tokenCache = {
    token: data.access_token,
    expiresAt: now + data.expires_in * 1000,
  };

  return data.access_token;
}

export async function fedexFetch<T = unknown>(
  endpoint: string,
  init: RequestInit = {}
): Promise<T> {
  const token = await getFedExToken();
  const baseUrl = process.env.FEDEX_API_BASE_URL ?? 'https://apis-sandbox.fedex.com';
  const url = `${baseUrl}${endpoint.startsWith('/') ? endpoint : '/' + endpoint}`;

  const response = await fetch(url, {
    ...init,
    headers: {
      Authorization: `Bearer ${token}`,
      'Content-Type': 'application/json',
      'X-Customer-Transaction-Id': `bolt-${Date.now()}`,
      'X-locale': 'en_US',
      ...init.headers,
    },
  });

  if (!response.ok) {
    const errorData = await response.text();
    throw new Error(`FedEx API ${response.status} at ${endpoint}: ${errorData}`);
  }

  return response.json() as Promise<T>;
}
```

**Expected result:** A configured FedEx authentication helper with token caching in lib/fedex.ts, and credentials in .env pointing at the sandbox environment.

### 2. Build the package tracking API route

The FedEx Tracking API (`POST /track/v1/trackingnumbers`) returns comprehensive shipment status including the latest scan event, all historical events, estimated delivery date and time, delivery location details, and exception information for delayed or problem shipments.

The request body requires a `trackingInfo` array with objects containing `trackingNumberInfo.trackingNumber`. You can track up to 30 shipments in a single API call, which is useful for building an order tracking page that shows multiple packages.

The response structure is nested: `output.completeTrackResults[0].trackResults[0]` contains the main tracking data. The `latestStatusDetail` object has `description` (human-readable status like 'On FedEx vehicle for delivery') and `code` (machine-readable status code like 'OD'). The `estimatedDeliveryTimeWindow` object has the expected delivery date range.

The `scanEvents` array contains all tracking events in reverse chronological order — the first element is the most recent. Each event has `date`, `time`, `eventType`, `eventDescription`, and `scanLocation` (city, state). Displaying the 5 most recent events gives users a clear view of their package's journey.

For the sandbox, FedEx provides test tracking numbers in their documentation: `128667043726` for a delivered package, `49044304137821` for in-transit. Always test against sandbox tracking numbers before switching to production credentials.

Important: the tracking API call works in Bolt's WebContainer — it is a straightforward outbound POST request and there is no WebContainer limitation for this use case.

```
// app/api/fedex/track/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { fedexFetch } from '@/lib/fedex';

export async function POST(request: NextRequest) {
  const { trackingNumber } = await request.json();

  if (!trackingNumber?.trim()) {
    return NextResponse.json({ error: 'trackingNumber is required' }, { status: 400 });
  }

  const body = {
    includeDetailedScans: true,
    trackingInfo: [{
      trackingNumberInfo: { trackingNumber: trackingNumber.trim() },
    }],
  };

  const data = await fedexFetch<Record<string, unknown>>('/track/v1/trackingnumbers', {
    method: 'POST',
    body: JSON.stringify(body),
  });

  const trackResult = (
    data as {
      output?: {
        completeTrackResults?: Array<{
          trackResults?: Array<{
            latestStatusDetail?: { description: string; code: string };
            estimatedDeliveryTimeWindow?: { window?: { ends?: string } };
            scanEvents?: Array<{
              date: string;
              time: string;
              eventType: string;
              eventDescription: string;
              scanLocation?: { city: string; stateOrProvinceCode: string };
            }>;
          }>;
        }>;
      };
    }
  )?.output?.completeTrackResults?.[0]?.trackResults?.[0];

  if (!trackResult) {
    return NextResponse.json({ error: 'Tracking number not found' }, { status: 404 });
  }

  const events = (trackResult.scanEvents ?? []).slice(0, 5).map(e => ({
    date: e.date,
    time: e.time,
    description: e.eventDescription,
    location: e.scanLocation
      ? `${e.scanLocation.city}, ${e.scanLocation.stateOrProvinceCode}`
      : 'Unknown location',
  }));

  return NextResponse.json({
    status: trackResult.latestStatusDetail?.code,
    description: trackResult.latestStatusDetail?.description,
    estimatedDelivery: trackResult.estimatedDeliveryTimeWindow?.window?.ends,
    events,
  });
}
```

**Expected result:** A tracking API route that returns current status, estimated delivery, and recent scan events for any FedEx tracking number.

### 3. Implement the FedEx Rate API for shipment cost estimation

The FedEx Rate API (`POST /rate/v1/rates/quotes`) returns price quotes for all available FedEx services between two addresses. This is the 'rate shopping' use case — showing customers or your fulfillment team the cost and estimated delivery time for Ground, Express, 2Day, and Overnight services before committing to a label.

The rate request body is detailed but follows a clear structure. You need: shipper address (including postal code and country), recipient address (including postal code, country, and residential flag), package details (weight with units, and optionally dimensions), and optionally a preferred service type. Setting `rateRequestType: ['LIST', 'PREFERRED']` returns list rates (published rates without account discounts) and any negotiated rates your FedEx account qualifies for.

The response includes a `rateReplyDetails` array with one entry per service type available for the route. Each entry has `serviceType` (e.g., FEDEX_GROUND, PRIORITY_OVERNIGHT), `serviceName` (human readable), `commit` (estimated delivery date and day), and `ratedShipmentDetails` array containing the shipment charges. The `totalNetCharge` within the rated shipment details is the total cost including all surcharges.

FedEx applies various surcharges that increase the base rate: Residential Delivery Surcharge (for deliveries to homes instead of businesses), Fuel Surcharge (percentage applied to all shipments), Oversized surcharge (for packages over certain dimensions), and Extended Delivery Area charges for remote ZIP codes. Your rate response will show these broken out, but `totalNetCharge` captures everything.

Sorting results by delivery date rather than price helps users make informed decisions — showing 'Ground: $8.50 (5 days)' versus 'Priority Overnight: $45.00 (1 day)' lets users pick based on urgency and budget.

```
// app/api/fedex/rates/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { fedexFetch } from '@/lib/fedex';

export async function POST(request: NextRequest) {
  const { fromZip, toZip, weightLbs, toResidential = false, dimensions } =
    await request.json();

  if (!fromZip || !toZip || !weightLbs) {
    return NextResponse.json(
      { error: 'fromZip, toZip, and weightLbs are required' },
      { status: 400 }
    );
  }

  const requestBody = {
    accountNumber: { value: process.env.FEDEX_ACCOUNT_NUMBER ?? '' },
    rateRequestType: ['LIST'],
    requestedShipment: {
      shipper: {
        address: {
          postalCode: fromZip,
          countryCode: 'US',
          residential: false,
        },
      },
      recipient: {
        address: {
          postalCode: toZip,
          countryCode: 'US',
          residential: toResidential,
        },
      },
      pickupType: 'USE_SCHEDULED_PICKUP',
      rateRequestType: ['LIST'],
      requestedPackageLineItems: [{
        weight: { value: weightLbs, units: 'LB' },
        ...(dimensions ? {
          dimensions: {
            length: dimensions.length,
            width: dimensions.width,
            height: dimensions.height,
            units: 'IN',
          },
        } : {}),
      }],
    },
  };

  const data = await fedexFetch<Record<string, unknown>>('/rate/v1/rates/quotes', {
    method: 'POST',
    body: JSON.stringify(requestBody),
  });

  const details = (data as { output?: { rateReplyDetails?: unknown[] } })
    ?.output?.rateReplyDetails ?? [];

  const rates = (details as Array<Record<string, unknown>>).map(d => {
    const ratedDetails = (d.ratedShipmentDetails as Array<Record<string, unknown>>)?.[0];
    const charges = ratedDetails?.shipmentRateDetail as Record<string, unknown>;
    const totalCharge = (charges?.totalNetCharge as Record<string, unknown>);
    return {
      serviceType: d.serviceType,
      serviceName: d.serviceName,
      deliveryDate: (d.commit as Record<string, unknown>)?.dateDetail,
      totalCharge: Number(totalCharge?.amount ?? 0),
      currency: String(totalCharge?.currency ?? 'USD'),
    };
  }).sort((a, b) => a.totalCharge - b.totalCharge);

  return NextResponse.json({ rates });
}
```

**Expected result:** A rate quote API route that returns FedEx service options with pricing and estimated delivery dates, sorted by total cost for easy comparison.

### 4. Deploy to Netlify and switch to production FedEx credentials

All FedEx API interactions in this guide — token exchange, tracking, and rate queries — are outbound HTTP requests that work perfectly in Bolt.new's WebContainer during development. The WebContainer limitation (no incoming connections) does not affect any of the primary FedEx API use cases described here.

FedEx does not offer webhook-style push notifications for most events — shipping events are pull-based (you call the Tracking API to get current status). However, if you implement FedEx Freight features or FedEx Signature Required notifications, those services may require a callback URL. For standard tracking and rating, no webhook configuration is needed.

The main deployment task is switching from the FedEx sandbox environment to production. In sandbox, you use test tracking numbers and simulated rate data. In production, you interact with real FedEx shipment data and your actual account rates.

To deploy: click Deploy in Bolt.new and connect to Netlify. After deployment, go to Netlify Dashboard → Site Configuration → Environment Variables. Add the production credentials: `FEDEX_CLIENT_ID` and `FEDEX_CLIENT_SECRET` (from your FedEx Developer Portal production project), and change `FEDEX_API_BASE_URL` from `https://apis-sandbox.fedex.com` to `https://apis.fedex.com`. Also add `FEDEX_ACCOUNT_NUMBER` if you use the rate API.

To create a production project in FedEx Developer Portal: go to My Projects → Create New Project, select 'Production', and request approval for the APIs you need. FedEx production access requires a brief approval process — submit your project for review and FedEx typically approves within 1-3 business days. Sandbox access is instant.

Update your FEDEX_API_BASE_URL environment variable to toggle between sandbox and production without changing any code.

```
// netlify.toml
[[plugins]]
package = "@netlify/plugin-nextjs"

[build]
  command = "npm run build"
  publish = ".next"

[build.environment]
  NEXT_TELEMETRY_DISABLED = "1"

# Environment variables needed (set in Netlify dashboard):
# FEDEX_CLIENT_ID — from FedEx Developer Portal (production project)
# FEDEX_CLIENT_SECRET — from FedEx Developer Portal (production project)
# FEDEX_API_BASE_URL — https://apis.fedex.com (production) or https://apis-sandbox.fedex.com (sandbox)
# FEDEX_ACCOUNT_NUMBER — your FedEx account number (required for rating API)
```

**Expected result:** A deployed Netlify app using production FedEx credentials, with sandbox credentials still used in local development via .env.

## Best practices

- Cache OAuth tokens in a module-level variable with expiry tracking — FedEx tokens last one hour and making a token exchange before every API call doubles your request count unnecessarily
- Keep separate FedEx Developer Portal projects for sandbox and production environments, with corresponding separate environment variable sets
- Store FEDEX_CLIENT_ID, FEDEX_CLIENT_SECRET, and FEDEX_ACCOUNT_NUMBER as server-side environment variables only — never use NEXT_PUBLIC_ prefix for any FedEx credentials
- Always develop against the sandbox (apis-sandbox.fedex.com) to avoid creating real shipments or consuming production API quota during development
- Include the X-Customer-Transaction-Id header in all API requests with a unique identifier — this makes it much easier to trace specific requests in FedEx support tickets
- Parse and surface FedEx error codes to users rather than generic 500 errors — FedEx returns detailed error messages that explain exactly what is wrong with a request
- For rate requests, always set the residential flag correctly — residential deliveries cost more than commercial, and an incorrect flag can cause checkout price discrepancies
- Validate tracking numbers format before sending to the API — FedEx tracking numbers are 12-22 digits, and validating client-side catches obvious typos before hitting the API

## Use cases

### Package Tracking Status Widget

After a customer receives their tracking number, display real-time FedEx tracking status directly in your app — showing the estimated delivery date, current scan location, and delivery progress — without sending them to fedex.com.

Prompt example:

```
Build a FedEx package tracking feature. Create an API route at app/api/fedex/track/route.ts that accepts POST with a trackingNumber. Exchange FedEx client credentials (FEDEX_CLIENT_ID and FEDEX_CLIENT_SECRET from process.env) for an OAuth token via POST to https://apis-sandbox.fedex.com/oauth/token with grant_type=client_credentials. Then call the FedEx Tracking API at /track/v1/trackingnumbers with the token. Return status, description, estimated delivery date, and the most recent scan event. Create a React TrackingWidget component with a tracking number input and status display.
```

### Checkout Shipping Rate Calculator

Before finalizing shipping options at checkout, query FedEx for real-time rate quotes based on the customer's destination ZIP code and your package dimensions. Display Ground, Express, and Overnight options with accurate prices and estimated delivery dates.

Prompt example:

```
Create a FedEx rate calculator for my checkout. Build an API route at app/api/fedex/rates/route.ts that accepts POST with fromZip, toZip, weightLbs, and optional dimensions (length, width, height in inches). Get an OAuth token from FedEx sandbox, then call the Rate API at /rate/v1/rates/quotes. Return a list of service options with serviceName, deliveryDate, and totalCharge. Sort by totalCharge ascending. Build a React ShippingOptions component that shows the rate options as selectable cards.
```

### Shipping Label Generator

Allow your fulfillment team to create FedEx shipping labels directly from your order management dashboard. Accept order details, get a rate quote for carrier selection, then generate a label and return the tracking number and printable label PDF.

Prompt example:

```
Build a shipping label creation flow. Create an API route at app/api/fedex/create-shipment/route.ts that accepts POST with shipper address, recipient address, package weight and dimensions, and serviceType (e.g., FEDEX_GROUND). Call the FedEx Ship API at /ship/v1/shipments with an OAuth token. Return the trackingNumber and label base64 data. Add a note that label creation uses real FedEx production credentials and charges the account — test only in sandbox mode. Create a form component for entering shipment details.
```

## Troubleshooting

### OAuth token exchange returns 401 or 'invalid_client' error

Cause: The Client ID or Client Secret is incorrect, or you are mixing sandbox and production credentials with the wrong API base URL. Sandbox credentials only work with apis-sandbox.fedex.com and production credentials only work with apis.fedex.com.

Solution: Verify the credentials match the environment: for sandbox development, use apis-sandbox.fedex.com with sandbox project credentials. Check that FEDEX_CLIENT_ID is the client ID (not the project name) and FEDEX_CLIENT_SECRET is the full secret string. Regenerate credentials in the FedEx Developer Portal if you suspect they were entered incorrectly.

### Rate API returns 'Account not found' or 400 error on rate requests

Cause: The FedEx Rate API requires a valid account number in the request body's accountNumber field. If FEDEX_ACCOUNT_NUMBER is not set or is incorrect, the rate request is rejected. This is different from the tracking API which does not require an account number.

Solution: Add FEDEX_ACCOUNT_NUMBER to your .env file. Your FedEx account number is a 9-digit number found in your FedEx account profile or in the FedEx Developer Portal under your project settings. In sandbox mode, use the test account number provided in the FedEx Developer Portal documentation.

```
// In .env — add your FedEx account number
// FEDEX_ACCOUNT_NUMBER=123456789
```

### FedEx API calls work in Bolt.new development but fail after deploying to Netlify

Cause: The most common cause is that environment variables were not added to Netlify's environment variable settings after deployment. .env files are local-only and never deployed. A secondary cause is that FEDEX_API_BASE_URL is still pointing at the sandbox URL when production credentials are being used.

Solution: Go to Netlify Dashboard → Site Configuration → Environment Variables and verify that FEDEX_CLIENT_ID, FEDEX_CLIENT_SECRET, FEDEX_API_BASE_URL, and FEDEX_ACCOUNT_NUMBER are all set. If switching to production, ensure FEDEX_API_BASE_URL is https://apis.fedex.com (not the sandbox URL). Trigger a redeploy after adding variables.

### Tracking API returns empty scan events for a tracking number that shows history on fedex.com

Cause: Sandbox mode only returns simulated data for the specific test tracking numbers provided in FedEx documentation. Real tracking numbers do not work in sandbox mode — only in production. If testing with real tracking numbers, ensure FEDEX_API_BASE_URL is set to the production URL.

Solution: For sandbox testing, use the test tracking numbers from FedEx documentation (e.g., 128667043726 for delivered status). For real shipment data, update FEDEX_API_BASE_URL to https://apis.fedex.com and use production credentials. The sandbox environment intentionally cannot access real shipment data.

## Frequently asked questions

### Can I use the FedEx API in Bolt.new without deploying?

Yes. All FedEx API calls — OAuth token exchange, tracking, rate quotes, and label creation — are outbound HTTP requests that work perfectly in Bolt.new's WebContainer during development. The WebContainer limitation only affects incoming connections (like webhook callbacks). FedEx does not push data to your app for core tracking and rating use cases, so no deployment is required for development and testing.

### Do I need a FedEx account to use the FedEx API?

Yes, you need a FedEx account to register on the FedEx Developer Portal. For sandbox/testing, you can use a developer account without an active shipping account. For production use (real tracking numbers and live rate quotes), you need a FedEx business account. FedEx accounts are free to create at fedex.com — you only pay when you actually ship packages.

### How do FedEx OAuth tokens work and how long do they last?

FedEx uses OAuth 2.0 client credentials flow. You POST your Client ID and Secret to /oauth/token and receive an access token that expires in 3600 seconds (one hour). You include this token as a Bearer Authorization header in every subsequent API call. The lib/fedex.ts helper in this guide caches the token in memory and automatically exchanges for a new one when it is close to expiry, so your API routes always have a valid token without manual management.

### What is the difference between FedEx sandbox and production APIs?

The sandbox (apis-sandbox.fedex.com) returns simulated data using a set of test tracking numbers and generates fake rate quotes. No real shipments are created and no charges are incurred. Production (apis.fedex.com) accesses real FedEx shipment data for your account, returns actual carrier rates, and creates real labels when you call the Ship API. Always develop against sandbox, and switch to production credentials only after testing your integration thoroughly.

### Why does the FedEx Rate API need an account number when Tracking does not?

The tracking API is publicly accessible with just an OAuth token — FedEx provides tracking data openly. Rate quotes, however, are account-specific: your negotiated rates, available services, and surcharges depend on your specific FedEx account and its contractual terms. The account number tells FedEx which rate tier and service agreements to apply when generating your quote.

---

Source: https://www.rapidevelopers.com/bolt-ai-integrations/fedex-api
© RapidDev — https://www.rapidevelopers.com/bolt-ai-integrations/fedex-api
