# How to Integrate Bolt.new with Shippo

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

## TL;DR

Connect Bolt.new to Shippo with a simple API token — no OAuth required. Shippo's REST API provides multi-carrier shipping rate comparison, label creation, and shipment tracking across 70+ carriers including USPS, UPS, FedEx, and DHL in a single API call. Store SHIPPO_API_TOKEN in your .env file and call Shippo's API from a Next.js API route to keep the token secure. Rate comparison and label creation work in Bolt's WebContainer preview.

## Build Multi-Carrier Shipping Rate Comparison and Label Creation with Bolt.new

Shippo's biggest advantage is the unified API: one API call returns rates from all the carriers you have enabled — USPS, UPS, FedEx, DHL, and dozens of regional carriers — without having to manage separate integrations for each. For e-commerce apps built in Bolt.new, this means you can show customers real-time shipping cost comparisons before checkout, automatically select the cheapest option, or let customers choose based on speed versus cost. The rate comparison feature alone — getting live rates from multiple carriers in a single request — saves weeks of work compared to integrating each carrier API individually.

Shippo's API is unusually developer-friendly: authentication is a single Bearer token (no OAuth, no key rotation dance), the request format is straightforward (sender address, recipient address, parcel dimensions and weight), and the response is a clean JSON array of rate objects sorted by price. Each rate includes the carrier name, service level, estimated delivery days, price, and a rate_object_id that you use to purchase the label. The entire flow from 'here is a package going from address A to address B' to 'here is a downloadable label PDF' takes about 3 API calls.

For Bolt.new development, Shippo's API calls are all outbound HTTP — compatible with Bolt's WebContainer. You can build and test the rate comparison UI, simulate label purchases (Shippo has a test token that generates dummy labels), and display tracking information all within the Bolt preview. The only WebContainer limitation applies to Shippo's tracking webhooks, which push status updates to your app when a package moves — those webhooks need a deployed URL. For development, you can poll Shippo's tracking API directly instead of waiting for webhooks.

## Before you start

- A Shippo account at goshippo.com (free to create — pay only for labels purchased)
- Your Shippo API token from the goshippo.com dashboard under API → API Keys (use the test token for development)
- A Bolt.new project using Next.js for server-side API routes that keep the Shippo token secure
- Basic understanding of shipping concepts: sender/recipient addresses, parcel dimensions and weight, carrier service levels
- Optional: a Supabase database for storing orders with shipping information

## Step-by-step guide

### 1. Get Your Shippo API Token and Set Up the Project

Go to goshippo.com and create a free account. After signing in, navigate to the API section in your dashboard — usually found under Settings → API Keys or directly at app.goshippo.com/api. Shippo provides two tokens: a test token (prefixed with 'shippo_test_') and a live token (prefixed with 'shippo_live_'). Use the test token during development — it generates real rate quotes from carriers but creates dummy labels rather than actual billable shipments. This means you can build and test the entire integration without incurring any label costs. The test token's rate quotes are real carrier rates, so your pricing UI will look exactly like production. Switch to the live token only when you are ready to create real, purchasable labels. Add SHIPPO_API_TOKEN to your .env file using the test token. Also add VITE_SHIPPO_API_TOKEN if you need to reference it in client-side code for display purposes (such as showing the API environment in a dev indicator) — but never use this for actual API calls. Create a lib/shippo.ts helper that wraps fetch calls to Shippo's API with the Authorization header, base URL, and error handling. Shippo's base URL is https://api.goshippo.com and all endpoints are lowercase REST paths. The API version is specified via the Shippo-API-Version header (use '2018-02-08' for the stable version). Shippo's error responses include an array-style error message — handle these in your wrapper function.

```
// lib/shippo.ts
export async function shippoRequest<T>(
  endpoint: string,
  options: RequestInit = {}
): Promise<T> {
  const token = process.env.SHIPPO_API_TOKEN;

  const response = await fetch(`https://api.goshippo.com${endpoint}`, {
    ...options,
    headers: {
      'Authorization': `ShippoToken ${token}`,
      'Shippo-API-Version': '2018-02-08',
      'Content-Type': 'application/json',
      ...(options.headers || {}),
    },
  });

  if (!response.ok) {
    const errorText = await response.text();
    throw new Error(`Shippo API error ${response.status}: ${errorText}`);
  }

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

**Expected result:** The Shippo API connection is verified. The lib/shippo.ts helper is ready to use in API routes throughout the app.

### 2. Build the Rate Comparison API Route

Shippo's rate comparison requires creating a Shipment object — the core resource in Shippo's API. A Shipment object contains the address_from (sender), address_to (recipient), parcels (dimensions and weight), and optionally carrier_accounts to restrict which carriers return rates. When you create a Shipment, Shippo automatically contacts all enabled carriers and returns an array of Rate objects. Each Rate includes: provider (e.g., 'USPS'), servicelevel.name (e.g., 'Priority Mail'), amount (price as a string), currency, estimated_days, and object_id (the rate ID you use to purchase a label). The shipment creation endpoint is POST /shipments. Addresses can be inline objects in the shipment creation request, or you can pre-create Address objects and reference them by ID for reuse. For the rate calculator, use inline addresses for simplicity. Parcel dimensions use the Parcel object: length, width, height (in inches by default), and weight (in ounces or pounds depending on your setting). Shippo returns asynchronously for some carriers — if the status field of the Shipment is 'QUEUED' rather than 'SUCCESS', poll the GET /shipments/:object_id endpoint until status becomes 'SUCCESS' and rates are populated. In practice, most modern carrier connections return synchronously. During development in Bolt's WebContainer, all outbound API calls to Shippo work correctly. You can test rate comparison with real addresses and real carrier rates without leaving the Bolt preview.

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

interface ShippoRate {
  object_id: string;
  provider: string;
  servicelevel: { name: string };
  amount: string;
  currency: string;
  estimated_days: number;
}

interface ShippoShipment {
  object_id: string;
  status: string;
  rates: ShippoRate[];
}

export async function POST(request: NextRequest) {
  const { recipientName, addressLine1, city, state, zip, country, weightLbs, lengthIn, widthIn, heightIn } =
    await request.json();

  const shipment = await shippoRequest<ShippoShipment>('/shipments', {
    method: 'POST',
    body: JSON.stringify({
      address_from: {
        name: 'Your Business Name',
        street1: '123 Main St', // Replace with your sender address
        city: 'San Francisco',
        state: 'CA',
        zip: '94105',
        country: 'US',
      },
      address_to: {
        name: recipientName,
        street1: addressLine1,
        city,
        state,
        zip,
        country: country || 'US',
      },
      parcels: [
        {
          length: String(lengthIn),
          width: String(widthIn),
          height: String(heightIn),
          distance_unit: 'in',
          weight: String(weightLbs),
          mass_unit: 'lb',
        },
      ],
      async: false, // Wait for all rates synchronously
    }),
  });

  const rates = (shipment.rates || []).map((rate) => ({
    objectId: rate.object_id,
    provider: rate.provider,
    serviceName: rate.servicelevel.name,
    amount: parseFloat(rate.amount),
    currency: rate.currency,
    estimatedDays: rate.estimated_days,
  }));

  // Sort by price ascending
  rates.sort((a, b) => a.amount - b.amount);

  return NextResponse.json({ rates, shipmentId: shipment.object_id });
}
```

**Expected result:** POSTing to /api/shipping/rates returns an array of carrier rates sorted by price. USPS, UPS, FedEx, and other enabled carriers are compared in a single API call.

### 3. Purchase a Label and Handle Tracking

Once a rate is selected (either by the customer or automatically), purchase the label by POSTing the rate's object_id to Shippo's /transactions endpoint. A Transaction in Shippo represents a purchased label. The response includes the label_url (a direct URL to download the label as PDF or PNG), tracking_number, tracking_url_provider (the carrier's tracking page), and status. Label status transitions are: QUEUED → WAITING → SUCCESS (or ERROR if something went wrong). For most carriers, the label is generated synchronously when async: false is passed. Store the tracking_number and label_url in your Supabase order record for future reference. For tracking status updates after deployment, Shippo supports webhooks that POST to your app when a package's tracking status changes. Register your webhook URL in the Shippo dashboard under API → Webhooks. During development in Bolt's WebContainer, incoming tracking webhooks cannot reach the browser-based runtime because the WebContainer has no public URL — this is a fundamental limitation of Bolt's development environment. Use the tracking poll API (GET /tracks/:carrier/:tracking_number) during development, and set up proper webhooks after deploying to Netlify or Vercel. When you do deploy, webhooks will fire to your production URL when packages are scanned at carrier facilities, updating order status in real time.

```
// app/api/shipping/label/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { shippoRequest } from '@/lib/shippo';
import { createClient } from '@supabase/supabase-js';

interface ShippoTransaction {
  status: string;
  tracking_number: string;
  label_url: string;
  tracking_url_provider: string;
  messages: Array<{ text: string }>;
}

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

  const transaction = await shippoRequest<ShippoTransaction>('/transactions', {
    method: 'POST',
    body: JSON.stringify({
      rate: rateObjectId,
      label_file_type: 'PDF',
      async: false,
    }),
  });

  if (transaction.status !== 'SUCCESS') {
    const messages = transaction.messages?.map((m) => m.text).join('; ');
    return NextResponse.json({ error: `Label creation failed: ${messages}` }, { status: 400 });
  }

  // Update Supabase order with tracking info
  if (orderId) {
    const supabase = createClient(
      process.env.NEXT_PUBLIC_SUPABASE_URL!,
      process.env.SUPABASE_SERVICE_ROLE_KEY!
    );
    await supabase
      .from('orders')
      .update({
        tracking_number: transaction.tracking_number,
        label_url: transaction.label_url,
        carrier_tracking_url: transaction.tracking_url_provider,
        status: 'label_created',
      })
      .eq('id', orderId);
  }

  return NextResponse.json({
    trackingNumber: transaction.tracking_number,
    labelUrl: transaction.label_url,
    trackingUrl: transaction.tracking_url_provider,
  });
}
```

**Expected result:** Clicking 'Purchase Label' creates a real shipping label in Shippo, returns a downloadable PDF URL, and stores the tracking number in the Supabase order record.

### 4. Deploy to Netlify and Register Tracking Webhooks

Your Bolt.new app's shipping rate comparison and label creation features work completely in the WebContainer preview — Shippo's API calls are all outbound HTTP, which Bolt's WebContainer handles without issues. The one feature that requires deployment first is Shippo's tracking webhooks. Webhooks are HTTP POST requests that Shippo sends to your app when a package's tracking status changes — for example, when USPS scans the package at a sorting facility. These incoming webhooks cannot reach Bolt's WebContainer during development because the browser-based runtime has no public URL accessible from the internet. To set up tracking webhooks: first deploy your app to Netlify via Bolt's Settings → Applications. After deployment, copy your Netlify URL. In the Shippo dashboard (app.goshippo.com), go to API → Webhooks and click 'Add a Webhook'. Enter your webhook URL (e.g., https://your-app.netlify.app/api/shipping/webhook), select the event type 'track_updated', and save. Create the webhook handler route in Bolt before deploying. The webhook payload from Shippo includes the tracking number, carrier, status, substatus, and an events array. Use Supabase's service role key (not the anon key) in your webhook handler to update order records without requiring user authentication. After deployment, set SHIPPO_API_TOKEN and SUPABASE_SERVICE_ROLE_KEY as environment variables in Netlify's Site Configuration → Environment Variables and redeploy.

```
// app/api/shipping/webhook/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { createClient } from '@supabase/supabase-js';

const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.SUPABASE_SERVICE_ROLE_KEY!
);

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

  // Shippo tracking webhook payload structure
  const trackingNumber = payload.data?.tracking_number;
  const carrier = payload.data?.carrier;
  const status = payload.data?.tracking_status?.status; // PRE_TRANSIT, TRANSIT, DELIVERED, RETURNED, FAILURE, UNKNOWN
  const statusDetails = payload.data?.tracking_status?.status_details;
  const estimatedDelivery = payload.data?.eta;

  if (!trackingNumber) {
    return NextResponse.json({ received: true });
  }

  // Update the order in Supabase
  await supabase
    .from('orders')
    .update({
      shipping_status: status,
      shipping_status_details: statusDetails,
      estimated_delivery: estimatedDelivery,
      last_tracking_update: new Date().toISOString(),
    })
    .eq('tracking_number', trackingNumber);

  return NextResponse.json({ received: true });
}
```

**Expected result:** After deployment, Shippo sends tracking status updates to your webhook endpoint whenever a package moves through the carrier network. Order statuses update automatically without manual polling.

## Best practices

- Use Shippo's test token (shippo_test_) during development — it returns real carrier rates and creates dummy labels without any charges
- Pass async: false when creating Shippo Shipment objects to get all carrier rates synchronously in the response, avoiding polling logic
- Always check transaction.status === 'SUCCESS' before using the label_url — failed label creation returns non-SUCCESS status with error messages in the messages array
- Register Shippo tracking webhooks with your deployed Netlify URL, not the Bolt WebContainer preview — incoming webhooks cannot reach the browser-based runtime during development
- Use the Shippo-specific 'ShippoToken' Authorization header scheme, not 'Bearer' — this is the most common authentication error when integrating Shippo
- Store tracking numbers and label URLs in Supabase immediately after label creation — Shippo test label URLs expire and should not be the only copy
- Cache rate quotes for short periods (5-10 minutes) for the same address/parcel combination — rates do not change frequently and caching reduces API calls

## Use cases

### Shipping Rate Calculator at Checkout

Add a shipping rate calculator to an e-commerce checkout flow. The customer enters their delivery address, your app calls Shippo to get live rates from multiple carriers, and the customer selects their preferred option before completing the order. Display carrier name, service level (ground, express), estimated delivery days, and price.

Prompt example:

```
Build a shipping rate calculator component for my checkout page. It has fields for: recipient name, address line 1, city, state, zip code, and country. When the customer clicks 'Calculate Shipping', POST to /api/shipping/rates with the recipient address and a fixed parcel size (10x8x4 inches, 2 lbs). Display the returned rates as a list of selectable options showing carrier logo placeholder, service name (e.g., 'USPS Priority Mail'), estimated delivery (e.g., '2-3 days'), and price. The customer selects a rate and it is stored in the order state.
```

### Automated Label Generation for Orders

Automatically generate shipping labels when an order is marked as ready to ship. Pull the shipping address from the order record, create a shipment in Shippo, select the cheapest available carrier rate, purchase the label, and store the label URL and tracking number in the order record.

Prompt example:

```
Build a 'Generate Label' flow for my order management page. When an admin clicks 'Generate Label' on an order, POST to /api/shipping/label with the order ID. The API route fetches the order from Supabase (customer name, shipping address, order weight), creates a Shippo shipment, gets rates, selects the cheapest USPS rate, purchases the label, and updates the Supabase order record with tracking_number and label_url. Return the label URL so the admin can click to download and print the label.
```

### Shipment Tracking Dashboard

Build an internal shipment tracking page that shows the current status of all active shipments. Poll Shippo's tracking API for each tracking number and display a live status feed with carrier, current location, status (in transit, out for delivery, delivered), and estimated delivery date.

Prompt example:

```
Build a shipment tracking dashboard. Fetch all orders from Supabase where status is 'shipped' and tracking_number is not null. For each order, call /api/shipping/track?carrier={carrier}&tracking={tracking_number} which calls Shippo's tracking API. Display a table with: order ID, customer name, carrier, tracking number, current status, last update location, and estimated delivery. Color-code status: green for delivered, blue for in transit, yellow for pending, red for exception.
```

## Troubleshooting

### Shippo returns 401 Unauthorized with 'Invalid token' message

Cause: The Authorization header format is wrong — using 'Bearer' instead of the Shippo-specific 'ShippoToken' scheme, or the token itself was copied incorrectly from the dashboard.

Solution: Ensure the Authorization header is exactly 'ShippoToken YOUR_TOKEN_HERE' — not 'Bearer YOUR_TOKEN'. Verify the token matches what is in your Shippo dashboard under API → API Keys. Check for leading or trailing whitespace in the .env file value.

```
// Correct Shippo authorization header:
headers: { 'Authorization': `ShippoToken ${process.env.SHIPPO_API_TOKEN}` }

// WRONG — do not use Bearer:
headers: { 'Authorization': `Bearer ${process.env.SHIPPO_API_TOKEN}` }
```

### Rate comparison returns an empty rates array for a valid shipment

Cause: No carrier accounts are enabled in your Shippo account, the parcel dimensions or weight are outside carrier limits, or the destination country is not supported by any enabled carriers.

Solution: In your Shippo dashboard, go to Carriers and verify at least one carrier is connected and active. For testing, USPS is enabled by default on Shippo test accounts. Also check that the parcel weight is greater than 0 and dimensions are positive numbers. For international shipments, ensure the country code is a valid 2-letter ISO code.

### Label URL returns a 404 error when trying to download the label PDF

Cause: Shippo's test labels have a limited validity period and the URL may have expired, or the transaction status was not 'SUCCESS' when the label URL was stored.

Solution: Always check transaction.status === 'SUCCESS' before using the label_url. For expired test labels, re-purchase the label with the test token to get a fresh URL. Production labels have a longer validity period but should also be stored immediately after creation.

```
if (transaction.status !== 'SUCCESS') {
  const errorMessages = transaction.messages?.map(m => m.text).join(', ');
  throw new Error(`Label creation failed: ${errorMessages}`);
}
// Only use label_url after confirming SUCCESS status
const labelUrl = transaction.label_url;
```

### Tracking webhooks never fire even though packages are moving through the carrier network

Cause: The webhook URL was registered with the Bolt WebContainer preview URL, which is not publicly accessible. Or the webhook was registered but is pointing to the wrong endpoint path.

Solution: Tracking webhooks require your deployed Netlify or Vercel URL — incoming webhooks cannot reach Bolt's WebContainer during development. In the Shippo dashboard, go to API → Webhooks and update the URL to your deployed domain (e.g., https://your-app.netlify.app/api/shipping/webhook). Test the webhook by clicking 'Send Test' in the Shippo dashboard after updating the URL.

## Frequently asked questions

### How do I connect Bolt.new to Shippo?

Get your Shippo API token from app.goshippo.com under API → API Keys. Add it to your .env file as SHIPPO_API_TOKEN. Create a lib/shippo.ts helper that adds the 'Authorization: ShippoToken YOUR_TOKEN' header to fetch calls. All Shippo API calls happen in Next.js API routes — never in client-side code. The rate comparison flow works in Bolt's WebContainer preview during development.

### Does Shippo work in Bolt's WebContainer during development?

Yes. Shippo's API calls are all outbound HTTP requests, which Bolt's WebContainer supports. You can test rate comparison, label creation (with the test token), and tracking status polling all within the Bolt preview. The only feature that requires deployment is Shippo's tracking webhooks — incoming webhooks cannot reach the browser-based WebContainer runtime.

### What is the difference between Shippo's test token and live token?

The test token (prefixed with 'shippo_test_') generates real carrier rate quotes but creates dummy labels that are not billable and cannot be used for real shipments. The live token (prefixed with 'shippo_live_') creates real, printable labels that are charged to your account (pay-per-label). Use the test token during development and only switch to the live token in production.

### How does Shippo's rate comparison work?

Create a Shipment object with the sender address, recipient address, and parcel dimensions/weight using POST /shipments. Set async: false to get all rates synchronously. Shippo contacts all your enabled carrier accounts and returns a rates array in the response. Each rate has the carrier, service level, price, and estimated delivery days. Select a rate and POST its object_id to /transactions to purchase the label.

### Can I set up tracking webhooks during development in Bolt?

No — tracking webhooks require a deployed app with a public HTTPS URL. Shippo's webhooks are incoming HTTP requests to your app, and Bolt's WebContainer during development does not have a public URL that Shippo can reach. Deploy your app to Netlify first, then register your deployed URL (e.g., https://your-app.netlify.app/api/shipping/webhook) in the Shippo dashboard under API → Webhooks. For development testing, use Shippo's polling API (GET /tracks/:carrier/:tracking_number) instead.

---

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