# How to Integrate FedEx API with V0

- Tool: V0
- Difficulty: Intermediate
- Time required: 45 minutes
- Last updated: March 2026

## TL;DR

To integrate the FedEx API with V0 by Vercel, generate a shipping UI with V0, create Next.js API routes that authenticate with FedEx's OAuth2 client credentials flow and call rate quote, tracking, and shipment endpoints, store your credentials in Vercel environment variables, and deploy. Your app can get shipping rates, track packages, and generate labels entirely server-side.

## Add FedEx Shipping Rates, Tracking, and Labels to V0-Generated Apps

The FedEx REST API (released 2022, with the older SOAP API being retired) provides programmatic access to FedEx's full logistics capabilities: rate quotes for all FedEx service types, real-time shipment tracking, label generation, and address validation. For e-commerce applications, embedded marketplaces, or any app that needs to quote or fulfill physical shipments, FedEx API integration adds significant value. V0 generates the shipping UI — rate comparison tables, tracking status pages, shipment forms — and API routes handle the FedEx communication securely.

FedEx API authentication differs from simple API key auth. It uses OAuth2 client credentials flow: you exchange your client ID and client secret for a short-lived access token (expiring in one hour), then include that Bearer token in all subsequent API requests. This means your API routes need to handle token lifecycle: obtain a token, cache it with its expiry timestamp, and refresh it before expiry. Implementing this correctly prevents unnecessary token requests on every API call while ensuring tokens are always valid.

Before your app goes live, FedEx requires moving from their sandbox environment (developer testing account) to production credentials. The sandbox uses test account numbers and doesn't generate real shipments or charges — perfect for development. The transition to production requires a FedEx agreement and account number. Most developers start in sandbox, build the full integration, then request production access.

## Before you start

- A FedEx Developer account — register at developer.fedex.com and create a new project to get your API key and secret
- FedEx sandbox account credentials: client ID (API key) and client secret from your FedEx Developer Portal project
- A FedEx account number — available from your FedEx customer account, used as the payor account for rate quotes and shipments
- A V0 account at v0.dev to generate the shipping UI
- A Vercel account to deploy and manage environment variables

## Step-by-step guide

### 1. Generate the Shipping UI with V0

Open V0 at v0.dev and describe the shipping interface you need. FedEx integrations commonly need one of three UI patterns: a rate calculator (form inputs → comparison table), a tracking page (tracking number input → timeline), or a shipment creation form (order details → label download). Be specific in your V0 prompt about the inputs users will provide (ZIP codes, dimensions, weight, tracking number) and the outputs to display (service types, prices, delivery estimates, event logs). V0 will generate React components using shadcn/ui and Tailwind. The rate comparison component will likely render a table or card grid — note how V0 structures the mock rate data objects since your API route needs to return matching shapes. For the tracking timeline, V0 works well with a vertical list component where each item has a timestamp, location, and description. Push the generated project to GitHub via V0's Git panel. At this stage the components show mock data — the next steps connect them to real FedEx API responses.

**Expected result:** A styled shipping rate calculator or tracking page renders in V0's preview with form inputs, a results table with mock rates, and loading/error states — ready to receive real FedEx API data.

### 2. Create the FedEx OAuth2 Token Helper

FedEx API authentication requires obtaining an OAuth2 access token before every set of API calls. Create a token management module at lib/fedex-auth.ts that handles the token lifecycle. The token endpoint is https://apis-sandbox.fedex.com/oauth/token (sandbox) or https://apis.fedex.com/oauth/token (production). You POST your client_id and client_secret as application/x-www-form-urlencoded with grant_type=client_credentials. FedEx returns an access_token valid for 3600 seconds (1 hour). Rather than requesting a new token on every API call, cache the token in a module-level variable with its expiry timestamp. The token helper checks if the cached token is still valid (with a 60-second safety margin) before deciding whether to request a new one. In Vercel's serverless environment, module-level variables persist within the same function instance during a request but are not shared between instances — this caching still provides value by avoiding redundant token requests within a single request that makes multiple FedEx API calls. For production, you might store tokens in Redis/Upstash if you need cross-instance caching.

```
// lib/fedex-auth.ts
interface CachedToken {
  accessToken: string;
  expiresAt: number; // Unix timestamp in ms
}

let tokenCache: CachedToken | null = null;

function getFedexBaseUrl(): string {
  return process.env.FEDEX_SANDBOX === 'true'
    ? 'https://apis-sandbox.fedex.com'
    : 'https://apis.fedex.com';
}

export async function getFedexAccessToken(): Promise<string> {
  const now = Date.now();
  const bufferMs = 60 * 1000; // 60 second safety margin

  // Return cached token if still valid
  if (tokenCache && tokenCache.expiresAt > now + bufferMs) {
    return tokenCache.accessToken;
  }

  const clientId = process.env.FEDEX_CLIENT_ID;
  const clientSecret = process.env.FEDEX_CLIENT_SECRET;

  if (!clientId || !clientSecret) {
    throw new Error('FedEx credentials not configured');
  }

  const body = new URLSearchParams({
    grant_type: 'client_credentials',
    client_id: clientId,
    client_secret: clientSecret,
  });

  const response = await fetch(`${getFedexBaseUrl()}/oauth/token`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: body.toString(),
    cache: 'no-store',
  });

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

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

  return tokenCache.accessToken;
}

export async function fedexFetch(path: string, options: RequestInit = {}): Promise<Response> {
  const token = await getFedexAccessToken();
  const baseUrl = getFedexBaseUrl();

  return fetch(`${baseUrl}${path}`, {
    ...options,
    headers: {
      ...options.headers,
      Authorization: `Bearer ${token}`,
      'Content-Type': 'application/json',
      'X-locale': 'en_US',
    },
  });
}
```

**Expected result:** The lib/fedex-auth.ts module exports a fedexFetch helper that automatically obtains and caches OAuth2 tokens, which API routes can use to make authenticated FedEx requests without managing tokens themselves.

### 3. Create the Rate Quote and Tracking API Routes

Create the API routes for FedEx rate quotes and tracking using the fedexFetch helper. The FedEx Rates and Transit Times API endpoint is /rate/v1/rates/quotes (POST). The request body specifies the shipper address, recipient address, package dimensions and weight, and requested service types. The response includes a list of rate replies, each containing the service type, total net charge, and transit time. Your API route transforms this into a clean array matching your dashboard component's expected format. For tracking, the FedEx Track API endpoint is /track/v1/trackingnumbers (POST). Send an array containing the tracking number and get back shipment events with status codes, timestamps, and location details. The FedEx API uses specific codes for service types (FEDEX_GROUND, FEDEX_2_DAY, STANDARD_OVERNIGHT, etc.) and event types — map these to human-readable labels in your transformation. FedEx's request schemas are detailed — pay close attention to required vs optional fields. The account number (FEDEX_ACCOUNT_NUMBER) appears in the rate request as the shipper's account and payment method.

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

const SERVICE_LABELS: Record<string, string> = {
  FEDEX_GROUND: 'FedEx Ground',
  FEDEX_2_DAY: 'FedEx 2Day',
  FEDEX_2_DAY_AM: 'FedEx 2Day AM',
  STANDARD_OVERNIGHT: 'FedEx Standard Overnight',
  PRIORITY_OVERNIGHT: 'FedEx Priority Overnight',
  FIRST_OVERNIGHT: 'FedEx First Overnight',
  FEDEX_EXPRESS_SAVER: 'FedEx Express Saver',
};

export async function POST(request: NextRequest) {
  try {
    const { originZip, destinationZip, weight, length, width, height } =
      await request.json();

    const accountNumber = process.env.FEDEX_ACCOUNT_NUMBER;
    if (!accountNumber) {
      return NextResponse.json({ error: 'FedEx account not configured' }, { status: 500 });
    }

    const rateRequest = {
      accountNumber: { value: accountNumber },
      requestedShipment: {
        shipper: { address: { postalCode: originZip, countryCode: 'US' } },
        recipient: { address: { postalCode: destinationZip, countryCode: 'US' } },
        pickupType: 'DROPOFF_AT_FEDEX_LOCATION',
        rateRequestType: ['ACCOUNT', 'LIST'],
        requestedPackageLineItems: [{
          weight: { units: 'LB', value: weight },
          dimensions: { length, width, height, units: 'IN' },
        }],
      },
    };

    const response = await fedexFetch('/rate/v1/rates/quotes', {
      method: 'POST',
      body: JSON.stringify(rateRequest),
    });

    if (!response.ok) {
      const error = await response.json();
      console.error('FedEx rate error:', error);
      return NextResponse.json({ error: 'Rate quote failed' }, { status: response.status });
    }

    const data = await response.json();
    const rateDetails = data.output?.rateReplyDetails || [];

    const rates = rateDetails.map((detail: Record<string, unknown>) => {
      const shipmentDetail = (detail.ratedShipmentDetails as Record<string, unknown>[])?.[0];
      const totalCharge = (shipmentDetail?.totalNetChargeWithDutiesAndTaxes as Record<string, unknown>)
        || (shipmentDetail?.totalNetCharge as Record<string, unknown>);

      return {
        serviceType: detail.serviceType,
        serviceName: SERVICE_LABELS[detail.serviceType as string] || detail.serviceType,
        deliveryDate: detail.commit?.dateDetail?.dayFormat || 'N/A',
        transitDays: detail.commit?.transitDays?.description || '',
        rate: parseFloat((totalCharge?.amount as string) || '0'),
        currency: totalCharge?.currency || 'USD',
      };
    }).sort((a: { rate: number }, b: { rate: number }) => a.rate - b.rate);

    return NextResponse.json({ rates });
  } catch (error) {
    console.error('FedEx rates error:', error);
    return NextResponse.json({ error: 'Failed to get rates' }, { status: 500 });
  }
}

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

export async function GET(request: NextRequest) {
  const { searchParams } = new URL(request.url);
  const trackingNumber = searchParams.get('trackingNumber');

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

  try {
    const response = await fedexFetch('/track/v1/trackingnumbers', {
      method: 'POST',
      body: JSON.stringify({
        includeDetailedScans: true,
        trackingInfo: [{ trackingNumberInfo: { trackingNumber } }],
      }),
    });

    if (!response.ok) {
      return NextResponse.json({ error: 'Tracking request failed' }, { status: response.status });
    }

    const data = await response.json();
    const trackResult = data.output?.completeTrackResults?.[0]?.trackResults?.[0];

    if (!trackResult) {
      return NextResponse.json({ error: 'No tracking data found' }, { status: 404 });
    }

    const scanEvents = (trackResult.scanEvents || []).map((event: Record<string, unknown>) => ({
      timestamp: event.date,
      eventType: event.eventType,
      description: event.eventDescription,
      location: [event.scanLocation?.city, event.scanLocation?.stateOrProvinceCode]
        .filter(Boolean).join(', '),
    }));

    return NextResponse.json({
      trackingNumber,
      status: trackResult.latestStatusDetail?.description,
      estimatedDelivery: trackResult.estimatedDeliveryTimeWindow?.window?.ends,
      events: scanEvents,
    });
  } catch (error) {
    console.error('FedEx tracking error:', error);
    return NextResponse.json({ error: 'Tracking failed' }, { status: 500 });
  }
}
```

**Expected result:** POST /api/fedex/rates with origin/destination ZIP codes and package dimensions returns an array of shipping options with rates. GET /api/fedex/track?trackingNumber=XXX returns scan events and current status.

### 4. Configure FedEx Credentials in Vercel and Deploy

Push your project to GitHub and add FedEx credentials to Vercel. Open the Vercel Dashboard, navigate to your project → Settings → Environment Variables. Add four variables: FEDEX_CLIENT_ID (your API key from the FedEx Developer Portal project), FEDEX_CLIENT_SECRET (your project's secret key), FEDEX_ACCOUNT_NUMBER (your FedEx customer account number, found in your FedEx account profile), and FEDEX_SANDBOX (set to 'true' for Preview deployments and 'false' for Production). None of these should have NEXT_PUBLIC_ prefix since all FedEx calls are server-side. For Production deployments, make sure you've applied for and received live FedEx API credentials through the FedEx Developer Portal — sandbox credentials only work against the sandbox API URL. After saving variables and deploying, test the rate calculator with real ZIP codes. The FedEx sandbox returns realistic-looking rates but uses test data. Once you've verified the integration works in sandbox, request production credentials through the FedEx Developer Portal to go live with real shipment creation and charges.

**Expected result:** Vercel deployment succeeds with FedEx credentials injected. The shipping rate calculator returns real FedEx rate quotes in the deployed app, and the tracking page shows scan events for real tracking numbers.

## Best practices

- Cache OAuth2 tokens for their full lifetime (3600 seconds minus a safety buffer) to avoid requesting a new token on every API call
- Use separate FedEx credentials (FEDEX_SANDBOX=true) for Vercel Preview deployments to avoid accidentally triggering real charges during development
- Always validate shipping addresses before creating shipments — invalid addresses cause label creation failures that waste both API calls and time
- Handle FedEx's error response structure carefully — error details appear in the alerts array alongside successful data, so check for errors even when the HTTP status is 200
- Store generated shipment tracking numbers in your database immediately after label creation in case the response needs to be recovered later
- Use FedEx's real-time rates rather than hardcoded rate estimates — shipping rates change based on fuel surcharges and destination zones
- Implement a graceful fallback for when FedEx API is unavailable — display a message asking users to contact support rather than showing a blank rate table

## Use cases

### Shipping Rate Calculator

A tool where users enter origin and destination ZIP codes plus package dimensions and weight to get real-time FedEx shipping quotes across all service types (Ground, 2Day, Overnight, etc.). Displays estimated delivery dates and prices in a comparison table.

Prompt example:

```
Create a shipping rate calculator with inputs for origin ZIP, destination ZIP, package weight (lbs), length, width, height (inches), and a 'Get Rates' button. Show results in a comparison table with columns: Service Type, Delivery Date, List Rate, and an 'Use This Rate' button. Include a loading state while rates load from /api/fedex/rates. Clean white card design with FedEx purple accents.
```

### Package Tracking Dashboard

A tracking page where users enter a FedEx tracking number to see the full shipment journey — scan events with locations and timestamps, current status, and estimated delivery. Can be embedded in an e-commerce order confirmation page.

Prompt example:

```
Build a package tracking page with a tracking number input, a status badge showing current status (In Transit, Delivered, etc.) with an appropriate icon, a vertical timeline of scan events each showing date/time, location, and event description, and a summary card at the top showing estimated delivery and origin/destination. Data from /api/fedex/track. Modern timeline design with FedEx orange for active events.
```

### E-commerce Checkout Shipping Options

A shipping options selector embedded in an e-commerce checkout flow. Fetches available FedEx services for the customer's address, displays prices and delivery estimates, and stores the selected service for order fulfillment.

Prompt example:

```
Design a shipping options selector component for a checkout page showing 3-5 FedEx shipping options as radio button cards. Each card shows service name (Economy, Priority, Overnight), estimated delivery date, and price. Mark one as 'Best Value'. When a user selects an option and clicks Continue, post the selection to /api/fedex/select-rate. Loading skeleton while fetching from /api/fedex/rates. Minimal clean checkout style.
```

## Troubleshooting

### OAuth token request returns 401 or 'Invalid credentials'

Cause: The FEDEX_CLIENT_ID or FEDEX_CLIENT_SECRET is incorrect. FedEx credentials are case-sensitive and should be copied exactly from the Developer Portal. Also verify you're using sandbox credentials against the sandbox URL and production credentials against the production URL.

Solution: Re-copy credentials from the FedEx Developer Portal project page. Confirm FEDEX_SANDBOX is set correctly for each Vercel environment. Check that the grant_type is exactly 'client_credentials' and the request uses Content-Type: application/x-www-form-urlencoded, not JSON.

### Rate quote API returns empty rateReplyDetails array

Cause: FedEx may not offer service to the requested destination postal code combination, the package dimensions or weight are outside FedEx's acceptable range, or the sandbox account doesn't have all service types enabled.

Solution: Test with well-known US ZIP codes (e.g., 10001 for New York to 90210 for Beverly Hills). Verify package weight is positive and dimensions are realistic. Check if FedEx Ground service is enabled on your account — not all accounts have all services. Review the raw FedEx response for specific error alerts in data.output.alerts.

```
// Log FedEx alerts for debugging:
const alerts = data.output?.alerts || [];
console.log('FedEx alerts:', alerts);
```

### Tracking returns 'No tracking data found' for a valid tracking number

Cause: The tracking number may be from a different carrier, the shipment hasn't been created yet in FedEx's system, or the tracking number format includes spaces or dashes that need to be stripped.

Solution: Strip all spaces and hyphens from tracking numbers before sending to the API: const cleanedNumber = trackingNumber.replace(/[\s-]/g, ''). Verify the tracking number is a FedEx tracking number (FedEx numbers are typically 12, 15, 20, or 22 digits). New shipments may take 1-4 hours to appear in tracking.

```
const cleanedNumber = trackingNumber.replace(/[\s-]/g, '');
```

### FEDEX_CLIENT_ID is undefined in the Vercel function despite being set in environment variables

Cause: The variable may have been added to Vercel after the current deployment was built, or it's accidentally prefixed with NEXT_PUBLIC_ which behaves differently from non-prefixed variables.

Solution: Verify the exact variable name in Vercel Dashboard → Settings → Environment Variables. Trigger a full redeploy (not just a redeploy with cached build) after adding new variables. Check that no NEXT_PUBLIC_ prefix was accidentally added.

## Frequently asked questions

### Do I need a FedEx account to use the API, or just developer credentials?

You need both. Developer credentials (client ID and secret) come from registering at developer.fedex.com and creating a project. A FedEx account number (separate from developer credentials) is required for rate quotes and shipment creation — it identifies who is shipping and who is billed. If you don't have a FedEx account, you can create one at fedex.com/open-account.

### How long does it take to move from sandbox to production FedEx API?

After completing development in the sandbox, you apply for production access through the FedEx Developer Portal. Approval typically takes 1-3 business days. FedEx reviews your integration and may require you to demonstrate that you're implementing shipping correctly. Once approved, you receive production credentials (different from sandbox credentials) to swap in your environment variables.

### Can I generate shipping labels and print them from my V0 app?

Yes. The FedEx Ship API (/ship/v1/shipments) creates shipments and returns a base64-encoded label image (PDF or PNG). Your API route decodes this and returns it as a downloadable file, or as a data URL that renders in a browser for printing. Label generation requires both developer credentials and a valid FedEx account number with active billing.

### What happens to cached OAuth tokens when Vercel serverless functions scale?

Module-level token caching in Vercel serverless functions persists within the same function instance's lifetime (warm instance) but is not shared across different instances. When traffic spikes create new function instances, each will request its own token. This is acceptable — FedEx allows many concurrent valid tokens per client credentials. For cross-instance token sharing, store tokens in Upstash Redis using the Vercel Marketplace integration.

### Is the FedEx REST API replacing the old SOAP API?

Yes. FedEx launched the REST API in 2022 and announced the SOAP API (FedEx Web Services) will be retired. FedEx has extended the SOAP deprecation timeline multiple times, but new integrations should use the REST API exclusively. This guide uses the current REST API at apis.fedex.com and apis-sandbox.fedex.com.

---

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