Shippo provides a unified API that connects businesses with multiple shipping carriers, simplifying label generation, rate comparison, and tracking. For Java applications, integrating with Shippo enables comprehensive shipping automation for e-commerce platforms, marketplaces, and logistics systems. This guide covers practical patterns for leveraging Shippo's API to create efficient shipping workflows.
Understanding Shippo API Architecture
Shippo operates on a RESTful architecture with:
- Carrier-agnostic API for 80+ shipping providers
- Webhook-driven notifications for tracking updates
- Batch operations for high-volume shipping
- Address validation and international shipping support
Core Integration Patterns
1. Project Setup and Dependencies
Configure Shippo Java SDK and HTTP client dependencies.
Maven Configuration:
<dependencies> <!-- Shippo Java SDK --> <dependency> <groupId>com.github.goshippo</groupId> <artifactId>shippo-java-client</artifactId> <version>2.1.0</version> </dependency> <!-- HTTP Client --> <dependency> <groupId>org.apache.httpcomponents</groupId> <artifactId>httpclient</artifactId> <version>4.5.14</version> </dependency> <!-- JSON Processing --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.15.2</version> </dependency> </dependencies>
2. Configuration and Client Setup
Configure Shippo API credentials and initialize the client.
Shippo Configuration:
@Configuration
@ConfigurationProperties(prefix = "shippo")
@Data
public class ShippoConfig {
private String apiKey;
private String apiBaseUrl = "https://api.goshippo.com";
private String webhookSecret;
private int timeout = 30000;
@Bean
public Shippo shippoClient() {
Shippo.setApiKey(apiKey);
Shippo.setApiVersion("2018-02-08");
return new Shippo();
}
}
@Component
public class ShippoService {
private final Shippo shippoClient;
private final ObjectMapper objectMapper;
public ShippoService(Shippo shippoClient, ObjectMapper objectMapper) {
this.shippoClient = shippoClient;
this.objectMapper = objectMapper;
}
}
3. Domain Models for Shipping Entities
Create Java models that represent Shippo entities and shipping concepts.
Core Domain Models:
@Data
public class ShippingAddress {
private String name;
private String company;
private String street1;
private String street2;
private String city;
private String state;
private String zip;
private String country;
private String phone;
private String email;
public boolean isInternational() {
return !"US".equalsIgnoreCase(country);
}
}
@Data
public class PackageDimension {
private BigDecimal length;
private BigDecimal width;
private BigDecimal height;
private String distanceUnit; // "in", "cm"
private BigDecimal weight;
private String massUnit; // "lb", "kg"
public String getDimensionString() {
return String.format("%sx% sx%s %s", length, width, height, distanceUnit);
}
}
@Data
public class ShipmentRequest {
private ShippingAddress fromAddress;
private ShippingAddress toAddress;
private ShippingAddress returnAddress;
private List<PackageDimension> parcels;
private String carrierAccount;
private List<String> services; // "usps_priority", "fedex_ground", etc.
private Map<String, Object> metadata;
private String orderId;
private String contentDescription;
}
@Data
public class ShippingRate {
private String rateId;
private String carrier;
private String service;
private BigDecimal amount;
private BigDecimal retailAmount;
private String currency;
private String durationTerms;
private Date estimatedDays;
private String provider;
private String serviceLevel;
public String getDisplayName() {
return String.format("%s %s - $%s", carrier, service, amount);
}
}
@Data
public class ShippingLabel {
private String labelId;
private String trackingNumber;
private String trackingUrl;
private String labelUrl;
private String labelFileType; // "PDF", "PNG"
private byte[] labelData;
private Map<String, Object> metadata;
private String commercialInvoiceUrl; // For international
}
4. Address Validation Service
Validate and verify shipping addresses before creating shipments.
Address Validation Service:
@Service
@Slf4j
public class AddressValidationService {
private final Shippo shippoClient;
public AddressValidationService(Shippo shippoClient) {
this.shippoClient = shippoClient;
}
public AddressValidationResult validateAddress(ShippingAddress address) {
try {
Map<String, Object> addressMap = createAddressMap(address);
// Create address in Shippo for validation
com.goshippo.model.Address shippoAddress =
com.goshippo.model.Address.create(addressMap);
return mapToValidationResult(shippoAddress);
} catch (Exception e) {
log.error("Address validation failed for: {}", address, e);
return AddressValidationResult.failed("Validation error: " + e.getMessage());
}
}
public boolean isAddressValid(ShippingAddress address) {
AddressValidationResult result = validateAddress(address);
return result.isValid();
}
private Map<String, Object> createAddressMap(ShippingAddress address) {
Map<String, Object> addressMap = new HashMap<>();
addressMap.put("name", address.getName());
addressMap.put("company", address.getCompany());
addressMap.put("street1", address.getStreet1());
addressMap.put("street2", address.getStreet2());
addressMap.put("city", address.getCity());
addressMap.put("state", address.getState());
addressMap.put("zip", address.getZip());
addressMap.put("country", address.getCountry());
addressMap.put("phone", address.getPhone());
addressMap.put("email", address.getEmail());
addressMap.put("validate", true);
return addressMap;
}
private AddressValidationResult mapToValidationResult(
com.goshippo.model.Address shippoAddress) {
AddressValidationResult result = new AddressValidationResult();
result.setOriginalAddress(shippoAddress.getName() + ", " + shippoAddress.getStreet1());
if (Boolean.TRUE.equals(shippoAddress.getIsComplete())) {
result.setValid(true);
result.setValidatedAddress(shippoAddress.toString());
} else {
result.setValid(false);
result.setValidationMessages(shippoAddress.getValidationResults().getMessages());
}
return result;
}
@Data
public static class AddressValidationResult {
private boolean isValid;
private String originalAddress;
private String validatedAddress;
private List<String> validationMessages;
public static AddressValidationResult failed(String message) {
AddressValidationResult result = new AddressValidationResult();
result.setValid(false);
result.setValidationMessages(List.of(message));
return result;
}
}
}
5. Shipment and Rate Management
Create shipments and retrieve shipping rates from multiple carriers.
Shipment Service:
@Service
@Slf4j
public class ShipmentService {
private final Shippo shippoClient;
private final AddressValidationService addressValidationService;
public ShipmentService(Shippo shippoClient,
AddressValidationService addressValidationService) {
this.shippoClient = shippoClient;
this.addressValidationService = addressValidationService;
}
public Shipment createShipment(ShipmentRequest request) throws ShippingException {
try {
// Validate addresses first
if (!addressValidationService.isAddressValid(request.getToAddress())) {
throw new ShippingException("Invalid destination address");
}
Map<String, Object> shipmentMap = createShipmentMap(request);
com.goshippo.model.Shipment shippoShipment =
com.goshippo.model.Shipment.create(shipmentMap);
return mapToShipment(shippoShipment);
} catch (Exception e) {
log.error("Shipment creation failed for order: {}", request.getOrderId(), e);
throw new ShippingException("Failed to create shipment: " + e.getMessage(), e);
}
}
public List<ShippingRate> getShippingRates(String shipmentId, String carrier)
throws ShippingException {
try {
Map<String, Object> params = new HashMap<>();
if (carrier != null) {
params.put("carrier", carrier);
}
com.goshippo.model.RateCollection rates =
com.goshippo.model.Rate.all(shipmentId, params);
return rates.getData().stream()
.map(this::mapToShippingRate)
.collect(Collectors.toList());
} catch (Exception e) {
throw new ShippingException("Failed to get shipping rates: " + e.getMessage(), e);
}
}
public ShippingRate getCheapestRate(String shipmentId) throws ShippingException {
List<ShippingRate> rates = getShippingRates(shipmentId, null);
return rates.stream()
.min(Comparator.comparing(ShippingRate::getAmount))
.orElseThrow(() -> new ShippingException("No rates available"));
}
public ShippingRate getFastestRate(String shipmentId) throws ShippingException {
List<ShippingRate> rates = getShippingRates(shipmentId, null);
return rates.stream()
.min(Comparator.comparing(rate ->
rate.getEstimatedDays() != null ? rate.getEstimatedDays() : Integer.MAX_VALUE))
.orElseThrow(() -> new ShippingException("No rates available"));
}
private Map<String, Object> createShipmentMap(ShipmentRequest request) {
Map<String, Object> shipmentMap = new HashMap<>();
// Addresses
shipmentMap.put("address_from", createAddressMap(request.getFromAddress()));
shipmentMap.put("address_to", createAddressMap(request.getToAddress()));
if (request.getReturnAddress() != null) {
shipmentMap.put("address_return", createAddressMap(request.getReturnAddress()));
}
// Parcels
List<Map<String, Object>> parcels = request.getParcels().stream()
.map(this::createParcelMap)
.collect(Collectors.toList());
shipmentMap.put("parcels", parcels);
// Additional options
Map<String, Object> extra = new HashMap<>();
if (request.isInternational()) {
extra.put("insurance", Map.of("amount", "100.00", "currency", "USD"));
extra.put("contents", request.getContentDescription());
}
shipmentMap.put("extra", extra);
shipmentMap.put("metadata", request.getMetadata());
shipmentMap.put("async", false);
return shipmentMap;
}
private Map<String, Object> createParcelMap(PackageDimension parcel) {
Map<String, Object> parcelMap = new HashMap<>();
parcelMap.put("length", parcel.getLength().toString());
parcelMap.put("width", parcel.getWidth().toString());
parcelMap.put("height", parcel.getHeight().toString());
parcelMap.put("distance_unit", parcel.getDistanceUnit());
parcelMap.put("weight", parcel.getWeight().toString());
parcelMap.put("mass_unit", parcel.getMassUnit());
return parcelMap;
}
private ShippingRate mapToShippingRate(com.goshippo.model.Rate rate) {
ShippingRate shippingRate = new ShippingRate();
shippingRate.setRateId(rate.getObjectId());
shippingRate.setCarrier(rate.getProvider());
shippingRate.setService(rate.getServicelevelName());
shippingRate.setAmount(new BigDecimal(rate.getAmount()));
shippingRate.setCurrency(rate.getCurrency());
shippingRate.setEstimatedDays(rate.getEstimatedDays());
shippingRate.setProvider(rate.getProvider());
shippingRate.setServiceLevel(rate.getServicelevelToken());
return shippingRate;
}
}
6. Label Generation and Tracking
Generate shipping labels and handle tracking.
Label Service:
@Service
@Slf4j
public class LabelService {
private final Shippo shippoClient;
public LabelService(Shippo shippoClient) {
this.shippoClient = shippoClient;
}
public ShippingLabel createLabel(String rateId, String labelFormat) throws ShippingException {
try {
Map<String, Object> transactionParams = new HashMap<>();
transactionParams.put("rate", rateId);
transactionParams.put("label_file_type", labelFormat != null ? labelFormat : "PDF");
transactionParams.put("async", false);
com.goshippo.model.Transaction transaction =
com.goshippo.model.Transaction.create(transactionParams);
return mapToShippingLabel(transaction);
} catch (Exception e) {
throw new ShippingException("Failed to create shipping label: " + e.getMessage(), e);
}
}
public ShippingLabel createLabelWithInsurance(String rateId, BigDecimal insuranceAmount,
String currency) throws ShippingException {
try {
Map<String, Object> transactionParams = new HashMap<>();
transactionParams.put("rate", rateId);
transactionParams.put("label_file_type", "PDF");
transactionParams.put("async", false);
// Add insurance for high-value items
Map<String, Object> extra = new HashMap<>();
extra.put("insurance", Map.of(
"amount", insuranceAmount.toString(),
"currency", currency
));
transactionParams.put("extra", extra);
com.goshippo.model.Transaction transaction =
com.goshippo.model.Transaction.create(transactionParams);
return mapToShippingLabel(transaction);
} catch (Exception e) {
throw new ShippingException("Failed to create insured label: " + e.getMessage(), e);
}
}
public Track getTrackingStatus(String carrier, String trackingNumber) throws ShippingException {
try {
com.goshippo.model.Track track =
com.goshippo.model.Track.getTrackingInfo(carrier, trackingNumber);
return mapToTrack(track);
} catch (Exception e) {
throw new ShippingException("Failed to get tracking info: " + e.getMessage(), e);
}
}
private ShippingLabel mapToShippingLabel(com.goshippo.model.Transaction transaction) {
ShippingLabel label = new ShippingLabel();
label.setLabelId(transaction.getObjectId());
label.setTrackingNumber(transaction.getTrackingNumber());
label.setTrackingUrl(transaction.getTrackingUrlProvider());
label.setLabelUrl(transaction.getLabelUrl());
label.setLabelFileType(transaction.getLabelFileType());
// Download label data
if (transaction.getLabelUrl() != null) {
try {
label.setLabelData(downloadLabelData(transaction.getLabelUrl()));
} catch (Exception e) {
log.warn("Failed to download label data from URL: {}", transaction.getLabelUrl());
}
}
return label;
}
private byte[] downloadLabelData(String labelUrl) throws IOException {
try (CloseableHttpClient client = HttpClients.createDefault()) {
HttpGet request = new HttpGet(labelUrl);
try (CloseableHttpResponse response = client.execute(request);
InputStream is = response.getEntity().getContent()) {
return IOUtils.toByteArray(is);
}
}
}
private Track mapToTrack(com.goshippo.model.Track shippoTrack) {
Track track = new Track();
track.setCarrier(shippoTrack.getCarrier());
track.setTrackingNumber(shippoTrack.getTrackingNumber());
track.setTrackingStatus(shippoTrack.getTrackingStatus().getStatus());
track.setEstimatedDelivery(shippoTrack.getEta());
List<TrackingEvent> events = shippoTrack.getTrackingHistory().stream()
.map(this::mapToTrackingEvent)
.collect(Collectors.toList());
track.setEvents(events);
return track;
}
@Data
public static class Track {
private String carrier;
private String trackingNumber;
private String trackingStatus;
private Date estimatedDelivery;
private List<TrackingEvent> events;
}
@Data
public static class TrackingEvent {
private Date eventDate;
private String status;
private String location;
private String description;
}
}
7. Webhook Handling for Tracking Updates
Process Shippo webhooks for real-time tracking updates.
Webhook Controller:
@RestController
@RequestMapping("/webhooks/shippo")
@Slf4j
public class ShippoWebhookController {
private final OrderService orderService;
private final NotificationService notificationService;
@Value("${shippo.webhook.secret}")
private String webhookSecret;
public ShippoWebhookController(OrderService orderService,
NotificationService notificationService) {
this.orderService = orderService;
this.notificationService = notificationService;
}
@PostMapping("/tracking")
public ResponseEntity<String> handleTrackingUpdate(
@RequestBody String payload,
@RequestHeader("X-Shippo-Webhook-Signature") String signature) {
if (!verifyWebhookSignature(payload, signature)) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
try {
ShippoWebhookEvent event = objectMapper.readValue(payload, ShippoWebhookEvent.class);
processWebhookEvent(event).subscribe();
return ResponseEntity.accepted().build();
} catch (Exception e) {
log.error("Failed to process Shippo webhook", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}
private Mono<Void> processWebhookEvent(ShippoWebhookEvent event) {
return Mono.fromRunnable(() -> {
String eventType = event.getEvent();
Map<String, Object> data = event.getData();
switch (eventType) {
case "track_updated":
handleTrackUpdated(data);
break;
case "transaction_created":
handleTransactionCreated(data);
break;
case "track_delivered":
handleDeliveryConfirmed(data);
break;
}
});
}
private void handleTrackUpdated(Map<String, Object> data) {
String trackingNumber = (String) data.get("tracking_number");
String status = (String) data.get("tracking_status");
String orderId = (String) data.get("metadata", Map.class).get("order_id");
log.info("Tracking update for order {}: {} - {}", orderId, trackingNumber, status);
orderService.updateOrderTrackingStatus(orderId, trackingNumber, status);
notificationService.sendTrackingUpdate(orderId, trackingNumber, status);
}
private void handleDeliveryConfirmed(Map<String, Object> data) {
String trackingNumber = (String) data.get("tracking_number");
String orderId = (String) data.get("metadata", Map.class).get("order_id");
log.info("Delivery confirmed for order {}: {}", orderId, trackingNumber);
orderService.markOrderDelivered(orderId);
notificationService.sendDeliveryConfirmation(orderId);
}
private boolean verifyWebhookSignature(String payload, String signature) {
try {
Mac sha256Hmac = Mac.getInstance("HmacSHA256");
SecretKeySpec secretKey = new SecretKeySpec(webhookSecret.getBytes(), "HmacSHA256");
sha256Hmac.init(secretKey);
String computedSignature = Base64.getEncoder()
.encodeToString(sha256Hmac.doFinal(payload.getBytes()));
return computedSignature.equals(signature);
} catch (Exception e) {
log.error("Webhook signature verification failed", e);
return false;
}
}
}
8. Batch Shipment Processing
Handle high-volume shipping with batch operations.
Batch Shipment Service:
@Service
public class BatchShipmentService {
private final Shippo shippoClient;
private final ShipmentService shipmentService;
public BatchShipmentService(Shippo shippoClient, ShipmentService shipmentService) {
this.shippoClient = shippoClient;
this.shipmentService = shipmentService;
}
@Async
public CompletableFuture<List<ShipmentResult>> processBatchShipments(
List<ShipmentRequest> requests) {
List<CompletableFuture<ShipmentResult>> futures = requests.stream()
.map(request -> CompletableFuture.supplyAsync(() ->
processSingleShipment(request)))
.collect(Collectors.toList());
return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]))
.thenApply(v -> futures.stream()
.map(CompletableFuture::join)
.collect(Collectors.toList()));
}
private ShipmentResult processSingleShipment(ShipmentRequest request) {
try {
Shipment shipment = shipmentService.createShipment(request);
ShippingRate bestRate = shipmentService.getCheapestRate(shipment.getId());
ShippingLabel label = labelService.createLabel(bestRate.getRateId(), "PDF");
return ShipmentResult.success(request.getOrderId(), label);
} catch (Exception e) {
log.error("Batch shipment failed for order: {}", request.getOrderId(), e);
return ShipmentResult.failed(request.getOrderId(), e.getMessage());
}
}
@Data
public static class ShipmentResult {
private String orderId;
private boolean success;
private ShippingLabel label;
private String errorMessage;
public static ShipmentResult success(String orderId, ShippingLabel label) {
ShipmentResult result = new ShipmentResult();
result.setOrderId(orderId);
result.setSuccess(true);
result.setLabel(label);
return result;
}
public static ShipmentResult failed(String orderId, String errorMessage) {
ShipmentResult result = new ShipmentResult();
result.setOrderId(orderId);
result.setSuccess(false);
result.setErrorMessage(errorMessage);
return result;
}
}
}
Best Practices for Shippo Integration
- Address Validation: Always validate addresses before creating shipments
- Error Handling: Implement comprehensive error handling for carrier API failures
- Rate Caching: Cache shipping rates for frequently shipped routes
- Webhook Security: Always verify webhook signatures
- Carrier Accounts: Use dedicated carrier accounts for high-volume shipping
- International Shipping: Handle customs forms and commercial invoices
- Testing: Use Shippo's test mode for development and staging
Conclusion: Unified Shipping Automation
Shippo provides Java developers with a powerful platform for unifying shipping operations across multiple carriers. By implementing address validation, rate comparison, label generation, and tracking workflows, businesses can create seamless shipping experiences while optimizing costs and delivery times.
This integration demonstrates that modern shipping doesn't require complex carrier-specific implementations—Shippo's unified API combined with Java's enterprise capabilities creates a robust foundation for scalable e-commerce logistics, enabling businesses to ship smarter while focusing on core product development.