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:
Field | Type | Description |
---|---|---|
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:
Field | Type | Description |
---|---|---|
rates | Rate[] | 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:
Field | Type | Description |
---|---|---|
provider | { id: string; accessToken: string; fromAddress: object } | Shipping provider configuration |
order | { shippingAddress: object } | Order with shipping details |
rateId | string | Selected shipping rate ID |
dimensions | object | Package dimensions |
lineItems | array | Order line items for customs declarations |
Response Format:
Field | Type | Description |
---|---|---|
status | string | Label creation status |
trackingNumber | string | Tracking number for the shipment |
labelUrl | string | URL to download the shipping label PDF |
carrier | string | Shipping carrier name |
service | string | Service 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:
Field | Type | Description |
---|---|---|
provider | { accessToken: string } | Provider credentials |
address | object | Address to validate |
Response Format:
Field | Type | Description |
---|---|---|
isValid | boolean | Whether the address is valid |
suggestedAddress | object | Corrected address if validation found issues |
errors | string[] | 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:
Field | Type | Description |
---|---|---|
provider | { accessToken: string } | Provider credentials |
trackingNumber | string | Tracking number to lookup |
Response Format:
Field | Type | Description |
---|---|---|
status | string | Current shipment status |
estimatedDelivery | string | Estimated delivery date |
trackingUrl | string | URL for carrier tracking page |
events | array | Array 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:
Field | Type | Description |
---|---|---|
provider | { accessToken: string } | Provider credentials |
labelId | string | Label/transaction ID to cancel |
Response Format:
Field | Type | Description |
---|---|---|
success | boolean | Whether cancellation was successful |
error? | string | Error 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:
- Navigate to Settings > Shipping Providers
- Click "Create Shipping Provider"
- 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.