EasyPost Integration
EasyPost provides multi-carrier shipping solutions. BookWish uses EasyPost for shipping labels, rate shopping, address validation, and package tracking.
Overview
BookWish uses EasyPost for:
- Shipping Rates: Get rates from multiple carriers (USPS, UPS, FedEx)
- Label Purchase: Buy and print shipping labels
- Address Validation: Verify and standardize addresses
- Package Tracking: Real-time tracking updates
- Refunds: Refund unused labels
- Media Mail: Discounted book shipping via USPS
Implementation
Location: /backend/src/integrations/easypost.ts
Configuration
Required environment variables:
EASYPOST_API_KEY=EZAK... # EasyPost API key
The integration uses the official @easypost/api SDK.
Features
1. Address Validation
Verify shipping addresses before creating shipments.
Validate Address
import { createEasyPostClient } from '../integrations/easypost';
const easypost = createEasyPostClient(process.env.EASYPOST_API_KEY);
const result = await easypost.validateAddress({
name: 'John Doe',
street1: '123 Main St',
city: 'San Francisco',
state: 'CA',
zip: '94102',
country: 'US',
phone: '415-555-0123'
});
if (result.isValid) {
// Use result.suggestedAddress (normalized/verified address)
console.log('Verified:', result.suggestedAddress);
} else {
// Show errors to user
console.error('Invalid address:', result.errors);
}
Address validation:
- Verifies address exists
- Standardizes format (e.g., "Street" → "St")
- Adds ZIP+4 codes
- Suggests corrections
2. Get Shipping Rates
Get rates from multiple carriers for comparison.
Get Rates
const fromAddress = {
name: 'Main Street Books',
street1: '456 Market St',
city: 'San Francisco',
state: 'CA',
zip: '94105',
country: 'US'
};
const toAddress = {
name: 'Customer Name',
street1: '789 Oak Ave',
city: 'Portland',
state: 'OR',
zip: '97201',
country: 'US'
};
const weight = 12; // ounces (typical paperback weight)
const rates = await easypost.getRates(fromAddress, toAddress, weight);
// Returns:
// [
// {
// id: 'rate_...',
// carrier: 'USPS',
// service: 'Media Mail',
// rate: '3.49',
// deliveryDays: 7,
// deliveryDate: '2024-01-15',
// deliveryDateGuaranteed: false
// },
// {
// id: 'rate_...',
// carrier: 'USPS',
// service: 'Priority Mail',
// rate: '8.95',
// deliveryDays: 3,
// deliveryDate: '2024-01-11',
// deliveryDateGuaranteed: false
// },
// // ... more rates
// ]
Find Cheapest Rate
const cheapest = easypost.getCheapestRate(rates);
// Returns rate with lowest price
Find Fastest Rate
const fastest = easypost.getFastestRate(rates);
// Returns rate with shortest delivery time
3. Create Shipment and Buy Label
Purchase a shipping label with selected rate.
Buy Label
const shipment = await easypost.createShipment(
fromAddress,
toAddress,
weight,
rates[0].id // Selected rate ID
);
// Returns:
// {
// id: 'shp_...',
// trackingCode: '9400111899563824449661',
// labelUrl: 'https://easypost-files.s3.amazonaws.com/...',
// trackingUrl: 'https://tools.usps.com/go/TrackConfirmAction?tLabels=...',
// carrier: 'USPS',
// service: 'Media Mail',
// rate: '3.49'
// }
The label URL is a PDF that can be:
- Downloaded and printed
- Emailed to store owner
- Displayed in UI for printing
4. Package Tracking
Track shipments in real-time.
Get Tracking Info
const tracking = await easypost.getTracking(
'9400111899563824449661', // Tracking code
'USPS' // Carrier (optional)
);
// Returns:
// {
// trackingCode: '9400111899563824449661',
// status: 'in_transit',
// statusDetail: 'Your package is moving within the USPS network',
// estimatedDelivery: Date('2024-01-15'),
// carrier: 'USPS',
// events: [
// {
// status: 'in_transit',
// message: 'Arrived at Post Office',
// datetime: Date('2024-01-10 14:32:00'),
// location: 'Portland, OR'
// },
// {
// status: 'pre_transit',
// message: 'Shipping Label Created',
// datetime: Date('2024-01-08 09:15:00'),
// location: 'San Francisco, CA'
// }
// ]
// }
Tracking Statuses
pre_transit: Label created, not yet shippedin_transit: Package moving through networkout_for_delivery: Out for delivery todaydelivered: Package deliveredreturned: Returned to senderfailure: Delivery failedcancelled: Shipment cancellederror: Tracking error
5. Refund Unused Labels
Refund shipping labels that haven't been used.
Refund Shipment
const refunded = await easypost.refundShipment('shp_...');
if (refunded) {
// Refund successful
// Credit will appear in EasyPost account
}
Note: Not all carriers support refunds, and there are time limits (typically 30 days).
Data Structures
Address
interface Address {
name: string;
street1: string;
street2?: string;
city: string;
state: string;
zip: string;
country: string;
phone?: string;
company?: string;
}
ShippingRate
interface ShippingRate {
id: string; // Rate ID for purchasing
carrier: string; // 'USPS', 'UPS', 'FedEx'
service: string; // 'Media Mail', 'Priority Mail'
rate: string; // Price as string (e.g., '3.49')
deliveryDays: number; // Estimated days
deliveryDate?: string; // Estimated date
deliveryDateGuaranteed?: boolean;
}
Shipment
interface Shipment {
id: string; // EasyPost shipment ID
trackingCode: string; // Carrier tracking number
labelUrl: string; // PDF label URL
trackingUrl: string; // Public tracking page
carrier: string;
service: string;
rate: string;
}
TrackingInfo
interface TrackingInfo {
trackingCode: string;
status: string;
statusDetail?: string;
estimatedDelivery?: Date;
carrier: string;
events: TrackingEvent[];
}
interface TrackingEvent {
status: string;
message: string;
datetime: Date;
location?: string;
}
Package Dimensions
The integration uses standard book package dimensions:
// Default parcel dimensions for books
{
length: 10, // inches
width: 8,
height: 2,
weight: 12 // ounces (variable)
}
Weight Guidelines
Typical book weights:
- Paperback: 8-16 ounces
- Hardcover: 16-32 ounces
- Large format: 32-64 ounces
Calculate weight based on book metadata or default to 12 oz.
USPS Media Mail
Media Mail is the most cost-effective option for books:
- Pros: Lowest cost ($3-6 typically)
- Cons: Slower (7-10 days), books only
- Eligibility: Books, manuscripts, printed music
BookWish prioritizes Media Mail when available.
Error Handling
Common Errors
try {
const rates = await easypost.getRates(from, to, weight);
} catch (error) {
if (error.message.includes('No shipping rates available')) {
// Invalid route (e.g., international)
} else if (error.message.includes('Failed to get shipping rates')) {
// API error
}
}
Address Validation Errors
const result = await easypost.validateAddress(address);
if (!result.isValid) {
// result.errors contains validation issues
// e.g., ["Street address not found", "Invalid ZIP code"]
}
Label Purchase Errors
- Insufficient funds: EasyPost account needs funding
- Invalid rate: Rate ID expired (rates expire after 24 hours)
- Address error: Invalid shipping address
Best Practices
- Validate First: Always validate addresses before getting rates
- Cache Rates: Rates are valid for 24 hours, cache if showing to user
- Default to Media Mail: Most cost-effective for books
- Error Recovery: Handle API failures gracefully
- Test Mode: Use test API key during development
- Batch Tracking: Update tracking statuses in background jobs
- Refund Unused: Implement process to refund cancelled orders
Testing
Test API Keys
EasyPost provides test mode keys:
- Test key prefix:
EZAK...test... - No real charges in test mode
- All carriers available
Test Addresses
EasyPost test addresses:
// Valid test address
{
street1: '417 MONTGOMERY ST',
city: 'SAN FRANCISCO',
state: 'CA',
zip: '94104',
country: 'US'
}
// Invalid test address (for error testing)
{
street1: '000 NO STREET',
city: 'NO CITY',
state: 'ZZ',
zip: '00000',
country: 'US'
}
Test Credit Cards
In test mode, labels are free but you can test the purchase flow.
Rate Shopping Strategy
Recommended approach for showing shipping options:
async function getShippingOptions(from, to, weight) {
// Get all rates
const rates = await easypost.getRates(from, to, weight);
// Filter to book-friendly options
const mediaMailRates = rates.filter(r => r.service.includes('Media Mail'));
const priorityRates = rates.filter(r => r.service.includes('Priority'));
return {
cheapest: mediaMailRates[0] || rates[0],
fastest: priorityRates[0] || rates[rates.length - 1]
};
}
Webhook Integration
EasyPost can send webhooks for tracking updates:
// Configure webhook URL in EasyPost dashboard
// POST /webhooks/easypost
app.post('/webhooks/easypost', async (req, res) => {
const event = req.body;
if (event.description === 'tracker.updated') {
const tracker = event.result;
// Update tracking status in database
await updateShipmentTracking(tracker.tracking_code, tracker.status);
}
res.status(200).send('OK');
});
Common webhook events:
tracker.updated: Tracking status changedbatch.created: Batch createdbatch.updated: Batch updated
Cost Calculation
Add shipping cost to order total:
const rates = await easypost.getRates(from, to, weight);
const selectedRate = easypost.getCheapestRate(rates);
const shippingCents = Math.round(parseFloat(selectedRate.rate) * 100);
const orderTotal = itemsTotal + shippingCents;
Limitations
- US Focus: Primarily US carriers (international available but expensive)
- 24 Hour Rates: Shipping rates expire after 24 hours
- Refund Windows: Label refunds must be within carrier-specific windows
- Weight Limits: Maximum weight varies by carrier (70 lbs USPS)
- Dimensions: EasyPost uses standard dimensions, may not fit all books
- API Key Required: Must have EasyPost account and API key