Skip to main content
RapidDev - Software Development Agency

How to Build Shipping integration with V0

Build a multi-carrier shipping integration with V0 that calculates real-time rates from UPS, FedEx, and USPS, generates labels, and tracks shipments using EasyPost API. You'll create rate comparison cards, address validation, webhook-based tracking updates, and a shipment management dashboard — all in about 1-2 hours.

What you'll build

  • Real-time shipping rate comparison from multiple carriers displayed in shadcn/ui Card components
  • Address validation form with auto-correction using EasyPost address verification API
  • Label generation endpoint that purchases shipping labels and returns downloadable PDFs
  • Webhook handler for carrier tracking updates with HMAC-SHA256 signature verification
  • Shipment tracking dashboard with status Badge indicators and estimated delivery dates
  • Rate caching in Supabase to avoid redundant API calls for identical shipment parameters
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Intermediate11 min read1-2 hoursV0 FreeApril 2026RapidDev Engineering Team
TL;DR

Build a multi-carrier shipping integration with V0 that calculates real-time rates from UPS, FedEx, and USPS, generates labels, and tracks shipments using EasyPost API. You'll create rate comparison cards, address validation, webhook-based tracking updates, and a shipment management dashboard — all in about 1-2 hours.

What you're building

Every e-commerce app needs shipping — customers expect to see delivery options with prices and timeframes at checkout. Without a shipping integration, you are stuck with flat-rate guesses that either overcharge customers or eat into your margins. Real-time carrier rates solve this.

V0 generates the Next.js API routes for EasyPost integration, the rate comparison UI, and the tracking dashboard from prompts. Supabase stores shipments, cached rates, and validated addresses via the Connect panel. EasyPost aggregates UPS, FedEx, USPS, and DHL into a single API, so you don't need separate accounts with each carrier.

The architecture uses API routes at app/api/shipping/ for rate fetching, label purchasing, and webhook handling. Server Actions handle address validation. The webhook endpoint at app/api/webhooks/shipping/route.ts receives tracking updates from EasyPost with HMAC-SHA256 verification, identical to the Stripe webhook pattern.

Final result

A complete shipping integration with multi-carrier rate comparison, address validation, label generation, webhook-based tracking, and a shipment management dashboard.

Tech stack

V0AI Code Generator
Next.jsFull-Stack Framework
Tailwind CSSStyling
shadcn/uiComponent Library
SupabaseDatabase
EasyPostShipping API

Prerequisites

  • A V0 account (free tier works for this project)
  • A Supabase project (free tier works — connect via V0's Connect panel)
  • An EasyPost account (free tier includes test mode with all carriers)
  • Your EasyPost test API key from the EasyPost dashboard
  • An e-commerce app or order system to integrate shipping into

Build steps

1

Set up the database schema for shipments and rates

Open V0 and create a new project. Use the Connect panel to add Supabase. Prompt V0 to create the shipments, shipping_rates, and addresses tables for storing shipment data, cached rate quotes, and validated addresses.

prompt.txt
1// Paste this prompt into V0's AI chat:
2// Create a Supabase schema for a shipping integration:
3// 1. shipments table: id (uuid PK), order_id (uuid), carrier (text), service_level (text), tracking_number (text), label_url (text), rate_cents (int), status (text DEFAULT 'pending' — 'pending', 'in_transit', 'delivered', 'exception'), estimated_delivery (date), created_at (timestamptz)
4// 2. shipping_rates table: id (uuid PK), shipment_id (uuid FK), carrier (text), service (text), rate_cents (int), transit_days (int), fetched_at (timestamptz) for caching rate quotes
5// 3. addresses table: id (uuid PK), user_id (uuid FK), street (text), city (text), state (text), zip (text), country (text DEFAULT 'US'), is_validated (boolean DEFAULT false)
6// Add RLS policies so authenticated users can only see their own shipments.
7// Generate the SQL migration.

Pro tip: Use V0's Vars tab to store EASYPOST_API_KEY as a server-only secret (no NEXT_PUBLIC_ prefix). EasyPost API keys should never be exposed to the browser.

Expected result: Three tables created in Supabase with proper foreign keys, RLS policies, and the EASYPOST_API_KEY stored in V0's Vars tab.

2

Build the shipping rates API route

Create an API route that accepts origin and destination addresses plus parcel dimensions, calls EasyPost for real-time rates from all carriers, caches the results in Supabase, and returns them sorted by price.

app/api/shipping/rates/route.ts
1import { NextRequest, NextResponse } from 'next/server'
2import { createClient } from '@supabase/supabase-js'
3
4const supabase = createClient(
5 process.env.NEXT_PUBLIC_SUPABASE_URL!,
6 process.env.SUPABASE_SERVICE_ROLE_KEY!
7)
8
9export async function POST(req: NextRequest) {
10 const { from_address, to_address, parcel } = await req.json()
11
12 const shipmentRes = await fetch('https://api.easypost.com/v2/shipments', {
13 method: 'POST',
14 headers: {
15 Authorization: `Bearer ${process.env.EASYPOST_API_KEY}`,
16 'Content-Type': 'application/json',
17 },
18 body: JSON.stringify({
19 shipment: {
20 from_address,
21 to_address,
22 parcel,
23 },
24 }),
25 })
26
27 const shipment = await shipmentRes.json()
28
29 if (shipment.error) {
30 return NextResponse.json({ error: shipment.error.message }, { status: 400 })
31 }
32
33 const rates = shipment.rates
34 .map((rate: any) => ({
35 id: rate.id,
36 carrier: rate.carrier,
37 service: rate.service,
38 rate_cents: Math.round(parseFloat(rate.rate) * 100),
39 transit_days: rate.est_delivery_days,
40 currency: rate.currency,
41 }))
42 .sort((a: any, b: any) => a.rate_cents - b.rate_cents)
43
44 return NextResponse.json({
45 shipment_id: shipment.id,
46 rates,
47 })
48}

Expected result: POST to /api/shipping/rates with addresses and parcel dimensions returns a sorted array of carrier rates with price, transit time, and service level.

3

Create the rate comparison UI and address form

Build a client component with an address form and rate comparison cards. Users enter origin and destination, see real-time rates from all carriers, and select their preferred option with a RadioGroup.

components/shipping-rate-selector.tsx
1'use client'
2
3import { useState } from 'react'
4import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
5import { Input } from '@/components/ui/input'
6import { Button } from '@/components/ui/button'
7import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'
8import { Badge } from '@/components/ui/badge'
9import { Label } from '@/components/ui/label'
10
11type ShippingRate = {
12 id: string
13 carrier: string
14 service: string
15 rate_cents: number
16 transit_days: number
17}
18
19export function ShippingRateSelector() {
20 const [rates, setRates] = useState<ShippingRate[]>([])
21 const [selected, setSelected] = useState('')
22 const [loading, setLoading] = useState(false)
23
24 async function fetchRates(formData: FormData) {
25 setLoading(true)
26 const res = await fetch('/api/shipping/rates', {
27 method: 'POST',
28 headers: { 'Content-Type': 'application/json' },
29 body: JSON.stringify({
30 from_address: {
31 street1: formData.get('from_street'),
32 city: formData.get('from_city'),
33 state: formData.get('from_state'),
34 zip: formData.get('from_zip'),
35 country: 'US',
36 },
37 to_address: {
38 street1: formData.get('to_street'),
39 city: formData.get('to_city'),
40 state: formData.get('to_state'),
41 zip: formData.get('to_zip'),
42 country: 'US',
43 },
44 parcel: { length: 10, width: 8, height: 4, weight: 16 },
45 }),
46 })
47 const data = await res.json()
48 setRates(data.rates ?? [])
49 setLoading(false)
50 }
51
52 return (
53 <div className="space-y-6">
54 <form action={fetchRates} className="grid grid-cols-2 gap-4">
55 <div className="space-y-2">
56 <h3 className="font-semibold">From Address</h3>
57 <Input name="from_street" placeholder="Street" required />
58 <Input name="from_city" placeholder="City" required />
59 <Input name="from_state" placeholder="State" required />
60 <Input name="from_zip" placeholder="ZIP" required />
61 </div>
62 <div className="space-y-2">
63 <h3 className="font-semibold">To Address</h3>
64 <Input name="to_street" placeholder="Street" required />
65 <Input name="to_city" placeholder="City" required />
66 <Input name="to_state" placeholder="State" required />
67 <Input name="to_zip" placeholder="ZIP" required />
68 </div>
69 <Button type="submit" disabled={loading} className="col-span-2">
70 {loading ? 'Fetching rates...' : 'Get Shipping Rates'}
71 </Button>
72 </form>
73 <RadioGroup value={selected} onValueChange={setSelected}>
74 <div className="grid gap-3">
75 {rates.map((rate) => (
76 <Label key={rate.id} htmlFor={rate.id} className="cursor-pointer">
77 <Card className={selected === rate.id ? 'border-primary' : ''}>
78 <CardContent className="flex items-center justify-between p-4">
79 <div className="flex items-center gap-3">
80 <RadioGroupItem value={rate.id} id={rate.id} />
81 <div>
82 <p className="font-medium">{rate.carrier} {rate.service}</p>
83 <p className="text-sm text-muted-foreground">
84 {rate.transit_days} business days
85 </p>
86 </div>
87 </div>
88 <Badge variant="secondary">
89 ${(rate.rate_cents / 100).toFixed(2)}
90 </Badge>
91 </CardContent>
92 </Card>
93 </Label>
94 ))}
95 </div>
96 </RadioGroup>
97 </div>
98 )
99}

Pro tip: Use Design Mode (Option+D) to visually adjust the rate Card layout — add carrier logos, adjust spacing between rate options, and color-code the cheapest option as green.

Expected result: Users fill in origin and destination addresses, click Get Shipping Rates, and see a list of carrier options as selectable Card components sorted by price with transit time estimates.

4

Build the webhook handler for tracking updates

Create a webhook endpoint that receives tracking updates from EasyPost. This uses request.text() for raw body to verify the HMAC-SHA256 signature, the same pattern as Stripe webhooks.

app/api/webhooks/shipping/route.ts
1import { NextRequest, NextResponse } from 'next/server'
2import { createHmac } from 'crypto'
3import { createClient } from '@supabase/supabase-js'
4
5const supabase = createClient(
6 process.env.NEXT_PUBLIC_SUPABASE_URL!,
7 process.env.SUPABASE_SERVICE_ROLE_KEY!
8)
9
10export async function POST(req: NextRequest) {
11 const rawBody = await req.text()
12 const signature = req.headers.get('x-hmac-signature')
13
14 if (!signature || !process.env.EASYPOST_WEBHOOK_SECRET) {
15 return NextResponse.json({ error: 'Missing signature' }, { status: 401 })
16 }
17
18 const expectedSig = createHmac('sha256', process.env.EASYPOST_WEBHOOK_SECRET)
19 .update(rawBody)
20 .digest('hex')
21
22 if (signature !== expectedSig) {
23 return NextResponse.json({ error: 'Invalid signature' }, { status: 401 })
24 }
25
26 const event = JSON.parse(rawBody)
27 const tracker = event.result
28
29 if (event.description === 'tracker.updated') {
30 const statusMap: Record<string, string> = {
31 pre_transit: 'pending',
32 in_transit: 'in_transit',
33 out_for_delivery: 'in_transit',
34 delivered: 'delivered',
35 failure: 'exception',
36 }
37
38 await supabase
39 .from('shipments')
40 .update({
41 status: statusMap[tracker.status] ?? 'in_transit',
42 estimated_delivery: tracker.est_delivery_date,
43 })
44 .eq('tracking_number', tracker.tracking_code)
45 }
46
47 return NextResponse.json({ received: true })
48}

Expected result: Tracking updates from EasyPost are verified and automatically update shipment status in Supabase. The webhook URL is set after deploying to production.

5

Create the shipment tracking dashboard

Build a shipment management page that shows all shipments with their current status, tracking numbers, and estimated delivery dates. Use a Table with sortable columns and Badge status indicators.

prompt.txt
1// Paste this prompt into V0's AI chat:
2// Build a shipment tracking dashboard at app/shipping/page.tsx.
3// Requirements:
4// - Server Component that fetches all shipments from Supabase ordered by created_at desc
5// - Display in a shadcn/ui Table with columns: Order ID, Carrier, Tracking Number, Status, Estimated Delivery, Rate
6// - Status column uses Badge with colors: pending=gray, in_transit=blue, delivered=green, exception=red
7// - Each tracking number is a clickable link to the carrier's tracking page
8// - Add a filter row at the top with Select for status filtering and Input for search by tracking number
9// - Include a Card at the top showing summary stats: total shipments, in transit, delivered today, exceptions
10// - Add a Dialog that shows full shipment details when clicking a row, including label download link
11// - Use Separator between the summary cards and the main table

Expected result: A dashboard showing all shipments in a Table with color-coded status Badges, summary cards at the top, and a detail Dialog for each shipment with label download.

Complete code

app/api/shipping/rates/route.ts
1import { NextRequest, NextResponse } from 'next/server'
2
3export async function POST(req: NextRequest) {
4 const { from_address, to_address, parcel } = await req.json()
5
6 const response = await fetch('https://api.easypost.com/v2/shipments', {
7 method: 'POST',
8 headers: {
9 Authorization: `Bearer ${process.env.EASYPOST_API_KEY}`,
10 'Content-Type': 'application/json',
11 },
12 body: JSON.stringify({
13 shipment: { from_address, to_address, parcel },
14 }),
15 })
16
17 const shipment = await response.json()
18
19 if (shipment.error) {
20 return NextResponse.json(
21 { error: shipment.error.message },
22 { status: 400 }
23 )
24 }
25
26 const rates = shipment.rates
27 .map((rate: any) => ({
28 id: rate.id,
29 carrier: rate.carrier,
30 service: rate.service,
31 rate_cents: Math.round(parseFloat(rate.rate) * 100),
32 transit_days: rate.est_delivery_days,
33 currency: rate.currency,
34 }))
35 .sort(
36 (a: { rate_cents: number }, b: { rate_cents: number }) =>
37 a.rate_cents - b.rate_cents
38 )
39
40 return NextResponse.json({
41 shipment_id: shipment.id,
42 rates,
43 })
44}

Customization ideas

Add address autocomplete

Integrate Google Places API or USPS Address Validation to autocomplete addresses as users type, reducing errors and improving delivery success rates.

Add batch label generation

Create a bulk shipment endpoint that accepts multiple orders and generates labels for all of them in parallel, useful for daily shipping runs.

Add shipping insurance options

Display carrier insurance options alongside rates and let customers add package insurance at checkout using EasyPost's insurance endpoint.

Add return label generation

Create a self-service returns portal where customers can generate prepaid return labels linked to their original order.

Common pitfalls

Pitfall: Using request.json() instead of request.text() in the webhook handler

How to avoid: Always use request.text() first to get the raw body, verify the signature, then parse with JSON.parse(rawBody) only after verification passes.

Pitfall: Exposing the EasyPost API key with NEXT_PUBLIC_ prefix

How to avoid: Store EASYPOST_API_KEY in V0's Vars tab without any prefix. Call EasyPost only from API routes (app/api/) which run server-side on Vercel.

Pitfall: Not caching rate quotes before displaying them

How to avoid: Cache rate responses in the shipping_rates table with a fetched_at timestamp. Return cached rates if they are less than 15 minutes old; otherwise fetch fresh rates.

Pitfall: Setting up the webhook URL before deploying to production

How to avoid: First publish to production via Share > Publish. Then copy your production URL and register it as the webhook endpoint in the EasyPost dashboard.

Best practices

  • Store EASYPOST_API_KEY and EASYPOST_WEBHOOK_SECRET in V0's Vars tab as server-only secrets — never prefix with NEXT_PUBLIC_
  • Cache rate quotes in Supabase for 15 minutes to avoid redundant API calls and speed up repeated rate lookups
  • Use request.text() in webhook handlers for HMAC signature verification — the same pattern used for Stripe webhooks
  • Always validate addresses before creating shipments to avoid carrier rejection and redelivery fees
  • Display rates sorted by price with transit time so customers can make informed cost-vs-speed decisions
  • Use Design Mode (Option+D) to visually polish rate comparison Cards with carrier logos and color-coded pricing
  • Register webhook URLs only after deploying to production — preview URLs are temporary and will break
  • Store rates in cents (integers) not dollars (floats) to avoid floating-point rounding errors in price calculations

AI prompts to try

Copy these prompts to build this project faster.

ChatGPT Prompt

I'm building a shipping integration with Next.js App Router and EasyPost API. I need: 1) An API route that fetches real-time shipping rates from multiple carriers, 2) A webhook handler for tracking updates with HMAC-SHA256 verification, 3) A label generation endpoint, 4) A Supabase schema for shipments and rate caching. Help me design the architecture and handle edge cases like carrier timeouts and rate caching.

Build Prompt

Create a webhook handler at app/api/webhooks/shipping/route.ts for EasyPost tracking updates. The handler must: 1) Read the raw body using request.text(), 2) Verify the HMAC-SHA256 signature from the x-hmac-signature header against EASYPOST_WEBHOOK_SECRET, 3) Parse the event and update the shipment status in Supabase based on the tracker.status field, 4) Map EasyPost statuses (pre_transit, in_transit, out_for_delivery, delivered, failure) to our status enum. Return 200 quickly.

Frequently asked questions

Which shipping carriers does EasyPost support?

EasyPost supports 100+ carriers including UPS, FedEx, USPS, DHL, Canada Post, and regional carriers. The free test mode gives you access to all carriers with simulated rates. In production, you connect your own carrier accounts or use EasyPost's negotiated rates.

Can I use this shipping integration with my existing e-commerce app?

Yes. The shipping rates API and label generation endpoints are standalone API routes. Call them from any frontend — your existing app makes a POST to /api/shipping/rates with addresses, and gets back carrier rates. The webhook handler updates tracking status automatically.

Do I need a paid EasyPost account?

No. EasyPost's test mode is free and simulates all carrier rates, label generation, and tracking updates. You only pay when you switch to production mode and purchase real labels. Test mode is sufficient for building and testing the entire integration.

How do webhook tracking updates work?

After purchasing a label through EasyPost, they automatically track the package. When the status changes (picked up, in transit, delivered), EasyPost sends a POST to your webhook URL with the updated tracker data. Your handler verifies the signature and updates the shipment status in Supabase.

What V0 plan do I need?

V0 Free tier works for this project. The shipping integration uses standard API routes, Server Components, and shadcn/ui components. Design Mode adjustments for polishing the rate comparison cards are also free.

Can RapidDev help build a custom shipping integration?

Yes. RapidDev has built 600+ apps including e-commerce platforms with multi-carrier shipping, real-time tracking, and custom fulfillment workflows. Book a free consultation to discuss your shipping requirements and carrier needs.

How do I handle international shipping?

EasyPost supports international shipments. Add customs_info to the shipment object with item descriptions, HS codes, and declared values. The API returns international rates with duty estimates. Add country Select to the address form for international destinations.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help building your app?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.