# How to Integrate Bolt.new with AfterShip

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

## TL;DR

Integrate AfterShip's multi-carrier tracking API into your Bolt.new app to track shipments across 1,100+ carriers with a single API. Create trackings with carrier and tracking number, fetch status and history, and build a unified shipment dashboard. HTTP/JSON with API key auth — works in Bolt's WebContainer for outbound calls. Webhook notifications for real-time status updates require a deployed URL.

## Build a Multi-Carrier Shipment Tracking Dashboard with AfterShip and Bolt.new

AfterShip's core value proposition is carrier normalization: instead of writing separate integration code for FedEx, UPS, DHL, USPS, Canada Post, and hundreds of other carriers — each with different APIs, authentication methods, and response formats — AfterShip provides a single API that works identically regardless of carrier. You create a tracking record with a carrier slug and tracking number, and AfterShip polls the carrier, normalizes the response, and returns a consistent data structure with standardized status tags.

For e-commerce businesses, logistics platforms, and supply chain tools built in Bolt.new, this is a significant time saver. The tracking status tags that AfterShip normalizes include: `InfoReceived` (label created, not yet picked up), `InTransit` (on its way), `OutForDelivery`, `Delivered`, `AttemptFail`, `Exception` (failed delivery, customs hold, lost), and `Expired` (no update in 30+ days). By building against these normalized tags, your UI code works correctly for all 1,100+ supported carriers.

The API is HTTP/JSON with API key authentication — a clean, REST-based design with no WebSocket requirements, no OAuth flows, and no native modules. Outbound calls to AfterShip work in Bolt's WebContainer during development. The limitation applies only to webhooks: AfterShip sends real-time status updates via HTTP POST to your registered webhook URL, which requires a publicly accessible endpoint. Test data fetching in the preview and test webhooks after deploying to Netlify or Bolt Cloud.

## Before you start

- An AfterShip account (free plan available at app.aftership.com) and API key
- A Bolt.new project using Next.js (for server-side API routes)
- Some tracking numbers from real shipments for testing (or AfterShip's test tracking numbers)
- A deployed URL on Netlify or Bolt Cloud for testing webhook notifications
- Optionally: Supabase for storing tracking records and customer data alongside AfterShip data

## Step-by-step guide

### 1. Get Your AfterShip API Key

Go to app.aftership.com and create a free account. After signing in, navigate to Settings → API Keys (or find it in your account settings). Click 'Generate New API Key'. Give it a name (e.g., 'Bolt Dashboard') and copy the key. AfterShip's free plan includes 100 trackings per month with full API access — sufficient for development and small-scale apps. The paid plans start at $11/month for 2,000 trackings per month. The API key is used in an `as-api-key` request header for all API calls (note: the header name is `as-api-key`, not `Authorization` or `X-API-Key`). Test your key immediately: create a tracking record using AfterShip's REST API explorer in their documentation, or send a test request to `https://api.aftership.com/tracking/2023-10/trackings` with your API key header. AfterShip also provides a list of carrier slugs at `/couriers` — each carrier has a unique slug string (e.g., `fedex`, `ups`, `dhl`, `usps`) used when creating tracking records.

```
// lib/aftership.ts
const AFTERSHIP_BASE = 'https://api.aftership.com/tracking/2023-10';

export type TrackingTag =
  | 'Pending'
  | 'InfoReceived'
  | 'InTransit'
  | 'OutForDelivery'
  | 'AttemptFail'
  | 'Delivered'
  | 'AvailableForPickup'
  | 'Exception'
  | 'Expired';

export interface TrackingEvent {
  slug: string;
  city: string | null;
  state: string | null;
  country_iso3: string | null;
  message: string;
  tag: TrackingTag;
  subtag: string;
  subtag_message: string;
  created_at: string;
  updated_at: string;
  location: string | null;
}

export interface Tracking {
  id: string;
  tracking_number: string;
  slug: string;
  title: string | null;
  customer_name: string | null;
  tag: TrackingTag;
  subtag_message: string;
  created_at: string;
  updated_at: string;
  last_updated_at: string;
  expected_delivery: string | null;
  origin_country_iso3: string | null;
  destination_country_iso3: string | null;
  checkpoints: TrackingEvent[];
}

export async function aftershipFetch<T>(
  path: string,
  options: RequestInit = {}
): Promise<{ meta: { code: number; message: string }; data: T }> {
  const res = await fetch(`${AFTERSHIP_BASE}${path}`, {
    ...options,
    headers: {
      'as-api-key': process.env.AFTERSHIP_API_KEY!,
      'Content-Type': 'application/json',
      ...options.headers,
    },
  });

  const json = await res.json();
  if (!res.ok) throw new Error(`AfterShip error ${res.status}: ${json.meta?.message}`);
  return json;
}
```

**Expected result:** The AfterShip utility is ready with proper authentication. API calls to AfterShip will return structured tracking data.

### 2. Create and Fetch Trackings

AfterShip's tracking flow has two steps: first create a tracking record (POST to `/trackings`), then fetch its current status (GET from `/trackings/{slug}/{tracking_number}`). When creating a tracking, you provide at minimum the `tracking_number` and optionally the `slug` (carrier code like `fedex` or `ups`). If you don't know the carrier, AfterShip can auto-detect it based on the tracking number format — pass the tracking number without a slug and AfterShip identifies the carrier automatically, though this uses more API quota and may be less accurate. After creation, AfterShip immediately begins polling the carrier for status updates. Subsequent GET requests return the current tracking status including all checkpoint events. Each checkpoint has: a timestamp (`created_at`), location (city, country), message, and standardized `tag` and `subtag`. Build API routes for both creating trackings (from a form in your UI) and fetching tracking details (for the tracking page).

```
// app/api/aftership/trackings/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { aftershipFetch } from '@/lib/aftership';

export async function GET(request: NextRequest) {
  const page = request.nextUrl.searchParams.get('page') ?? '1';
  const limit = request.nextUrl.searchParams.get('limit') ?? '20';

  try {
    const result = await aftershipFetch<{ trackings: unknown[]; total: number }>(
      `/trackings?page=${page}&limit=${limit}`
    );
    return NextResponse.json(result.data);
  } catch (error) {
    return NextResponse.json({ error: String(error) }, { status: 500 });
  }
}

export async function POST(request: NextRequest) {
  const body = await request.json();
  const { tracking_number, slug, title, customer_name } = body;

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

  try {
    const result = await aftershipFetch('/trackings', {
      method: 'POST',
      body: JSON.stringify({
        tracking: {
          tracking_number,
          slug: slug || undefined,
          title: title || undefined,
          customer_name: customer_name || undefined,
        },
      }),
    });
    return NextResponse.json(result.data);
  } catch (error) {
    return NextResponse.json({ error: String(error) }, { status: 500 });
  }
}
```

**Expected result:** POST /api/aftership/trackings creates a new tracking. GET /api/aftership/trackings returns a paginated list of all trackings with current status.

### 3. Build the Tracking Dashboard UI

With API routes serving tracking data, build the dashboard UI. A well-designed shipment tracking dashboard has three main views: a tracking list (all active shipments with status badges), a tracking detail (full checkpoint timeline for a single shipment), and an add tracking form (tracking number + optional carrier selection). For the status badges, map AfterShip's normalized tags to colors: `Delivered` → green, `InTransit` / `OutForDelivery` → blue, `InfoReceived` / `Pending` → gray, `AttemptFail` / `Exception` / `Expired` → red. The checkpoint timeline for the detail view shows carrier scan events in reverse chronological order with location, timestamp, and event description. The carrier slug list is available from the AfterShip API at `/couriers` — fetch it once and cache it to populate the carrier selector dropdown. Since AfterShip supports 1,100+ carriers, a searchable autocomplete is better than a plain dropdown for carrier selection.

**Expected result:** The tracking dashboard shows all shipments with color-coded status badges. Clicking a shipment shows the full checkpoint timeline with carrier scan history.

### 4. Set Up AfterShip Webhooks for Real-Time Updates

AfterShip webhooks send HTTP POST requests to your app whenever a tracking status changes — carrier picked up the package, package is out for delivery, delivered, or an exception occurred. This is the mechanism for sending automated customer notifications. During development in Bolt's WebContainer, webhooks cannot be received because the preview URL is dynamic and unreachable from AfterShip's servers. Deploy your app first. Once deployed, go to AfterShip Dashboard → Settings → Notifications → Webhook. Add your deployed URL: `https://your-app.netlify.app/api/aftership/webhook`. Select the events you want to receive (usually: InTransit, OutForDelivery, Delivered, Exception). AfterShip sends a POST request to your webhook URL with a JSON payload containing the full tracking object including the updated tag and latest checkpoint. Verify the webhook using AfterShip's `hmac` header — AfterShip signs the payload with your webhook secret (found in AfterShip settings) using HMAC-SHA256.

```
// app/api/aftership/webhook/route.ts
// NOTE: Webhooks only work on deployed apps (Netlify/Bolt Cloud)
// The WebContainer cannot receive incoming HTTP connections
import { NextRequest, NextResponse } from 'next/server';
import crypto from 'crypto';

export async function POST(request: NextRequest) {
  const body = await request.text();
  const signature = request.headers.get('hmac') ?? '';

  const webhookSecret = process.env.AFTERSHIP_WEBHOOK_SECRET;
  if (webhookSecret) {
    const expected = crypto
      .createHmac('sha256', webhookSecret)
      .update(body)
      .digest('hex');
    if (signature !== expected) {
      return NextResponse.json({ error: 'Invalid signature' }, { status: 401 });
    }
  }

  const payload = JSON.parse(body);
  const tracking = payload.msg;

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

  const tag = tracking.tag as string;
  const trackingNumber = tracking.tracking_number as string;
  const customerName = tracking.customer_name as string;

  console.log(`AfterShip webhook: ${trackingNumber} is now ${tag}`);

  switch (tag) {
    case 'OutForDelivery':
      console.log(`Notifying ${customerName}: Package out for delivery`);
      // Send email via SendGrid here
      break;
    case 'Delivered':
      console.log(`Notifying ${customerName}: Package delivered`);
      break;
    case 'Exception':
      console.log(`Alert: Delivery exception for ${trackingNumber}`);
      break;
  }

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

**Expected result:** After deploying and registering the webhook URL in AfterShip, status changes trigger POST requests to your webhook handler, enabling real-time notifications.

## Best practices

- Use AfterShip's normalized status tags (InTransit, Delivered, Exception, etc.) to drive your UI logic — never parse raw carrier messages, which vary unpredictably
- Store tracking records in your own database (Supabase/Bolt Database) alongside AfterShip IDs so you can associate trackings with orders and customers
- Always verify webhook HMAC signatures before processing events to prevent spoofed status changes from malicious actors
- Register webhook URLs only after deploying — Bolt's WebContainer cannot receive incoming HTTP connections
- Cache the carrier list (/couriers endpoint) in your database or build config rather than fetching it on every page load — it changes rarely
- Handle the 'Tracking already exists' error gracefully by fetching and returning the existing tracking rather than showing an error to the user
- Normalize tracking numbers to uppercase and trim whitespace before sending to AfterShip — carrier tracking numbers are case-sensitive

## Use cases

### E-Commerce Order Tracking Page

Add a 'Track My Order' page to an e-commerce app where customers enter their order number or tracking number to see real-time shipment status, estimated delivery date, and a timeline of carrier scan events.

Prompt example:

```
Build an order tracking page for my e-commerce app using the AfterShip API. Users enter a tracking number and optionally select their carrier. The page shows: current status with an icon (In Transit, Out for Delivery, Delivered), estimated delivery date, origin and destination, and a timeline of all carrier scan events with timestamp, location, and description. Use AfterShip API with the key from environment variables.
```

### Unified Shipment Dashboard for Operations Teams

An internal dashboard showing all active shipments across multiple carriers, sorted by expected delivery date. Color-coded status indicators highlight exceptions, late deliveries, and same-day deliveries. Filter by carrier, status, and date range.

Prompt example:

```
Build an operations shipment dashboard using AfterShip. Show all tracked shipments in a table with: tracking number, carrier, recipient name, current status (color-coded badge), last update location, and expected delivery date. Add filters for status (In Transit, Delivered, Exception, All) and sort by expected delivery ascending. Fetch from /api/aftership/trackings. Highlight Exception status rows in red.
```

### Automated Shipment Notifications

When AfterShip detects a status change (package out for delivery, delivered, exception), send the customer an email notification via SendGrid with the updated status and carrier link. Triggered by AfterShip webhooks after deployment.

Prompt example:

```
Set up AfterShip webhooks to send email notifications when a shipment status changes. When AfterShip sends a webhook to /api/aftership/webhook, check the tag field: if 'OutForDelivery' send an email via SendGrid saying 'Your package is out for delivery'; if 'Delivered' send 'Your package has been delivered'; if 'Exception' send 'There's an issue with your delivery'. Look up the customer email from Supabase using the tracking number.
```

## Troubleshooting

### API calls return 401 Unauthorized with the correct API key

Cause: The AfterShip API requires the header name 'as-api-key' — not 'Authorization', 'X-API-Key', or 'api-key'. Using the wrong header name results in a 401 even with a valid key.

Solution: Verify your request headers include exactly 'as-api-key': process.env.AFTERSHIP_API_KEY. Check AfterShip's API documentation for the current header name — it differs from most other APIs.

```
headers: {
  'as-api-key': process.env.AFTERSHIP_API_KEY!,
  'Content-Type': 'application/json',
}
```

### Creating a tracking returns 'Tracking already exists'

Cause: AfterShip only allows one tracking record per tracking number per carrier combination. Attempting to create a duplicate returns a 4002 error code.

Solution: Check if the tracking already exists before creating it by calling GET /trackings/{slug}/{tracking_number}. If it exists, fetch and return the existing record instead of creating a new one.

### Webhook events are not arriving after deployment

Cause: The webhook URL is not registered in AfterShip's settings, the wrong events are selected, or the URL is unreachable from AfterShip's servers.

Solution: In AfterShip Dashboard → Settings → Notifications → Webhook, verify your deployed URL is listed and active. Use AfterShip's 'Test Webhook' button to send a test payload. Ensure your Netlify URL is publicly accessible — check that there's no authentication middleware blocking the /api/aftership/webhook route.

### Tracking checkpoints are empty even though the package is in transit

Cause: AfterShip polls carriers periodically — a newly created tracking may not have checkpoint data immediately. Some carriers take 15-60 minutes to return first checkpoint data after creation.

Solution: Wait 15-30 minutes after creating a tracking and then fetch it again. Add a 'Refresh' button to your UI to manually re-fetch the latest status. The `last_updated_at` field shows when AfterShip last polled the carrier.

## Frequently asked questions

### How do I connect Bolt.new to AfterShip for shipment tracking?

Get a free AfterShip API key from app.aftership.com, add it to your .env.local file as AFTERSHIP_API_KEY, create a Next.js API route that adds the 'as-api-key' header to requests, and proxy tracking queries from your frontend. The full setup is covered in this guide and takes about 30 minutes.

### Does AfterShip work in Bolt's WebContainer preview?

Outbound API calls to AfterShip work in the preview for creating and fetching trackings. The WebContainer limitation only affects webhooks — AfterShip sends HTTP POST requests to your app for real-time status updates, which require a publicly accessible URL. Test data fetching in the preview and test webhooks after deploying to Netlify or Bolt Cloud.

### How many carriers does AfterShip support?

AfterShip supports 1,100+ carriers globally as of 2026, including all major carriers (FedEx, UPS, DHL, USPS, Royal Mail, Australia Post, Canada Post, Aramex) and hundreds of regional and last-mile carriers. The full list with carrier slugs is available via GET /couriers in the AfterShip API.

### Can AfterShip auto-detect the carrier from the tracking number?

Yes — when creating a tracking without specifying a slug, AfterShip attempts to identify the carrier from the tracking number format. However, auto-detection is less reliable for carriers with ambiguous number formats and uses more API quota. Providing the carrier slug explicitly when known is more reliable.

### How do I deploy a Bolt.new app with AfterShip webhooks to Netlify?

Deploy via Bolt Settings → Applications → Netlify → Publish. Add AFTERSHIP_API_KEY and AFTERSHIP_WEBHOOK_SECRET to Netlify's environment variables. After deployment, register your Netlify URL in AfterShip Dashboard → Settings → Notifications → Webhook. Select the events you want (Delivered, Exception, OutForDelivery) and save. AfterShip will now POST to your webhook endpoint when shipment statuses change.

---

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