Skip to main content

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 shipped
  • in_transit: Package moving through network
  • out_for_delivery: Out for delivery today
  • delivered: Package delivered
  • returned: Returned to sender
  • failure: Delivery failed
  • cancelled: Shipment cancelled
  • error: 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

  1. Validate First: Always validate addresses before getting rates
  2. Cache Rates: Rates are valid for 24 hours, cache if showing to user
  3. Default to Media Mail: Most cost-effective for books
  4. Error Recovery: Handle API failures gracefully
  5. Test Mode: Use test API key during development
  6. Batch Tracking: Update tracking statuses in background jobs
  7. 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 changed
  • batch.created: Batch created
  • batch.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

Additional Resources