Create Shipping Integration

Learn how to create custom shipping integrations for Openfront using built-in adapters

Overview

Shipping integrations in Openfront allow you to connect any shipping carrier through a standardized interface. Unlike HTTP-based custom shipping providers, shipping integrations are TypeScript modules that run directly within Openfront, providing better performance and type safety.

How Shipping Integrations Work

Shipping integrations in Openfront are built as TypeScript modules that implement a standard interface. Openfront has built-in integrations for Shippo and ShipEngine, and you can create new integrations by following the same pattern.

For example:

  • Built-in Integration: getRatesFunction: "shippo"
  • Custom Integration: getRatesFunction: "my-custom-carrier"

Shippo Shipping Integration Reference

The Shippo shipping integration (/features/integrations/shipping/shippo.ts) demonstrates all required functions for a complete shipping integration. This guide will explain how each function works and how to implement it for your own shipping integration.

Function Reference

Get Shipping Rates - getRatesFunction

Purpose: Openfront uses this function to calculate shipping rates for customer orders during checkout

Request Body:

FieldTypeDescription
provider{ id: string; accessToken: string; fromAddress: object }Shipping provider configuration and credentials
order{ shippingAddress: object }Order with customer shipping address
dimensions{ length: number; width: number; height: number; weight: number; unit: string }Package dimensions and weight

Response Format:

FieldTypeDescription
ratesRate[]Array of available shipping rate objects
export async function getRatesFunction({ provider, order, dimensions }) {
  if (!dimensions) {
    throw new Error("Dimensions are required to get shipping rates");
  }

  // Create address first
  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,
  }));
}

Create Shipping Label - createLabelFunction

Purpose: Openfront uses this function to purchase shipping labels when orders are ready for fulfillment

Request Body:

FieldTypeDescription
provider{ id: string; accessToken: string; fromAddress: object }Shipping provider configuration
order{ shippingAddress: object }Order with shipping details
rateIdstringSelected shipping rate ID
dimensionsobjectPackage dimensions
lineItemsarrayOrder line items for customs declarations

Response Format:

FieldTypeDescription
statusstringLabel creation status
trackingNumberstringTracking number for the shipment
labelUrlstringURL to download the shipping label PDF
carrierstringShipping carrier name
servicestringService level used
export async function createLabelFunction({
  provider,
  order,
  rateId,
  dimensions,
  lineItems,
}) {
  // Create transaction (label) with the specific rate
  const transactionResponse = await fetch(`${SHIPPO_API_URL}/transactions/`, {
    method: "POST",
    headers: {
      Authorization: `ShippoToken ${provider.accessToken}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      rate: rateId,
      label_file_type: "PDF",
      async: false,
    }),
  });

  const transaction = await transactionResponse.json();

  if (transaction.status === "ERROR") {
    const errorMessage = transaction.messages?.[0]?.text || "Label creation failed";
    throw new Error(errorMessage);
  }

  if (!transaction.label_url) {
    throw new Error("No label URL received from Shippo");
  }

  return {
    status: "purchased",
    data: transaction,
    rate: transaction.rate,
    carrier: transaction.provider,
    service: transaction.servicelevel?.name,
    trackingNumber: transaction.tracking_number,
    trackingUrl: transaction.tracking_url_provider,
    labelUrl: transaction.label_url,
  };
}

Validate Address - validateAddressFunction

Purpose: Openfront uses this function to verify and standardize shipping addresses before label creation

Request Body:

FieldTypeDescription
provider{ accessToken: string }Provider credentials
addressobjectAddress to validate

Response Format:

FieldTypeDescription
isValidbooleanWhether the address is valid
suggestedAddressobjectCorrected address if validation found issues
errorsstring[]Validation error messages
export async function validateAddressFunction({ provider, address }) {
  try {
    const response = await fetch(`${SHIPPO_API_URL}/addresses/`, {
      method: "POST",
      headers: {
        Authorization: `ShippoToken ${provider.accessToken}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        name: `${address.firstName} ${address.lastName}`,
        street1: address.address1,
        street2: address.address2,
        city: address.city,
        state: address.province,
        zip: address.postalCode,
        country: address.country.iso2,
        validate: true,
      }),
    });

    const validation = await response.json();

    return {
      isValid: validation.validation_results.is_valid,
      suggestedAddress: validation.validation_results.is_valid
        ? {
            address1: validation.street1,
            address2: validation.street2,
            city: validation.city,
            province: validation.state,
            postalCode: validation.zip,
            country: validation.country,
          }
        : null,
      errors: validation.validation_results.messages || [],
    };
  } catch (error) {
    return {
      isValid: false,
      errors: [error.message],
    };
  }
}

Track Shipment - trackShipmentFunction

Purpose: Openfront uses this function to retrieve tracking information for shipped orders

Request Body:

FieldTypeDescription
provider{ accessToken: string }Provider credentials
trackingNumberstringTracking number to lookup

Response Format:

FieldTypeDescription
statusstringCurrent shipment status
estimatedDeliverystringEstimated delivery date
trackingUrlstringURL for carrier tracking page
eventsarrayArray of tracking events
export async function trackShipmentFunction({ provider, trackingNumber }) {
  const response = await fetch(`${SHIPPO_API_URL}/tracks/`, {
    method: "POST",
    headers: {
      Authorization: `ShippoToken ${provider.accessToken}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      carrier: "usps",
      tracking_number: trackingNumber,
    }),
  });

  const tracking = await response.json();

  return {
    status: tracking.tracking_status.status,
    estimatedDelivery: tracking.eta,
    trackingUrl: tracking.tracking_url,
    events: tracking.tracking_history.map((event) => ({
      status: event.status,
      location: event.location,
      timestamp: event.status_date,
      message: event.status_details,
    })),
  };
}

Cancel Label - cancelLabelFunction

Purpose: Openfront uses this function to cancel shipping labels and request refunds when orders are cancelled

Request Body:

FieldTypeDescription
provider{ accessToken: string }Provider credentials
labelIdstringLabel/transaction ID to cancel

Response Format:

FieldTypeDescription
successbooleanWhether cancellation was successful
error?stringError message if cancellation failed
export async function cancelLabelFunction({ provider, labelId }) {
  try {
    const response = await fetch(`${SHIPPO_API_URL}/refunds/`, {
      method: "POST",
      headers: {
        Authorization: `ShippoToken ${provider.accessToken}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        transaction: labelId,
      }),
    });

    const refund = await response.json();
    
    return { success: true };
  } catch (error) {
    return {
      success: false,
      error: error.message,
    };
  }
}

Creating Your Custom Shipping Integration

Step 1: Create Integration File

Create your integration in /features/integrations/shipping/:

// /features/integrations/shipping/my-carrier.ts

const MY_CARRIER_API_URL = "https://api.mycarrier.com";

export async function getRatesFunction({ provider, order, dimensions }) {
  if (!dimensions) {
    throw new Error("Dimensions are required to get shipping rates");
  }

  const response = await fetch(`${MY_CARRIER_API_URL}/rates`, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${provider.accessToken}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      origin: provider.fromAddress,
      destination: {
        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,
      },
      package: {
        length: dimensions.length,
        width: dimensions.width,
        height: dimensions.height,
        weight: dimensions.weight,
        units: dimensions.unit,
      },
    }),
  });

  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.total_cost,
    currency: rate.currency,
    estimatedDays: rate.transit_days,
  }));
}

export async function createLabelFunction({
  provider,
  order,
  rateId,
  dimensions,
  lineItems,
}) {
  const response = await fetch(`${MY_CARRIER_API_URL}/labels`, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${provider.accessToken}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      rate_id: rateId,
      label_format: "PDF",
    }),
  });

  const label = await response.json();

  if (label.error) {
    throw new Error(label.error.message);
  }

  return {
    status: "purchased",
    data: label,
    carrier: label.carrier,
    service: label.service,
    trackingNumber: label.tracking_number,
    trackingUrl: label.tracking_url,
    labelUrl: label.label_url,
  };
}

export async function validateAddressFunction({ provider, address }) {
  try {
    const response = await fetch(`${MY_CARRIER_API_URL}/address/validate`, {
      method: "POST",
      headers: {
        Authorization: `Bearer ${provider.accessToken}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        name: `${address.firstName} ${address.lastName}`,
        street1: address.address1,
        street2: address.address2,
        city: address.city,
        state: address.province,
        zip: address.postalCode,
        country: address.country.iso2,
      }),
    });

    const validation = await response.json();

    return {
      isValid: validation.is_valid,
      suggestedAddress: validation.is_valid
        ? {
            address1: validation.corrected_address.street1,
            address2: validation.corrected_address.street2,
            city: validation.corrected_address.city,
            province: validation.corrected_address.state,
            postalCode: validation.corrected_address.zip,
            country: validation.corrected_address.country,
          }
        : null,
      errors: validation.errors || [],
    };
  } catch (error) {
    return {
      isValid: false,
      errors: [error.message],
    };
  }
}

export async function trackShipmentFunction({ provider, trackingNumber }) {
  const response = await fetch(`${MY_CARRIER_API_URL}/tracking/${trackingNumber}`, {
    headers: {
      Authorization: `Bearer ${provider.accessToken}`,
    },
  });

  const tracking = await response.json();

  return {
    status: tracking.status,
    estimatedDelivery: tracking.estimated_delivery,
    trackingUrl: tracking.tracking_url,
    events: tracking.events.map((event) => ({
      status: event.status,
      location: event.location,
      timestamp: event.timestamp,
      message: event.description,
    })),
  };
}

export async function cancelLabelFunction({ provider, labelId }) {
  try {
    const response = await fetch(`${MY_CARRIER_API_URL}/labels/${labelId}/cancel`, {
      method: "POST",
      headers: {
        Authorization: `Bearer ${provider.accessToken}`,
      },
    });

    const result = await response.json();
    
    return { success: result.cancelled };
  } catch (error) {
    return {
      success: false,
      error: error.message,
    };
  }
}

Step 2: Register the Integration

Add your integration to the shipping adapters registry:

// /features/integrations/shipping/index.ts
export const shippingProviderAdapters = {
  shippo: () => import("./shippo"),
  shipengine: () => import("./shipengine"),
  manual: () => import("./manual"),
  "my-carrier": () => import("./my-carrier"), // Add your integration
};

Step 3: Configure Environment Variables

Add the required environment variables to your .env file:

# My Carrier Configuration
MY_CARRIER_API_KEY=your_api_key_here

Step 4: Create Shipping Provider

In the Openfront admin panel:

  1. Navigate to Settings > Shipping Providers
  2. Click "Create Shipping Provider"
  3. Configure your provider:
Provider Configuration:
├── Name: "My Custom Carrier"
├── isActive: true
├── getRatesFunction: "my-carrier"
├── createLabelFunction: "my-carrier"
├── validateAddressFunction: "my-carrier"
├── trackShipmentFunction: "my-carrier"
├── cancelLabelFunction: "my-carrier"
├── accessToken: "your-api-key"
├── fromAddress: {
    "firstName": "John",
    "lastName": "Doe",
    "company": "Your Company",
    "address1": "123 Warehouse St",
    "city": "Warehouse City",
    "province": "CA",
    "postalCode": "12345",
    "country": { "iso2": "US" }
  }
└── metadata: {
    "environment": "sandbox",
    "supportedServices": ["ground", "express", "overnight"]
  }

Error Handling Best Practices

API Response Errors

Handle carrier API errors gracefully:

export async function getRatesFunction({ provider, order, dimensions }) {
  try {
    const response = await fetch(`${MY_CARRIER_API_URL}/rates`, {
      method: "POST",
      headers: {
        Authorization: `Bearer ${provider.accessToken}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        // request data
      }),
    });

    if (!response.ok) {
      const error = await response.json();
      throw new Error(error.message || `HTTP ${response.status}`);
    }

    const rates = await response.json();
    return rates.map(rate => ({
      // transform rate data
    }));
  } catch (error) {
    console.error('Rate calculation failed:', error);
    
    // Handle specific error types
    if (error.message.includes('INVALID_ADDRESS')) {
      throw new Error('Please verify the shipping address');
    }
    
    if (error.message.includes('SERVICE_UNAVAILABLE')) {
      throw new Error('Shipping service temporarily unavailable');
    }
    
    throw new Error(`Rate calculation failed: ${error.message}`);
  }
}

Dimension Validation

Validate package dimensions before making API calls:

function validateDimensions(dimensions) {
  if (!dimensions) {
    throw new Error('Package dimensions are required');
  }

  const required = ['length', 'width', 'height', 'weight'];
  for (const field of required) {
    if (!dimensions[field] || dimensions[field] <= 0) {
      throw new Error(`Invalid ${field}: must be greater than 0`);
    }
  }

  // Check maximum dimensions
  const maxDimension = 108; // inches
  if (dimensions.length > maxDimension || 
      dimensions.width > maxDimension || 
      dimensions.height > maxDimension) {
    throw new Error('Package dimensions exceed carrier limits');
  }
}

Testing Your Shipping Integration

Unit Testing

Create comprehensive tests for your shipping functions:

// /features/integrations/shipping/my-carrier.test.ts
import { getRatesFunction, createLabelFunction } from './my-carrier';

describe('My Carrier Integration', () => {
  const mockProvider = {
    id: 'provider_123',
    accessToken: 'test_token',
    fromAddress: {
      firstName: 'John',
      lastName: 'Doe',
      address1: '123 Warehouse St',
      city: 'Warehouse City',
      province: 'CA',
      postalCode: '12345',
      country: { iso2: 'US' }
    }
  };

  const mockOrder = {
    shippingAddress: {
      firstName: 'Jane',
      lastName: 'Smith',
      address1: '456 Customer Ave',
      city: 'Customer City',
      province: 'NY',
      postalCode: '67890',
      country: { iso2: 'US' }
    }
  };

  const mockDimensions = {
    length: 10,
    width: 8,
    height: 6,
    weight: 2,
    unit: 'in'
  };

  test('should get shipping rates successfully', async () => {
    const rates = await getRatesFunction({
      provider: mockProvider,
      order: mockOrder,
      dimensions: mockDimensions
    });

    expect(rates).toBeInstanceOf(Array);
    expect(rates[0]).toHaveProperty('id');
    expect(rates[0]).toHaveProperty('price');
  });

  test('should create shipping label successfully', async () => {
    const label = await createLabelFunction({
      provider: mockProvider,
      order: mockOrder,
      rateId: 'rate_123',
      dimensions: mockDimensions,
      lineItems: []
    });

    expect(label.trackingNumber).toBeDefined();
    expect(label.labelUrl).toBeDefined();
  });
});

Deployment Checklist

Pre-deployment Testing

  • Test all shipping functions with test credentials
  • Verify rate calculation for various package sizes
  • Test label creation and cancellation
  • Validate address verification functionality
  • Test tracking information retrieval

Production Deployment

  • Switch to production API credentials
  • Update fromAddress with actual warehouse location
  • Configure webhook endpoints for tracking updates
  • Set up monitoring and alerting
  • Test with real shipping scenarios

Post-deployment Monitoring

  • Monitor shipping rate calculation success rates
  • Track label creation and printing success
  • Monitor tracking information accuracy
  • Set up alerts for API failures
  • Regular audit of shipping costs vs. rates charged

Your custom shipping integration is now ready to calculate rates and create shipping labels through Openfront while maintaining reliability and accuracy standards.