Creating a Custom Shipping Provider
Build custom shipping integrations for Openfront
Learn how to create custom shipping providers to integrate any shipping carrier or fulfillment service with Openfront. This guide covers both file-based adapters and HTTP endpoint integrations.
Overview
Openfront's shipping provider system allows you to integrate any shipping carrier through a standardized interface. You can either:
- File-based adapter: Create a local adapter module
- HTTP endpoint: Use external API endpoints for fulfillment services
Prerequisites
Before creating a custom shipping provider:
- Understanding of JavaScript/TypeScript
- Access to shipping carrier API documentation
- Development environment with Openfront
- API credentials for your shipping service
Understanding the Shipping Provider Interface
Every shipping provider must implement these core functions:
interface ShippingProviderAdapter {
getRatesFunction(params: RatesParams): Promise<RateResult[]>;
createLabelFunction(params: LabelParams): Promise<LabelResult>;
validateAddressFunction(params: AddressParams): Promise<ValidationResult>;
trackShipmentFunction(params: TrackingParams): Promise<TrackingResult>;
cancelLabelFunction(params: CancelParams): Promise<CancelResult>;
}
Option 1: File-Based Adapter
Create Adapter File
Create your adapter in /features/integrations/shipping/
:
// /features/integrations/shipping/my-custom-carrier.ts
export async function getRatesFunction({ provider, order, dimensions }) {
try {
// Initialize your shipping carrier client
const carrier = new MyShippingCarrier({
apiKey: provider.accessToken,
environment: provider.metadata.environment || 'sandbox'
});
// Prepare shipment data
const shipment = {
from: provider.fromAddress,
to: order.shippingAddress,
packages: dimensions.map(dim => ({
length: dim.length,
width: dim.width,
height: dim.height,
weight: dim.weight,
units: dim.units
}))
};
// Get rates from carrier
const rates = await carrier.getRates(shipment);
// Transform to Openfront format
return rates.map(rate => ({
carrierId: rate.carrier_code,
serviceType: rate.service_code,
serviceName: rate.service_name,
amount: Math.round(rate.total_cost * 100), // Convert to cents
currency: rate.currency,
estimatedDays: rate.estimated_delivery_days,
metadata: {
rateId: rate.id,
transitTime: rate.transit_time
}
}));
} catch (error) {
console.error('Rate calculation failed:', error);
throw new Error(`Rate calculation failed: ${error.message}`);
}
}
export async function createLabelFunction({ provider, order, rateId, dimensions, lineItems }) {
try {
const carrier = new MyShippingCarrier({
apiKey: provider.accessToken,
environment: provider.metadata.environment
});
// Prepare shipment data
const shipmentData = {
from: provider.fromAddress,
to: order.shippingAddress,
packages: dimensions,
rateId: rateId,
contents: lineItems.map(item => ({
description: item.productVariant.product.title,
quantity: item.quantity,
value: item.unitPrice,
weight: item.productVariant.weight || 1,
sku: item.productVariant.sku
}))
};
// Create shipping label
const label = await carrier.createLabel(shipmentData);
return {
labelId: label.id,
trackingNumber: label.tracking_number,
labelUrl: label.label_url,
carrierId: label.carrier_id,
serviceType: label.service_type,
cost: Math.round(label.cost * 100), // Convert to cents
metadata: {
labelFormat: label.format,
estimatedDelivery: label.estimated_delivery_date
}
};
} catch (error) {
console.error('Label creation failed:', error);
throw new Error(`Label creation failed: ${error.message}`);
}
}
export async function validateAddressFunction({ provider, address }) {
try {
const carrier = new MyShippingCarrier({
apiKey: provider.accessToken,
environment: provider.metadata.environment
});
const validation = await carrier.validateAddress({
street1: address.address1,
street2: address.address2,
city: address.city,
state: address.province,
zip: address.postalCode,
country: address.countryCode
});
return {
isValid: validation.is_valid,
correctedAddress: validation.is_valid ? {
address1: validation.normalized_address.street1,
address2: validation.normalized_address.street2,
city: validation.normalized_address.city,
province: validation.normalized_address.state,
postalCode: validation.normalized_address.zip,
countryCode: validation.normalized_address.country
} : null,
messages: validation.messages || []
};
} catch (error) {
console.error('Address validation failed:', error);
return {
isValid: false,
correctedAddress: null,
messages: [`Address validation failed: ${error.message}`]
};
}
}
export async function trackShipmentFunction({ provider, trackingNumber }) {
try {
const carrier = new MyShippingCarrier({
apiKey: provider.accessToken,
environment: provider.metadata.environment
});
const tracking = await carrier.trackShipment(trackingNumber);
// Map carrier status to Openfront status
const statusMap = {
'created': 'created',
'picked_up': 'in_transit',
'in_transit': 'in_transit',
'out_for_delivery': 'out_for_delivery',
'delivered': 'delivered',
'exception': 'exception',
'returned': 'returned'
};
return {
status: statusMap[tracking.status] || 'unknown',
estimatedDelivery: tracking.estimated_delivery_date,
actualDelivery: tracking.delivered_date,
events: tracking.tracking_events.map(event => ({
timestamp: event.occurred_at,
status: statusMap[event.status] || event.status,
location: event.city_locality ? `${event.city_locality}, ${event.state_province}` : '',
description: event.description
}))
};
} catch (error) {
console.error('Tracking failed:', error);
throw new Error(`Tracking failed: ${error.message}`);
}
}
export async function cancelLabelFunction({ provider, labelId }) {
try {
const carrier = new MyShippingCarrier({
apiKey: provider.accessToken,
environment: provider.metadata.environment
});
const cancellation = await carrier.cancelLabel(labelId);
return {
success: cancellation.status === 'cancelled',
refundAmount: cancellation.refund_amount ? Math.round(cancellation.refund_amount * 100) : 0,
refundId: cancellation.refund_id,
message: cancellation.message
};
} catch (error) {
console.error('Label cancellation failed:', error);
throw new Error(`Label cancellation failed: ${error.message}`);
}
}
Register the Adapter
Add your adapter to the adapter registry:
// /features/integrations/shipping/index.ts
export const shippingProviderAdapters = {
shippo: () => import("./shippo"),
shipengine: () => import("./shipengine"),
manual: () => import("./manual"),
"my-custom-carrier": () => import("./my-custom-carrier"), // Add your adapter
};
Create Shipping Provider
In the Openfront admin:
- Go to Shipping Providers
- Click "Create Provider"
- Configure settings:
Provider Configuration:
├── Name: "My Custom Carrier"
├── isActive: true
├── getRatesFunction: "my-custom-carrier"
├── createLabelFunction: "my-custom-carrier"
├── validateAddressFunction: "my-custom-carrier"
├── trackShipmentFunction: "my-custom-carrier"
├── cancelLabelFunction: "my-custom-carrier"
├── accessToken: "your-api-key"
├── fromAddress: {
"company": "Your Company",
"address1": "123 Warehouse St",
"city": "Warehouse City",
"province": "State",
"postalCode": "12345",
"countryCode": "US"
}
└── metadata: {
"environment": "sandbox",
"supportedServices": ["ground", "express", "overnight"]
}
Option 2: HTTP Endpoint Integration
Deploy HTTP Endpoints
Create external API endpoints that match Openfront's interface:
// Example: Express.js endpoint for getRates
app.post('/api/shipping/rates', async (req, res) => {
try {
const { provider, order, dimensions } = req.body;
// Your rate calculation logic
const rates = await yourShippingService.calculateRates({
origin: provider.fromAddress,
destination: order.shippingAddress,
packages: dimensions,
credentials: provider.accessToken
});
res.json(rates.map(rate => ({
carrierId: rate.carrier,
serviceType: rate.service,
serviceName: rate.name,
amount: rate.price,
currency: rate.currency,
estimatedDays: rate.transitTime
})));
} catch (error) {
res.status(400).json({ error: error.message });
}
});
app.post('/api/shipping/create-label', async (req, res) => {
try {
const { provider, order, rateId, dimensions, lineItems } = req.body;
const label = await yourShippingService.createLabel({
rateId,
order,
packages: dimensions,
contents: lineItems
});
res.json({
labelId: label.id,
trackingNumber: label.tracking,
labelUrl: label.labelUrl,
carrierId: label.carrier,
serviceType: label.service,
cost: label.cost
});
} catch (error) {
res.status(400).json({ error: error.message });
}
});
// Implement other endpoints...
Configure HTTP Provider
Create provider with HTTP endpoint URLs:
Provider Configuration:
├── Name: "My HTTP Carrier"
├── isActive: true
├── getRatesFunction: "https://api.mycarrier.com/shipping/rates"
├── createLabelFunction: "https://api.mycarrier.com/shipping/create-label"
├── validateAddressFunction: "https://api.mycarrier.com/shipping/validate"
├── trackShipmentFunction: "https://api.mycarrier.com/shipping/track"
├── cancelLabelFunction: "https://api.mycarrier.com/shipping/cancel"
├── accessToken: "your-api-token"
└── fromAddress: { ... }
Handling Dimensions and Units
Openfront standardizes package dimensions. Your adapter should handle unit conversions:
function convertDimensions(dimensions, targetUnits) {
return dimensions.map(dim => {
const converted = { ...dim };
// Convert length units
if (dim.units.length !== targetUnits.length) {
const lengthMultiplier = dim.units.length === 'cm' ? 0.393701 : 2.54;
converted.length *= lengthMultiplier;
converted.width *= lengthMultiplier;
converted.height *= lengthMultiplier;
}
// Convert weight units
if (dim.units.weight !== targetUnits.weight) {
const weightMultipliers = {
'lb_to_kg': 0.453592,
'kg_to_lb': 2.20462,
'oz_to_g': 28.3495,
'g_to_oz': 0.035274
};
const key = `${dim.units.weight}_to_${targetUnits.weight}`;
if (weightMultipliers[key]) {
converted.weight *= weightMultipliers[key];
}
}
converted.units = targetUnits;
return converted;
});
}
Testing Your Shipping Provider
Rate Calculation Testing
// Test rate calculation
const testOrder = {
shippingAddress: {
address1: '123 Test St',
city: 'Test City',
province: 'CA',
postalCode: '90210',
countryCode: 'US'
}
};
const testDimensions = [{
length: 10,
width: 8,
height: 6,
weight: 2,
units: { length: 'in', weight: 'lb' }
}];
const rates = await getRatesFunction({
provider: testProvider,
order: testOrder,
dimensions: testDimensions
});
console.log('Calculated rates:', rates);
Label Creation Testing
Test the complete shipping flow:
- Calculate rates
- Select a rate
- Create shipping label
- Verify tracking number
- Test label cancellation
Address Validation Testing
Test various address scenarios:
- Valid addresses
- Invalid addresses
- International addresses
- PO boxes and military addresses
Error Handling Best Practices
Network Errors
// Implement retry logic for network failures
async function apiCallWithRetry(apiCall, maxRetries = 3) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await apiCall();
} catch (error) {
if (attempt === maxRetries || !isRetryableError(error)) {
throw error;
}
// Exponential backoff
await new Promise(resolve =>
setTimeout(resolve, Math.pow(2, attempt) * 1000)
);
}
}
}
function isRetryableError(error) {
return error.code === 'NETWORK_ERROR' ||
error.status >= 500 ||
error.code === 'TIMEOUT';
}
Validation Errors
// Validate required fields
function validateShipmentData(order, dimensions) {
if (!order.shippingAddress) {
throw new Error('Shipping address is required');
}
if (!dimensions || dimensions.length === 0) {
throw new Error('Package dimensions are required');
}
dimensions.forEach((dim, index) => {
if (!dim.weight || dim.weight <= 0) {
throw new Error(`Invalid weight for package ${index + 1}`);
}
});
}
Security Considerations
API Key Management
- Store API keys securely in provider configuration
- Use environment-specific keys (sandbox vs production)
- Rotate API keys regularly
- Monitor for unauthorized usage
Address Data Protection
- Don't log complete addresses in production
- Implement proper data retention policies
- Comply with regional privacy regulations
- Use secure communication protocols
Monitoring and Logging
Key Metrics to Monitor
- Rate calculation success rates
- Label creation success rates
- Average response times
- Error rates by operation type
Logging Best Practices
// Log important events without sensitive data
console.log(`Rates calculated for order ${order.id}: ${rates.length} options`);
console.log(`Label created: ${labelId} for tracking ${trackingNumber}`);
// Don't log sensitive information
// console.log(`API Key: ${apiKey}`); // ❌ Never do this
// console.log(`Full address: ${JSON.stringify(address)}`); // ❌ Avoid in production
Deployment Checklist
Pre-deployment Testing
- Test all functions with various scenarios
- Verify error handling for edge cases
- Test with production-like data volumes
- Validate address handling for target regions
- Test webhook endpoints (if applicable)
Production Deployment
- Switch to production API credentials
- Update environment configurations
- Set up monitoring and alerting
- Document configuration for team members
- Plan rollback procedures
Post-deployment Monitoring
- Monitor success rates for all operations
- Track response times and performance
- Set up alerts for high error rates
- Regular audit of shipping costs vs actual rates
Creating custom shipping providers enables you to integrate any shipping carrier while maintaining Openfront's consistent rate calculation and fulfillment experience.