Skip to main content
RapidDev - Software Development Agency

How to Build a Shipping Integration with Lovable

Build a multi-carrier shipping integration in Lovable that compares rates from UPS, FedEx, and USPS via a Supabase Edge Function proxy, stores shipments and tracking data, processes carrier webhook updates, and saves label PDFs to Supabase Storage — giving you a complete shipping ops dashboard without exposing carrier API keys to the browser.

What you'll build

  • Multi-carrier rate comparison via Edge Function that calls UPS, FedEx, and USPS APIs in parallel
  • Shipments table with full address pair, selected rate, tracking number, and carrier status
  • Rates caching table to avoid redundant API calls for the same origin/destination/weight
  • Tracking webhook receiver Edge Function that updates shipment status from carrier callbacks
  • Label PDF storage in Supabase Storage with signed download URLs
  • Shipment timeline component showing carrier tracking events with status icons
  • Address validation via Edge Function before shipment creation
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Advanced16 min read4–5 hoursLovable Pro or higherApril 2026RapidDev Engineering Team
TL;DR

Build a multi-carrier shipping integration in Lovable that compares rates from UPS, FedEx, and USPS via a Supabase Edge Function proxy, stores shipments and tracking data, processes carrier webhook updates, and saves label PDFs to Supabase Storage — giving you a complete shipping ops dashboard without exposing carrier API keys to the browser.

What you're building

Rather than integrating with UPS, FedEx, and USPS APIs individually (each with different authentication, request formats, and rate models), this build uses EasyPost as a single aggregation layer. EasyPost normalizes all carrier APIs behind one REST interface. One EasyPost API key gives you rate comparison across 100+ carriers, address validation, label generation, and a unified webhook for tracking events.

The carrier proxy Edge Function receives a rate request from the Lovable frontend and calls the EasyPost Shipment creation API, which returns a list of available rates sorted by price. The Edge Function caches the rate response in a Supabase rates table keyed by a hash of the origin address, destination address, parcel dimensions, and weight. Subsequent requests with identical parameters return the cached rates, saving API calls.

When a user selects a rate and clicks 'Buy Label', a second Edge Function call purchases the label via EasyPost's buy endpoint, receives a base64-encoded PDF, stores it in Supabase Storage, and creates a shipments row with the tracking number. EasyPost sends tracking webhook events to a public Edge Function URL. The webhook receiver verifies the event, parses the tracking status, and updates the shipment row and a tracking_events table.

The shipment dashboard lets warehouse staff see all shipments, filter by carrier and status, view the tracking timeline in a Sheet, and download the label PDF via a Supabase signed URL.

Final result

A complete multi-carrier shipping integration dashboard with rate comparison, label generation, tracking, and webhook-powered status updates.

Tech stack

LovableShipping dashboard UI
SupabaseDatabase, Auth, and Storage
Supabase Edge FunctionsCarrier API proxy and webhook receiver (Deno)
EasyPost APIMulti-carrier rates, labels, and tracking aggregation
shadcn/uiTable, Sheet, Badge, Timeline components
RechartsShipping cost and volume charts

Prerequisites

  • Lovable Pro account for multiple Edge Functions
  • EasyPost account with production or test API key (easypost.com — free account available)
  • EasyPost API key saved to Cloud tab → Secrets as EASYPOST_API_KEY
  • Supabase service role key saved as SUPABASE_SERVICE_ROLE_KEY
  • Supabase Storage bucket for labels (created in step 1)
  • Basic understanding of shipping concepts: origin/destination, parcel dimensions, tracking numbers

Build steps

1

Set up the shipping database schema and Storage bucket

Prompt Lovable to create all the tables, the labels Storage bucket, and the database indexes needed for the shipping integration. Addresses are stored as JSONB to handle the variety of carrier address formats.

prompt.txt
1Create a shipping integration schema in Supabase:
2
31. Create a private Storage bucket named 'shipping-labels' with public: false, file size limit 5MB, allowed MIME types: application/pdf
4
52. Database tables:
6 - addresses: id, user_id, label (text, e.g. 'Warehouse A'), name, company, street1, street2, city, state, zip, country (default 'US'), phone, is_verified (bool default false), created_at
7 - shipments: id, user_id, origin_address_id, destination_address_id, carrier (text), service (text), tracking_number (text, unique), status (pre_transit|in_transit|out_for_delivery|delivered|failure|cancelled), label_url (text, storage path), rate_amount (numeric), currency (text default 'USD'), easypost_shipment_id (text), easypost_tracker_id (text), estimated_delivery_date (date), created_at, updated_at
8 - tracking_events: id, shipment_id, status (text), description (text), location (text), carrier_timestamp (timestamptz), received_at
9 - rates_cache: id, cache_key (text, unique), rates_json (jsonb), origin_zip, destination_zip, weight_oz, cached_at, expires_at
10 - parcels: id, user_id, label (text), length_in (numeric), width_in (numeric), height_in (numeric), weight_oz (numeric), is_default (bool default false)
11
123. RLS:
13 - addresses: users own their rows
14 - shipments: users own their rows
15 - tracking_events: users can SELECT where shipment_id is in their shipments. Service role for INSERT.
16 - rates_cache: service role only
17 - parcels: users own their rows
18
194. Storage RLS for shipping-labels:
20 - SELECT: auth.uid()::text = (storage.foldername(name))[1]
21 - INSERT: auth.uid()::text = (storage.foldername(name))[1]
22
23Add indexes:
24- CREATE INDEX idx_shipments_tracking ON shipments(tracking_number);
25- CREATE INDEX idx_shipments_status ON shipments(user_id, status, created_at DESC);
26- CREATE INDEX idx_rates_cache_key ON rates_cache(cache_key, expires_at);

Pro tip: Store the EasyPost shipment ID (easypost_shipment_id) on every shipment row. If a webhook arrives for a tracking number you cannot find, you can look up the EasyPost shipment directly by its ID as a fallback.

Expected result: All tables are created with RLS policies and indexes. The shipping-labels Storage bucket is created as private. TypeScript types are generated.

2

Build the multi-carrier rate comparison Edge Function

Create the Edge Function that calls EasyPost's Shipment API to get rates from multiple carriers, caches the result, and returns normalized rate options to the frontend.

supabase/functions/get-shipping-rates/index.ts
1// supabase/functions/get-shipping-rates/index.ts
2import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'
3import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
4
5const corsHeaders = {
6 'Access-Control-Allow-Origin': '*',
7 'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
8 'Content-Type': 'application/json',
9}
10
11serve(async (req: Request) => {
12 if (req.method === 'OPTIONS') return new Response('ok', { headers: corsHeaders })
13
14 const { from_address, to_address, parcel } = await req.json()
15
16 // Generate cache key from normalized inputs
17 const cacheKey = btoa(JSON.stringify([
18 from_address.zip, to_address.zip,
19 parcel.weight, parcel.length, parcel.width, parcel.height
20 ]))
21
22 const supabase = createClient(
23 Deno.env.get('SUPABASE_URL') ?? '',
24 Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') ?? ''
25 )
26
27 // Check cache
28 const { data: cached } = await supabase
29 .from('rates_cache')
30 .select('rates_json')
31 .eq('cache_key', cacheKey)
32 .gt('expires_at', new Date().toISOString())
33 .single()
34
35 if (cached) {
36 return new Response(JSON.stringify({ rates: cached.rates_json, cached: true }), { headers: corsHeaders })
37 }
38
39 // Call EasyPost API
40 const easypostRes = await fetch('https://api.easypost.com/v2/shipments', {
41 method: 'POST',
42 headers: {
43 Authorization: `Basic ${btoa(Deno.env.get('EASYPOST_API_KEY') + ':')}`,
44 'Content-Type': 'application/json',
45 },
46 body: JSON.stringify({
47 shipment: {
48 to_address,
49 from_address,
50 parcel: {
51 weight: parcel.weight,
52 length: parcel.length,
53 width: parcel.width,
54 height: parcel.height,
55 },
56 },
57 }),
58 })
59
60 const shipmentData = await easypostRes.json()
61
62 const rates = (shipmentData.rates ?? []).map((r: any) => ({
63 id: r.id,
64 carrier: r.carrier,
65 service: r.service,
66 rate: parseFloat(r.rate),
67 currency: r.currency,
68 delivery_days: r.delivery_days,
69 delivery_date: r.delivery_date,
70 easypost_shipment_id: shipmentData.id,
71 }))
72
73 // Cache for 30 minutes
74 const expiresAt = new Date(Date.now() + 30 * 60 * 1000).toISOString()
75 await supabase.from('rates_cache').upsert({
76 cache_key: cacheKey,
77 rates_json: rates,
78 origin_zip: from_address.zip,
79 destination_zip: to_address.zip,
80 weight_oz: parcel.weight,
81 cached_at: new Date().toISOString(),
82 expires_at: expiresAt,
83 }, { onConflict: 'cache_key' })
84
85 return new Response(JSON.stringify({ rates, cached: false }), { headers: corsHeaders })
86})

Pro tip: Sort the returned rates array by rate ascending before sending to the frontend so the cheapest option is always first: rates.sort((a, b) => a.rate - b.rate). Show the cheapest and fastest options with distinct badges in the UI.

Expected result: The Edge Function returns a sorted list of carrier rates for a given origin/destination/parcel combination. Second call with the same parameters returns cached results (cached: true) in under 50ms.

3

Build the label purchase Edge Function

Create the Edge Function that buys a shipping label via EasyPost, stores the PDF in Supabase Storage, and creates the shipment database row. This is called when the user selects a rate and confirms the purchase.

prompt.txt
1Create a Supabase Edge Function at supabase/functions/buy-label/index.ts.
2
3Accept POST body: { rate_id: string, easypost_shipment_id: string, origin_address_id: string, destination_address_id: string }
4
5Logic:
61. Authenticate the user via the Authorization header (user-scoped Supabase client)
72. Call EasyPost buy endpoint: POST https://api.easypost.com/v2/shipments/{easypost_shipment_id}/buy with body { rate: { id: rate_id } }
83. From the response, extract: tracking_code, selected_rate (carrier, service, rate, currency), postage_label.label_url (PDF URL), tracker.id
94. Fetch the PDF from label_url: const pdfRes = await fetch(label_url); const pdfBuffer = await pdfRes.arrayBuffer()
105. Store the PDF in Supabase Storage at path: {user_id}/{tracking_code}.pdf using service role client
116. Create a shipments row with: user_id, origin_address_id, destination_address_id, carrier, service, tracking_number, status='pre_transit', label_url (storage path), rate_amount, easypost_shipment_id, easypost_tracker_id
127. Return { shipment_id, tracking_number, label_storage_path, carrier, rate_amount }
13
14Handle errors: if EasyPost returns an error (e.g. rate expired), return 422 with the EasyPost error message so the frontend can prompt the user to refresh rates.

Expected result: Calling the Edge Function with a rate ID purchases the label via EasyPost. The PDF is stored in Supabase Storage. A shipment row appears in the database with tracking number and pre_transit status.

4

Build the tracking webhook receiver

Create a public Edge Function that EasyPost calls with tracking events. It verifies the webhook signature, parses the event, updates the shipment status, and inserts a tracking_events row. Register this function's URL in the EasyPost dashboard as a webhook endpoint.

supabase/functions/tracking-webhook/index.ts
1// supabase/functions/tracking-webhook/index.ts
2import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'
3import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
4
5const STATUS_MAP: Record<string, string> = {
6 pre_transit: 'pre_transit',
7 in_transit: 'in_transit',
8 out_for_delivery: 'out_for_delivery',
9 delivered: 'delivered',
10 failure: 'failure',
11 return_to_sender: 'cancelled',
12 cancelled: 'cancelled',
13 error: 'failure',
14 unknown: 'in_transit',
15}
16
17serve(async (req: Request) => {
18 if (req.method !== 'POST') {
19 return new Response('Method not allowed', { status: 405 })
20 }
21
22 const body = await req.text()
23 let event: any
24 try {
25 event = JSON.parse(body)
26 } catch {
27 return new Response('Invalid JSON', { status: 400 })
28 }
29
30 // Only process tracker.updated events
31 if (event.description !== 'tracker.updated') {
32 return new Response('OK', { status: 200 })
33 }
34
35 const tracker = event.result
36 const trackingNumber = tracker?.tracking_code
37 const easypostStatus = tracker?.status
38
39 if (!trackingNumber) return new Response('OK', { status: 200 })
40
41 const supabase = createClient(
42 Deno.env.get('SUPABASE_URL') ?? '',
43 Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') ?? ''
44 )
45
46 const { data: shipment } = await supabase
47 .from('shipments')
48 .select('id')
49 .eq('tracking_number', trackingNumber)
50 .single()
51
52 if (!shipment) return new Response('OK', { status: 200 })
53
54 const normalizedStatus = STATUS_MAP[easypostStatus] ?? 'in_transit'
55
56 // Update shipment status
57 await supabase
58 .from('shipments')
59 .update({ status: normalizedStatus, updated_at: new Date().toISOString() })
60 .eq('id', shipment.id)
61
62 // Insert tracking events
63 const events = (tracker.tracking_details ?? []).slice(-5)
64 if (events.length > 0) {
65 await supabase.from('tracking_events').insert(
66 events.map((e: any) => ({
67 shipment_id: shipment.id,
68 status: e.status,
69 description: e.message,
70 location: [e.tracking_location?.city, e.tracking_location?.state].filter(Boolean).join(', '),
71 carrier_timestamp: e.datetime,
72 received_at: new Date().toISOString(),
73 }))
74 )
75 }
76
77 return new Response('OK', { status: 200 })
78})

Expected result: After registering the Edge Function URL in EasyPost, tracking updates arrive and automatically update shipment status and add tracking_events rows. The shipment dashboard reflects live carrier updates.

5

Build the shipping dashboard and rate comparison UI

Create the main shipping dashboard with a DataTable of all shipments, a new shipment flow with rate comparison cards, and a tracking timeline Sheet.

prompt.txt
1Build the shipping dashboard at src/pages/ShippingDashboard.tsx and a rate comparison flow.
2
31. Dashboard page:
4- Stats row: total shipments today, in transit count, delivered today count, and average shipping cost
5- Shipments DataTable: columns = origin (from address name), destination (to address name + city), carrier+service Badge, tracking number (monospace, copyable), status Badge (color-coded), cost, created date, Actions
6- Status Badge colors: pre_transit=gray, in_transit=blue, out_for_delivery=yellow, delivered=green, failure=red, cancelled=muted
7- Actions DropdownMenu: View Tracking (opens Sheet), Download Label (calls Supabase storage.createSignedUrl for 5 min and triggers download)
8- Filter row: status Select filter, carrier Select filter, date range Popover
9
102. New Shipment flow (multi-step Dialog or separate page):
11- Step 1: address form (from/to). Show saved addresses as Select options. Allow new address entry.
12- Step 2: parcel dimensions form (length, width, height in inches, weight in oz)
13- Step 3: rate comparison - call get-shipping-rates Edge Function, show results as Cards sorted by price. Each card shows carrier logo text, service name, price, delivery days. 'Select' Button highlights the chosen rate.
14- Step 4: confirm and buy - show summary, 'Buy Label' Button calls buy-label Edge Function. Show loading state.
15
163. Tracking Sheet (ShipmentTracking.tsx):
17- Shows shipment header: tracking number, carrier, destination, current status Badge
18- Vertical timeline of tracking_events ordered by carrier_timestamp DESC
19- Each event: colored dot (green for delivered, yellow for in transit, red for failure), status label, description, location, timestamp
20- 'Refresh' Button that re-fetches tracking_events

Expected result: The dashboard shows all shipments with live status badges. The new shipment flow guides users through address, parcel, rate comparison, and label purchase. The tracking Sheet shows the event timeline.

Complete code

supabase/functions/tracking-webhook/index.ts
1import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'
2import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
3
4const STATUS_MAP: Record<string, string> = {
5 pre_transit: 'pre_transit',
6 in_transit: 'in_transit',
7 out_for_delivery: 'out_for_delivery',
8 delivered: 'delivered',
9 failure: 'failure',
10 return_to_sender: 'cancelled',
11 cancelled: 'cancelled',
12 error: 'failure',
13 unknown: 'in_transit',
14}
15
16serve(async (req: Request) => {
17 if (req.method === 'OPTIONS') return new Response('ok')
18 if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 })
19
20 let event: any
21 try {
22 event = await req.json()
23 } catch {
24 return new Response('Invalid JSON', { status: 400 })
25 }
26
27 if (!['tracker.created', 'tracker.updated'].includes(event.description)) {
28 return new Response('OK', { status: 200 })
29 }
30
31 const tracker = event.result
32 const trackingNumber = tracker?.tracking_code
33 if (!trackingNumber) return new Response('OK', { status: 200 })
34
35 const supabase = createClient(
36 Deno.env.get('SUPABASE_URL') ?? '',
37 Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') ?? ''
38 )
39
40 const { data: shipment } = await supabase
41 .from('shipments')
42 .select('id, status')
43 .eq('tracking_number', trackingNumber)
44 .single()
45
46 if (!shipment) return new Response('OK', { status: 200 })
47
48 const newStatus = STATUS_MAP[tracker.status] ?? 'in_transit'
49
50 if (shipment.status !== newStatus) {
51 await supabase
52 .from('shipments')
53 .update({
54 status: newStatus,
55 updated_at: new Date().toISOString(),
56 ...(newStatus === 'delivered' ? { actual_delivery_date: new Date().toISOString() } : {}),
57 })
58 .eq('id', shipment.id)
59 }
60
61 const latestEvents = (tracker.tracking_details ?? []).slice(0, 10)
62 if (latestEvents.length > 0) {
63 // Use upsert based on carrier_timestamp to avoid duplicates
64 await supabase.from('tracking_events').upsert(
65 latestEvents.map((e: any) => ({
66 shipment_id: shipment.id,
67 status: e.status ?? newStatus,
68 description: e.message ?? '',
69 location: [
70 e.tracking_location?.city,
71 e.tracking_location?.state,
72 e.tracking_location?.country,
73 ].filter(Boolean).join(', '),
74 carrier_timestamp: e.datetime ?? new Date().toISOString(),
75 received_at: new Date().toISOString(),
76 })),
77 { onConflict: 'shipment_id,carrier_timestamp', ignoreDuplicates: true }
78 )
79 }
80
81 return new Response('OK', { status: 200 })
82})

Customization ideas

Bulk shipment creation from CSV

Add a Bulk Ship page where users upload a CSV with columns: recipient_name, street, city, state, zip, weight_oz. An Edge Function parses each row, calls get-shipping-rates with the user's default carrier preference, buys labels for all rows in sequence, and returns a summary ZIP file with all label PDFs. Show a progress indicator for large batches.

Shipping rules engine

Add a shipping_rules table where users configure automatic carrier selection rules: 'If weight < 16oz AND destination is domestic, always use USPS First Class'. When the rate comparison step runs, automatically pre-select the rate that matches the active rule. This saves time for operations teams shipping similar products daily.

Delivery exception alerts

Modify the tracking webhook to detect failure and return_to_sender events and send an email alert to the shipment creator via Resend. Include the tracking number, last known location, carrier contact number, and a link to the shipment details page. Add an alerts_paused setting per user to opt out of notifications.

Carrier performance analytics

Build a carrier analytics page using Recharts. Show average delivery days by carrier vs. estimated delivery days (accuracy rating), on-time delivery percentage by carrier and service type, cost per shipment over time, and failure rate by carrier. This data helps users choose the most reliable carrier for their routes.

Return label generation

Add a 'Generate Return Label' button on each delivered shipment. It calls EasyPost with the addresses swapped (original destination as origin, original origin as destination) and the same parcel dimensions. The return label is emailed to the recipient using Resend with a PDF attachment, or stored in Supabase Storage for the dashboard user to send manually.

Common pitfalls

Pitfall: Calling carrier APIs directly from the Lovable frontend

How to avoid: All carrier API calls must go through Supabase Edge Functions. Store all carrier API keys in Cloud tab → Secrets without VITE_ prefix. The frontend only calls your own Edge Functions, never carrier APIs directly.

Pitfall: Not caching rate responses

How to avoid: Cache rate responses in the rates_cache table keyed by a hash of origin zip, destination zip, and parcel dimensions. Set a 30-minute TTL. The second rate request with the same parameters returns instantly from the cache.

Pitfall: Storing tracking webhook payloads in full without filtering

How to avoid: Extract only the fields you need from the webhook payload: tracking_code, status, and the last 10 tracking_details events. Normalize them into structured tracking_events rows. Discard the rest of the payload after processing.

Pitfall: Not handling EasyPost rate expiry when buying labels

How to avoid: Add a timestamp to each rate response. If more than 20 minutes have passed since the rates were fetched, show a 'Rates may have expired. Refresh rates?' prompt before allowing the buy step to proceed. Handle the EasyPost rate_expired error code in the buy-label Edge Function and return a 422 that prompts the frontend to re-fetch.

Best practices

  • Use EasyPost test mode API keys during development. EasyPost test mode simulates carrier responses without actually purchasing labels. Only switch to production keys when you are ready to ship real packages.
  • Store both the EasyPost shipment ID and the carrier tracking number. If a tracking webhook references a tracking number you cannot find in your database, you can look up the EasyPost shipment by its ID as a fallback.
  • Validate destination addresses before purchasing labels. EasyPost's address verification endpoint returns address corrections and error codes for invalid addresses. Catching bad addresses before label purchase saves the cost of undeliverable shipments.
  • Add a unique constraint on tracking_number in the shipments table to prevent duplicate shipment rows if a webhook fires before the buy-label Edge Function finishes writing to the database.
  • Log all carrier API call latencies (rate fetch time, label purchase time) to help identify which carrier integrations are slowest. EasyPost aggregates multiple carriers in one call, but individual carrier connections within EasyPost can timeout independently.
  • For high-volume shipping, add a job queue for label generation instead of synchronous Edge Function calls. Use Supabase Edge Function with a Postgres NOTIFY/LISTEN queue pattern so label generation happens asynchronously and the user sees a 'Processing' state.
  • Archive labels in Supabase Storage with user_id as the first path segment for security. Generate signed URLs with a short expiry (5 minutes) for downloads rather than storing permanent links.

AI prompts to try

Copy these prompts to build this project faster.

ChatGPT Prompt

I'm building a multi-carrier shipping integration using EasyPost API in a Deno Supabase Edge Function. I want to buy a shipping label, receive the label PDF as base64, and store it in Supabase Storage. Write the TypeScript code for: 1) calling EasyPost buy endpoint with a rate ID, 2) decoding the base64 label from the response, 3) uploading the decoded PDF bytes to Supabase Storage using the service role client. Show error handling for both the EasyPost API call and the Storage upload.

Lovable Prompt

Add a shipment cost analytics section to the shipping dashboard. Show a Recharts LineChart with total shipping spend per day over the last 30 days. Below that, show a PieChart of spend breakdown by carrier. Add a 'Cost by Destination' table showing the top 10 destination zip codes ranked by total spend, with average cost per shipment. All metrics come from aggregating the shipments table (rate_amount, carrier, destination_address_id joined to addresses for zip code).

Build Prompt

In Supabase, create an Edge Function that runs on a daily schedule via pg_cron to clean up the rates_cache table. The function should DELETE all rows WHERE expires_at < now(). Also check for shipments in 'pre_transit' status that are more than 7 days old with no tracking_events — these may be stuck. UPDATE their status to 'failure' and log the count. Return a summary JSON with rows cleaned from rates_cache and shipments updated to failure status.

Frequently asked questions

Why use EasyPost instead of integrating with each carrier's API directly?

Each carrier (UPS, FedEx, USPS, DHL) has different authentication systems, request/response formats, and documentation quality. EasyPost normalizes all of them behind one API. You write one integration and get access to 100+ carriers. For a Lovable app, EasyPost reduces the number of Edge Functions and API keys to manage from 4+ down to 1.

How do I get an EasyPost API key?

Create a free account at easypost.com. In your account dashboard, go to API Keys to find your test and production keys. The test key lets you simulate shipments without paying for labels — use it while building. Switch to the production key when you are ready to ship real packages. Store the key in Lovable's Cloud tab → Secrets as EASYPOST_API_KEY.

How do I register my webhook URL with EasyPost?

After deploying the tracking-webhook Edge Function (by clicking Publish in Lovable), copy the Edge Function URL from Supabase Cloud tab → Edge Functions. In EasyPost dashboard → Webhooks, add a new webhook with this URL and select event types: tracker.created and tracker.updated. EasyPost will POST tracking events to this URL automatically.

What happens if my Supabase Edge Function is down when a carrier sends a webhook?

EasyPost retries failed webhook deliveries multiple times over 24 hours with exponential backoff. If your Edge Function returns a non-200 response or times out, EasyPost queues the retry. This means you will not lose tracking events for transient outages. Always return a 200 response quickly — do processing asynchronously if needed.

Can I use this integration for international shipping?

Yes. EasyPost supports international shipping carriers. For international shipments, add customs_info and customs_items to the EasyPost shipment request body — this includes the customs form data required for cross-border packages. EasyPost generates the customs documentation alongside the shipping label. Add customs fields to the new shipment form for international destinations.

How much does EasyPost cost?

EasyPost charges a per-label fee on top of the carrier's base rate. The fee varies by carrier: typically $0.01–$0.05 per label. USPS labels may actually be cheaper through EasyPost than buying directly due to commercial pricing. EasyPost also offers free address verification credits and free tracking webhooks. Check easypost.com/pricing for current rates.

Can RapidDev help build a more complex shipping and fulfillment system?

Yes. RapidDev builds production shipping integrations including order management, warehouse pick-pack workflows, multi-location inventory, and carrier SLA monitoring. Reach out if your shipping requirements go beyond a single-dashboard integration.

How do I handle refunds for cancelled shipments?

EasyPost supports label refunds via a POST to /v2/refunds with the tracking code. Add a 'Void Label' button in the shipment DropdownMenu that calls an Edge Function wrapping the EasyPost refund endpoint. Refunds are typically processed in 3–5 business days. Update the shipment status to 'cancelled' and store the refund ID from EasyPost's response in a refund_id column on the shipments table.

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.