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.
Integration method
AfterShip's REST API uses simple API key authentication over HTTPS, making all outbound calls compatible with Bolt's WebContainer runtime. Create trackings, fetch statuses, and build tracking dashboards through Next.js API routes that proxy requests to AfterShip's endpoints. Webhook notifications for real-time carrier status updates require a deployed URL on Netlify or Bolt Cloud — the WebContainer cannot receive incoming HTTP connections.
Prerequisites
- 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
Get Your AfterShip API Key
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.
Set up the AfterShip API integration in my Next.js app. Add AFTERSHIP_API_KEY to .env.local. Create lib/aftership.ts with a helper function aftershipFetch(path, options) that makes requests to https://api.aftership.com/tracking/2023-10 with the as-api-key header from the environment variable. Export types for Tracking and TrackingEvent.
Paste this in Bolt.new chat
1// lib/aftership.ts2const AFTERSHIP_BASE = 'https://api.aftership.com/tracking/2023-10';34export type TrackingTag =5 | 'Pending'6 | 'InfoReceived'7 | 'InTransit'8 | 'OutForDelivery'9 | 'AttemptFail'10 | 'Delivered'11 | 'AvailableForPickup'12 | 'Exception'13 | 'Expired';1415export interface TrackingEvent {16 slug: string;17 city: string | null;18 state: string | null;19 country_iso3: string | null;20 message: string;21 tag: TrackingTag;22 subtag: string;23 subtag_message: string;24 created_at: string;25 updated_at: string;26 location: string | null;27}2829export interface Tracking {30 id: string;31 tracking_number: string;32 slug: string;33 title: string | null;34 customer_name: string | null;35 tag: TrackingTag;36 subtag_message: string;37 created_at: string;38 updated_at: string;39 last_updated_at: string;40 expected_delivery: string | null;41 origin_country_iso3: string | null;42 destination_country_iso3: string | null;43 checkpoints: TrackingEvent[];44}4546export async function aftershipFetch<T>(47 path: string,48 options: RequestInit = {}49): Promise<{ meta: { code: number; message: string }; data: T }> {50 const res = await fetch(`${AFTERSHIP_BASE}${path}`, {51 ...options,52 headers: {53 'as-api-key': process.env.AFTERSHIP_API_KEY!,54 'Content-Type': 'application/json',55 ...options.headers,56 },57 });5859 const json = await res.json();60 if (!res.ok) throw new Error(`AfterShip error ${res.status}: ${json.meta?.message}`);61 return json;62}Pro tip: AfterShip's header name is 'as-api-key' — not 'Authorization' or 'X-API-Key'. This is a common mistake that causes 401 errors. Double-check the header name in your requests.
Expected result: The AfterShip utility is ready with proper authentication. API calls to AfterShip will return structured tracking data.
Create and Fetch Trackings
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).
Create two AfterShip API routes. app/api/aftership/trackings/route.ts should handle: GET (list all trackings, paginated) and POST (create a new tracking with tracking_number, slug, title, customer_name from request body). app/api/aftership/trackings/[slug]/[trackingNumber]/route.ts should handle GET (fetch a single tracking with all checkpoints). Use the aftershipFetch helper from lib/aftership.ts.
Paste this in Bolt.new chat
1// app/api/aftership/trackings/route.ts2import { NextRequest, NextResponse } from 'next/server';3import { aftershipFetch } from '@/lib/aftership';45export async function GET(request: NextRequest) {6 const page = request.nextUrl.searchParams.get('page') ?? '1';7 const limit = request.nextUrl.searchParams.get('limit') ?? '20';89 try {10 const result = await aftershipFetch<{ trackings: unknown[]; total: number }>(11 `/trackings?page=${page}&limit=${limit}`12 );13 return NextResponse.json(result.data);14 } catch (error) {15 return NextResponse.json({ error: String(error) }, { status: 500 });16 }17}1819export async function POST(request: NextRequest) {20 const body = await request.json();21 const { tracking_number, slug, title, customer_name } = body;2223 if (!tracking_number) {24 return NextResponse.json({ error: 'tracking_number is required' }, { status: 400 });25 }2627 try {28 const result = await aftershipFetch('/trackings', {29 method: 'POST',30 body: JSON.stringify({31 tracking: {32 tracking_number,33 slug: slug || undefined,34 title: title || undefined,35 customer_name: customer_name || undefined,36 },37 }),38 });39 return NextResponse.json(result.data);40 } catch (error) {41 return NextResponse.json({ error: String(error) }, { status: 500 });42 }43}Pro tip: AfterShip tracking numbers are case-sensitive and must match exactly what the carrier uses. When allowing user input, normalize to uppercase and trim whitespace before sending to the API.
Expected result: POST /api/aftership/trackings creates a new tracking. GET /api/aftership/trackings returns a paginated list of all trackings with current status.
Build the Tracking Dashboard UI
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.
Build a shipment tracking dashboard at app/tracking/page.tsx with: (1) a list of trackings showing tracking number, carrier, customer name, status badge (color-coded by AfterShip tag), last event description, and expected delivery date; (2) an AddTracking button that opens a form for tracking number, carrier slug, and customer name; (3) clicking a row opens a detail page at app/tracking/[slug]/[id]/page.tsx with a full checkpoint timeline showing timestamp, location, and event message for each scan.
Paste this in Bolt.new chat
Pro tip: For the carrier dropdown, hard-code the top 20 most common carriers (fedex, ups, usps, dhl, royal-mail, australia-post, canada-post, etc.) and add a text input for other carriers. Loading all 1,100+ carriers in a dropdown is impractical.
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.
Set Up AfterShip Webhooks for Real-Time Updates
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.
Create an AfterShip webhook handler at app/api/aftership/webhook/route.ts. It should: (1) read the raw request body as text, (2) verify the HMAC-SHA256 signature using the hmac header against AFTERSHIP_WEBHOOK_SECRET environment variable, (3) parse the JSON payload, (4) switch on the tracking tag (InTransit, OutForDelivery, Delivered, Exception), (5) log the event and optionally trigger a SendGrid email notification. Remember: webhooks only work on deployed apps, not in Bolt's WebContainer preview.
Paste this in Bolt.new chat
1// app/api/aftership/webhook/route.ts2// NOTE: Webhooks only work on deployed apps (Netlify/Bolt Cloud)3// The WebContainer cannot receive incoming HTTP connections4import { NextRequest, NextResponse } from 'next/server';5import crypto from 'crypto';67export async function POST(request: NextRequest) {8 const body = await request.text();9 const signature = request.headers.get('hmac') ?? '';1011 const webhookSecret = process.env.AFTERSHIP_WEBHOOK_SECRET;12 if (webhookSecret) {13 const expected = crypto14 .createHmac('sha256', webhookSecret)15 .update(body)16 .digest('hex');17 if (signature !== expected) {18 return NextResponse.json({ error: 'Invalid signature' }, { status: 401 });19 }20 }2122 const payload = JSON.parse(body);23 const tracking = payload.msg;2425 if (!tracking) {26 return NextResponse.json({ received: true });27 }2829 const tag = tracking.tag as string;30 const trackingNumber = tracking.tracking_number as string;31 const customerName = tracking.customer_name as string;3233 console.log(`AfterShip webhook: ${trackingNumber} is now ${tag}`);3435 switch (tag) {36 case 'OutForDelivery':37 console.log(`Notifying ${customerName}: Package out for delivery`);38 // Send email via SendGrid here39 break;40 case 'Delivered':41 console.log(`Notifying ${customerName}: Package delivered`);42 break;43 case 'Exception':44 console.log(`Alert: Delivery exception for ${trackingNumber}`);45 break;46 }4748 return NextResponse.json({ received: true });49}Pro tip: Register your webhook in AfterShip after deploying to Netlify or Bolt Cloud. During development, test webhook logic by manually POSTing sample payloads to your API route using Postman or curl.
Expected result: After deploying and registering the webhook URL in AfterShip, status changes trigger POST requests to your webhook handler, enabling real-time notifications.
Common 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.
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.
Copy this prompt to try it in Bolt.new
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.
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.
Copy this prompt to try it in Bolt.new
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.
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.
Copy this prompt to try it in Bolt.new
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.
1headers: {2 'as-api-key': process.env.AFTERSHIP_API_KEY!,3 'Content-Type': 'application/json',4}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.
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
Alternatives
DHL's direct API is better if you exclusively ship with DHL and need deeper integration with DHL-specific features like pick-up scheduling and advanced customs data.
FedEx's direct API provides richer FedEx-specific data including door tag events and FedEx Delivery Manager options not available through AfterShip's normalized view.
UPS's direct API is preferred for UPS-heavy logistics operations that need UPS My Choice integrations and detailed UPS-specific service level data.
ShipStation combines shipping label creation, carrier rate shopping, and order management alongside tracking — better if you need to both create and track shipments.
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.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation