Skip to main content

Overview

Openfront provides a unified shipping adapter system that supports multiple carriers through providers like Shippo and ShipEngine, or manual rate configuration for custom needs.

Shipping Adapter Interface

All shipping providers implement a standard interface:
  • getRatesFunction - Get shipping rates for an order
  • createLabelFunction - Generate shipping labels
  • validateAddressFunction - Validate shipping addresses
  • trackShipmentFunction - Track shipment status
  • cancelLabelFunction - Cancel/refund shipping labels
The adapter registry is defined in features/integrations/shipping-providers/index.ts:
features/integrations/shipping-providers/index.ts
export const shippingProviderAdapters = {
  shippo: () => import("./shippo"),
  shipengine: () => import("./shipengine"),
  manual: () => import("./manual"),
};

Shippo Configuration

Setup

  1. Sign up for a Shippo account
  2. Get your API token from the dashboard
  3. Configure the provider in Openfront admin

Environment Variables

Store your Shippo API token securely:
.env
SHIPPO_API_TOKEN=shippo_test_...

Implementation Details

The Shippo adapter (features/integrations/shipping-providers/shippo.ts) provides full integration:
export async function getRatesFunction({ provider, order, dimensions }) {
  if (!dimensions) {
    throw new Error("Dimensions are required to get shipping rates");
  }

  // Create shipping address
  const addressToResponse = await fetch(`${SHIPPO_API_URL}/addresses/`, {
    method: "POST",
    headers: {
      Authorization: `ShippoToken ${provider.accessToken}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      name: `${order.shippingAddress.firstName} ${order.shippingAddress.lastName}`,
      street1: order.shippingAddress.address1,
      city: order.shippingAddress.city,
      state: order.shippingAddress.province,
      zip: order.shippingAddress.postalCode,
      country: order.shippingAddress.country.iso2,
    }),
  });

  const addressTo = await addressToResponse.json();

  // Create shipment to get rates
  const shipmentResponse = await fetch(`${SHIPPO_API_URL}/shipments/`, {
    method: "POST",
    headers: {
      Authorization: `ShippoToken ${provider.accessToken}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      address_from: { /* provider.fromAddress */ },
      address_to: addressTo.object_id,
      parcels: [{
        length: dimensions.length,
        width: dimensions.width,
        height: dimensions.height,
        distance_unit: dimensions.unit,
        weight: dimensions.weight,
        mass_unit: dimensions.weightUnit,
      }],
    }),
  });

  const shipment = await shipmentResponse.json();

  return shipment.rates.map((rate) => ({
    id: rate.object_id,
    providerId: provider.id,
    service: rate.servicelevel.name,
    carrier: rate.provider,
    price: rate.amount,
    currency: rate.currency,
    estimatedDays: rate.estimated_days,
  }));
}

ShipEngine Configuration

Setup

  1. Sign up for a ShipEngine account
  2. Get your API key from the dashboard
  3. Connect your carrier accounts (USPS, UPS, FedEx, etc.)

Environment Variables

.env
SHIPENGINE_API_KEY=your_api_key

Implementation Details

The ShipEngine adapter (features/integrations/shipping-providers/shipengine.ts) handles unit conversions and multi-carrier support:
export async function getRatesFunction({ provider, order, dimensions }) {
  // List available carriers
  const carriers = await listCarriersFunction(provider);
  const carrier_ids = carriers.map((carrier) => carrier.carrier_id);

  // Convert dimensions to ShipEngine format
  const convertedDimensions = convertDimensions(dimensions);
  const convertedWeight = convertWeight(dimensions);

  const payload = {
    shipment: {
      ship_to: {
        name: `${order.shippingAddress.firstName} ${order.shippingAddress.lastName}`,
        address_line1: order.shippingAddress.address1,
        city_locality: order.shippingAddress.city,
        state_province: order.shippingAddress.province,
        postal_code: order.shippingAddress.postalCode,
        country_code: order.shippingAddress.country.iso2,
      },
      ship_from: { /* provider.fromAddress */ },
      packages: [{
        weight: convertedWeight,
        dimensions: {
          length: convertedDimensions.length,
          width: convertedDimensions.width,
          height: convertedDimensions.height,
          unit: convertedDimensions.unit,
        },
      }],
    },
    rate_options: { carrier_ids },
  };

  const response = await fetch(`${SHIPENGINE_API_URL}/rates`, {
    method: "POST",
    headers: {
      "API-Key": provider.accessToken,
      "Content-Type": "application/json",
    },
    body: JSON.stringify(payload),
  });

  const result = await response.json();

  return result.rate_response.rates.map((rate) => {
    const totalPrice = Number(
      (rate.shipping_amount?.amount || 0) + (rate.other_amount?.amount || 0)
    ).toFixed(2);

    return {
      id: JSON.stringify({ id: rate.rate_id, service: rate.service_code }),
      providerId: provider.id,
      service: rate.service_type || rate.service_code,
      carrier: rate.carrier_friendly_name || rate.carrier_code,
      price: totalPrice,
      currency: rate.shipping_amount?.currency.toUpperCase() || "USD",
      estimatedDays: rate.delivery_days || rate.estimated_delivery_days,
    };
  });
}

Unit Conversion

ShipEngine requires specific unit formats. The adapter handles conversions:
features/integrations/shipping-providers/shipengine.ts
const WEIGHT_UNIT_MAP = {
  oz: "ounce",
  lb: "pound",
  kg: "kilogram",
  g: "gram",
};

const DIMENSION_UNIT_MAP = {
  in: "inch",
  cm: "centimeter",
};

function convertDimensions(dim) {
  if (dim.unit === "m") {
    return {
      length: dim.length * 100,
      width: dim.width * 100,
      height: dim.height * 100,
      unit: "centimeter",
    };
  } else if (dim.unit === "ft") {
    return {
      length: dim.length * 12,
      width: dim.width * 12,
      height: dim.height * 12,
      unit: "inch",
    };
  }
  return {
    ...dim,
    unit: DIMENSION_UNIT_MAP[dim.unit] || dim.unit,
  };
}

Manual Shipping Provider

The manual provider simulates shipping rates without external API calls, useful for:
  • Local pickup
  • Flat rate shipping
  • Testing and development
  • Custom rate calculations
features/integrations/shipping-providers/manual.ts
export async function getRatesFunction({ provider, order }) {
  return [
    {
      id: "rate_usps_1",
      providerId: provider.id,
      service: "Priority Mail",
      carrier: "USPS",
      price: "7.99",
      currency: "USD",
      estimatedDays: 3,
    },
    {
      id: "rate_ups_1",
      providerId: provider.id,
      service: "Ground",
      carrier: "UPS",
      price: "8.99",
      currency: "USD",
      estimatedDays: 4,
    },
  ];
}

Building Custom Shipping Adapters

Create custom adapters for direct carrier integrations or specialized shipping logic:
features/integrations/shipping-providers/custom.ts
export async function getRatesFunction({ provider, order, dimensions }) {
  // Call your custom API
  const response = await fetch('https://api.yourcarrier.com/rates', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${provider.accessToken}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      origin: provider.fromAddress,
      destination: order.shippingAddress,
      package: dimensions,
    }),
  });

  const rates = await response.json();

  return rates.map(rate => ({
    id: rate.id,
    providerId: provider.id,
    service: rate.service_name,
    carrier: rate.carrier_name,
    price: rate.price,
    currency: rate.currency,
    estimatedDays: rate.delivery_days,
  }));
}

export async function createLabelFunction({ provider, order, rateId, dimensions }) {
  // Implement label generation
}

export async function validateAddressFunction({ provider, address }) {
  // Implement address validation
}

export async function trackShipmentFunction({ provider, trackingNumber }) {
  // Implement tracking
}

export async function cancelLabelFunction({ provider, labelId }) {
  // Implement label cancellation
}
Register your custom adapter:
features/integrations/shipping-providers/index.ts
export const shippingProviderAdapters = {
  shippo: () => import("./shippo"),
  shipengine: () => import("./shipengine"),
  manual: () => import("./manual"),
  custom: () => import("./custom"),
};

Package Dimensions

All shipping providers require package dimensions:
const dimensions = {
  length: 10,      // Length of package
  width: 8,        // Width of package
  height: 6,       // Height of package
  unit: "in",      // in, cm, m, ft
  weight: 2.5,     // Weight value
  weightUnit: "lb" // lb, oz, kg, g
};

Using Shipping Providers

Shipping providers are configured in the KeystoneJS admin panel:
type ShippingProvider {
  id: ID!
  name: String!
  provider: String!  # shippo, shipengine, manual
  isActive: Boolean!
  accessToken: String
  fromAddress: Address
}
Use the adapter in your application:
import { shippingProviderAdapters } from '@/features/integrations/shipping-providers';

const adapter = await shippingProviderAdapters[provider.provider]();
const rates = await adapter.getRatesFunction({
  provider,
  order,
  dimensions,
});

Best Practices

  • Always provide accurate package dimensions
  • Include all packaging materials in weight
  • Test with various package sizes
  • Cache rates when possible to reduce API calls
  • Validate addresses before calculating rates
  • Handle address suggestions gracefully
  • Provide clear error messages for invalid addresses
  • Support international address formats
  • Store label URLs securely
  • Implement label reprinting functionality
  • Track label usage and costs
  • Handle label cancellation within carrier time limits
  • Implement fallback providers
  • Handle carrier API outages gracefully
  • Log errors for debugging
  • Provide user-friendly error messages

Build docs developers (and LLMs) love