AfterShip Tracking in Java: Enterprise Package Visibility

AfterShip provides a comprehensive package tracking API that aggregates data from 900+ carriers worldwide, offering real-time visibility into shipment status, delivery estimates, and exception handling. For Java applications, integrating with AfterShip enables robust tracking capabilities for e-commerce platforms, logistics systems, and customer service applications. This guide covers practical patterns for implementing end-to-end tracking solutions.

Understanding AfterShip API Architecture

AfterShip operates on a RESTful architecture with:

  • Multi-carrier tracking with unified response format
  • Webhook-driven notifications for status updates
  • Automated detection of couriers and tracking numbers
  • Predictive delivery dates and exception alerts
  • Custom tracking pages for customer-facing status

Core Integration Patterns

1. Project Setup and Dependencies

Configure AfterShip Java client and HTTP dependencies.

Maven Configuration:

<dependencies>
<!-- 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>
<!-- Reactive Streams -->
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-core</artifactId>
<version>3.5.10</version>
</dependency>
</dependencies>

2. Configuration and Client Setup

Configure AfterShip API credentials and initialize the client.

AfterShip Configuration:

@Configuration
@ConfigurationProperties(prefix = "aftership")
@Data
public class AfterShipConfig {
private String apiKey;
private String apiBaseUrl = "https://api.aftership.com";
private String apiVersion = "2024-10";
private String webhookSecret;
private int timeout = 30000;
@Bean
public WebClient afterShipWebClient() {
return WebClient.builder()
.baseUrl(apiBaseUrl + "/" + apiVersion)
.defaultHeader("Content-Type", "application/json")
.defaultHeader("as-api-key", apiKey)
.build();
}
}
@Component
public class AfterShipClient {
private final WebClient webClient;
private final ObjectMapper objectMapper;
public AfterShipClient(WebClient afterShipWebClient, ObjectMapper objectMapper) {
this.webClient = afterShipWebClient;
this.objectMapper = objectMapper;
}
public <T> Mono<T> executeGet(String path, Map<String, Object> params, Class<T> responseType) {
return webClient.get()
.uri(uriBuilder -> {
UriBuilder builder = uriBuilder.path(path);
if (params != null) {
params.forEach((key, value) -> builder.queryParam(key, value.toString()));
}
return builder.build();
})
.retrieve()
.bodyToMono(responseType);
}
public <T> Mono<T> executePost(String path, Object body, Class<T> responseType) {
return webClient.post()
.uri(path)
.bodyValue(body)
.retrieve()
.bodyToMono(responseType);
}
public <T> Mono<T> executePut(String path, Object body, Class<T> responseType) {
return webClient.put()
.uri(path)
.bodyValue(body)
.retrieve()
.bodyToMono(responseType);
}
public Mono<Void> executeDelete(String path) {
return webClient.delete()
.uri(path)
.retrieve()
.bodyToMono(Void.class);
}
}

3. Domain Models for Tracking Entities

Create comprehensive Java models for tracking data.

Core Domain Models:

@Data
public class Tracking {
private String id;
private String trackingNumber;
private String slug; // Carrier code
private String trackingUrl;
private String customerName;
private String orderId;
private String orderNumber;
private String orderDate;
private String destinationCountry;
private String shippingDate;
private String originCountry;
private Map<String, Object> customFields;
// Status information
private String active;
private String tag; // Current status tag
private String expectedDelivery;
private String shipmentType;
private String shipmentWeight;
private String shipmentWeightUnit;
// Timeline
private List<Checkpoint> checkpoints;
private Date createdAt;
private Date updatedAt;
public boolean isDelivered() {
return "Delivered".equals(tag);
}
public boolean hasException() {
return "Exception".equals(tag) || "InfoReceived".equals(tag);
}
}
@Data
public class Checkpoint {
private Date createdAt;
private String checkpointTime;
private String city;
private String country;
private String countryCode;
private String state;
private String zip;
private String message;
private String tag;
private String substag;
private String substagMessage;
public boolean isDeliveryEvent() {
return "Delivered".equals(tag) || "Pickup".equals(tag);
}
public boolean isExceptionEvent() {
return "Exception".equals(tag) || "InfoReceived".equals(tag);
}
}
@Data
public class Courier {
private String slug;
private String name;
private String phone;
private String url;
private String requiredFields;
private Boolean trackable;
private Boolean recommend;
private List<String> supportedCountryCodes;
public boolean supportsCountry(String countryCode) {
return supportedCountryCodes != null && 
supportedCountryCodes.contains(countryCode.toUpperCase());
}
}
@Data
public class TrackingRequest {
private String trackingNumber;
private String slug; // Optional - auto-detect if not provided
private String customerName;
private String orderId;
private String orderNumber;
private String orderDate;
private String destinationCountry;
private String shippingDate;
private String originCountry;
private String expectedDelivery;
private Map<String, Object> customFields;
public boolean hasCarrierSpecified() {
return slug != null && !slug.trim().isEmpty();
}
}
@Data
public class TrackingResponse {
private Meta meta;
private TrackingData data;
@Data
public static class Meta {
private Integer code;
private String message;
private String type;
}
@Data
public static class TrackingData {
private Tracking tracking;
}
}

4. Tracking Management Service

Core service for managing package tracking.

Tracking Service:

@Service
@Slf4j
public class TrackingService {
private final AfterShipClient client;
private final CourierService courierService;
public TrackingService(AfterShipClient client, CourierService courierService) {
this.client = client;
this.courierService = courierService;
}
public Mono<Tracking> createTracking(TrackingRequest request) {
return validateTrackingRequest(request)
.flatMap(validRequest -> {
Map<String, Object> body = createTrackingBody(validRequest);
return client.executePost("/trackings", body, TrackingResponse.class);
})
.map(response -> {
if (response.getMeta().getCode() != 201) {
throw new AfterShipException("Failed to create tracking: " + 
response.getMeta().getMessage());
}
return response.getData().getTracking();
})
.doOnSuccess(tracking -> 
log.info("Tracking created: {} for order {}", 
tracking.getTrackingNumber(), tracking.getOrderId()))
.doOnError(error -> 
log.error("Failed to create tracking for order: {}", 
request.getOrderId(), error));
}
public Mono<Tracking> getTracking(String trackingNumber, String slug) {
String path = String.format("/trackings/%s/%s", slug, trackingNumber);
return client.executeGet(path, null, TrackingResponse.class)
.map(response -> {
if (response.getMeta().getCode() != 200) {
throw new AfterShipException("Tracking not found: " + 
response.getMeta().getMessage());
}
return response.getData().getTracking();
})
.doOnError(error -> 
log.warn("Failed to get tracking: {} - {}", trackingNumber, slug, error));
}
public Mono<Tracking> getTrackingById(String trackingId) {
String path = String.format("/trackings/%s", trackingId);
return client.executeGet(path, null, TrackingResponse.class)
.map(response -> response.getData().getTracking());
}
public Flux<Tracking> getTrackingsByOrder(String orderId) {
Map<String, Object> params = Map.of(
"order_id", orderId,
"limit", 50
);
return client.executeGet("/trackings", params, TrackingListResponse.class)
.flatMapMany(response -> 
Flux.fromIterable(response.getData().getTrackings()));
}
public Flux<Tracking> getActiveTrackings() {
Map<String, Object> params = Map.of(
"tag", "Active",
"limit", 100
);
return client.executeGet("/trackings", params, TrackingListResponse.class)
.flatMapMany(response -> 
Flux.fromIterable(response.getData().getTrackings()));
}
public Mono<Tracking> updateTracking(String trackingId, TrackingRequest updates) {
String path = String.format("/trackings/%s", trackingId);
Map<String, Object> body = createTrackingBody(updates);
return client.executePut(path, body, TrackingResponse.class)
.map(response -> response.getData().getTracking());
}
public Mono<Void> deleteTracking(String trackingId) {
String path = String.format("/trackings/%s", trackingId);
return client.executeDelete(path);
}
public Mono<Tracking> retrack(String trackingId) {
String path = String.format("/trackings/%s/retrack", trackingId);
return client.executePost(path, Map.of(), TrackingResponse.class)
.map(response -> response.getData().getTracking());
}
private Mono<TrackingRequest> validateTrackingRequest(TrackingRequest request) {
return Mono.fromCallable(() -> {
if (request.getTrackingNumber() == null || 
request.getTrackingNumber().trim().isEmpty()) {
throw new AfterShipException("Tracking number is required");
}
// Auto-detect carrier if not specified
if (!request.hasCarrierSpecified()) {
return autoDetectCarrier(request);
}
return request;
});
}
private TrackingRequest autoDetectCarrier(TrackingRequest request) {
// In a real implementation, you might call AfterShip's courier detection
// For now, we'll return the request as-is and let AfterShip handle detection
log.info("Auto-detecting carrier for tracking: {}", request.getTrackingNumber());
return request;
}
private Map<String, Object> createTrackingBody(TrackingRequest request) {
Map<String, Object> body = new HashMap<>();
Map<String, Object> tracking = new HashMap<>();
tracking.put("tracking_number", request.getTrackingNumber());
if (request.hasCarrierSpecified()) {
tracking.put("slug", request.getSlug());
}
// Optional fields
if (request.getCustomerName() != null) {
tracking.put("customer_name", request.getCustomerName());
}
if (request.getOrderId() != null) {
tracking.put("order_id", request.getOrderId());
}
if (request.getOrderNumber() != null) {
tracking.put("order_number", request.getOrderNumber());
}
if (request.getDestinationCountry() != null) {
tracking.put("destination_country_iso3", request.getDestinationCountry());
}
body.put("tracking", tracking);
return body;
}
}

5. Courier Detection and Management

Handle carrier detection and courier information.

Courier Service:

@Service
public class CourierService {
private final AfterShipClient client;
public CourierService(AfterShipClient client) {
this.client = client;
}
public Flux<Courier> getAllCouriers() {
return client.executeGet("/couriers", null, CourierListResponse.class)
.flatMapMany(response -> 
Flux.fromIterable(response.getData().getCouriers()));
}
public Flux<Courier> detectCourier(String trackingNumber, String destinationCountry) {
Map<String, Object> params = new HashMap<>();
params.put("tracking_number", trackingNumber);
if (destinationCountry != null) {
params.put("destination_country", destinationCountry);
}
return client.executeGet("/couriers/detect", params, CourierDetectionResponse.class)
.flatMapMany(response -> 
Flux.fromIterable(response.getData().getCouriers()));
}
public Mono<Courier> getCourierBySlug(String slug) {
return getAllCouriers()
.filter(courier -> slug.equals(courier.getSlug()))
.next()
.switchIfEmpty(Mono.error(new AfterShipException("Courier not found: " + slug)));
}
public Flux<Courier> getCouriersByCountry(String countryCode) {
return getAllCouriers()
.filter(courier -> courier.supportsCountry(countryCode));
}
public Mono<String> detectCourierSlug(String trackingNumber) {
return detectCourier(trackingNumber, null)
.next()
.map(Courier::getSlug)
.switchIfEmpty(Mono.just("unknown"));
}
}

6. Webhook Handling for Real-time Updates

Process AfterShip webhooks for tracking updates.

Webhook Controller:

@RestController
@RequestMapping("/webhooks/aftership")
@Slf4j
public class AfterShipWebhookController {
private final TrackingService trackingService;
private final NotificationService notificationService;
private final OrderService orderService;
@Value("${aftership.webhook.secret}")
private String webhookSecret;
public AfterShipWebhookController(TrackingService trackingService,
NotificationService notificationService,
OrderService orderService) {
this.trackingService = trackingService;
this.notificationService = notificationService;
this.orderService = orderService;
}
@PostMapping("/tracking")
public ResponseEntity<String> handleTrackingWebhook(
@RequestBody String payload,
@RequestHeader(value = "aftership-hmac-sha256", required = false) String signature) {
if (!verifyWebhookSignature(payload, signature)) {
log.warn("Invalid webhook signature received");
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
try {
AfterShipWebhookEvent event = parseWebhookPayload(payload);
processWebhookEvent(event).subscribe();
return ResponseEntity.accepted().build();
} catch (Exception e) {
log.error("Failed to process AfterShip webhook", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}
private Mono<Void> processWebhookEvent(AfterShipWebhookEvent event) {
return Mono.fromRunnable(() -> {
String eventType = event.getEvent();
Tracking tracking = event.getData().getTracking();
String orderId = tracking.getOrderId();
log.info("Processing webhook event: {} for order: {}", eventType, orderId);
switch (eventType) {
case "tracking_created":
handleTrackingCreated(tracking);
break;
case "tracking_updated":
handleTrackingUpdated(tracking);
break;
case "tracking_delivered":
handleTrackingDelivered(tracking);
break;
case "tracking_fulfilled":
handleTrackingFulfilled(tracking);
break;
case "tracking_attempted":
handleTrackingAttempted(tracking);
break;
case "tracking_undelivered":
handleTrackingUndelivered(tracking);
break;
case "tracking_expired":
handleTrackingExpired(tracking);
break;
}
});
}
private void handleTrackingUpdated(Tracking tracking) {
String orderId = tracking.getOrderId();
String status = tracking.getTag();
// Update order status in local system
orderService.updateOrderTrackingStatus(orderId, tracking.getTrackingNumber(), status);
// Send notification to customer
if (shouldNotifyCustomer(tracking)) {
notificationService.sendTrackingUpdate(
orderId, 
tracking.getTrackingNumber(), 
status,
tracking.getCheckpoints()
);
}
// Check for exceptions
if (tracking.hasException()) {
handleTrackingException(tracking);
}
}
private void handleTrackingDelivered(Tracking tracking) {
String orderId = tracking.getOrderId();
log.info("Order delivered: {}", orderId);
orderService.markOrderDelivered(orderId);
notificationService.sendDeliveryConfirmation(orderId, tracking.getTrackingNumber());
// Trigger post-delivery actions
triggerPostDeliveryActions(orderId);
}
private void handleTrackingException(Tracking tracking) {
String orderId = tracking.getOrderId();
Checkpoint latestCheckpoint = getLatestCheckpoint(tracking);
log.warn("Tracking exception for order {}: {}", orderId, 
latestCheckpoint != null ? latestCheckpoint.getMessage() : "Unknown exception");
orderService.flagOrderException(orderId, latestCheckpoint);
notificationService.sendExceptionAlert(orderId, tracking, latestCheckpoint);
}
private boolean shouldNotifyCustomer(Tracking tracking) {
// Only notify for significant status changes
String currentTag = tracking.getTag();
return List.of("InTransit", "OutForDelivery", "AvailableForPickup", "Exception")
.contains(currentTag);
}
private Checkpoint getLatestCheckpoint(Tracking tracking) {
if (tracking.getCheckpoints() == null || tracking.getCheckpoints().isEmpty()) {
return null;
}
return tracking.getCheckpoints().get(tracking.getCheckpoints().size() - 1);
}
private void triggerPostDeliveryActions(String orderId) {
// Trigger review requests, feedback surveys, etc.
log.info("Triggering post-delivery actions for order: {}", orderId);
}
private boolean verifyWebhookSignature(String payload, String signature) {
if (webhookSecret == null || webhookSecret.isEmpty()) {
log.warn("Webhook secret not configured, skipping signature verification");
return true;
}
try {
Mac sha256Hmac = Mac.getInstance("HmacSHA256");
SecretKeySpec secretKey = new SecretKeySpec(webhookSecret.getBytes(), "HmacSHA256");
sha256Hmac.init(secretKey);
String computedSignature = Hex.encodeHexString(
sha256Hmac.doFinal(payload.getBytes()));
return computedSignature.equals(signature);
} catch (Exception e) {
log.error("Webhook signature verification failed", e);
return false;
}
}
}

7. Notification and Alert Service

Handle customer notifications and exception alerts.

Notification Service:

@Service
public class NotificationService {
public Mono<Void> sendTrackingUpdate(String orderId, String trackingNumber, 
String status, List<Checkpoint> checkpoints) {
return Mono.fromRunnable(() -> {
// Implement email/SMS/push notification logic
log.info("Sending tracking update for order {}: {} - {}", 
orderId, trackingNumber, status);
// Example: Send email notification
String subject = String.format("Order %s Update", orderId);
String message = buildTrackingMessage(orderId, trackingNumber, status, checkpoints);
// emailService.send(order.getCustomerEmail(), subject, message);
});
}
public Mono<Void> sendDeliveryConfirmation(String orderId, String trackingNumber) {
return Mono.fromRunnable(() -> {
log.info("Sending delivery confirmation for order {}: {}", orderId, trackingNumber);
String subject = String.format("Order %s Delivered", orderId);
String message = String.format(
"Your order %s has been delivered. Tracking: %s", 
orderId, trackingNumber);
// emailService.send(order.getCustomerEmail(), subject, message);
});
}
public Mono<Void> sendExceptionAlert(String orderId, Tracking tracking, 
Checkpoint exceptionCheckpoint) {
return Mono.fromRunnable(() -> {
log.warn("Sending exception alert for order {}: {}", orderId, 
exceptionCheckpoint.getMessage());
// Notify customer service team
String alertMessage = String.format(
"Tracking exception for order %s: %s - %s", 
orderId, tracking.getTrackingNumber(), exceptionCheckpoint.getMessage());
// alertService.notifyCustomerService(alertMessage);
});
}
private String buildTrackingMessage(String orderId, String trackingNumber, 
String status, List<Checkpoint> checkpoints) {
StringBuilder message = new StringBuilder();
message.append(String.format("Order %s Tracking Update\n\n", orderId));
message.append(String.format("Tracking Number: %s\n", trackingNumber));
message.append(String.format("Current Status: %s\n\n", status));
if (checkpoints != null && !checkpoints.isEmpty()) {
message.append("Recent Updates:\n");
checkpoints.stream()
.sorted((c1, c2) -> c2.getCreatedAt().compareTo(c1.getCreatedAt()))
.limit(3)
.forEach(checkpoint -> {
message.append(String.format("- %s: %s\n", 
checkpoint.getCheckpointTime(), 
checkpoint.getMessage()));
});
}
return message.toString();
}
}

8. Batch Tracking Operations

Handle high-volume tracking operations.

Batch Tracking Service:

@Service
public class BatchTrackingService {
private final TrackingService trackingService;
private final CourierService courierService;
public BatchTrackingService(TrackingService trackingService, 
CourierService courierService) {
this.trackingService = trackingService;
this.courierService = courierService;
}
@Async
public CompletableFuture<List<BatchResult>> createBatchTrackings(
List<TrackingRequest> requests) {
List<CompletableFuture<BatchResult>> futures = requests.stream()
.map(request -> CompletableFuture.supplyAsync(() -> 
processSingleTracking(request)))
.collect(Collectors.toList());
return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]))
.thenApply(v -> futures.stream()
.map(CompletableFuture::join)
.collect(Collectors.toList()));
}
public Flux<Tracking> syncActiveTrackings() {
return trackingService.getActiveTrackings()
.flatMap(tracking -> trackingService.retrack(tracking.getId()))
.doOnNext(tracking -> 
log.info("Synced tracking: {}", tracking.getTrackingNumber()))
.onErrorContinue((error, tracking) -> 
log.warn("Failed to sync tracking: {}", error.getMessage()));
}
private BatchResult processSingleTracking(TrackingRequest request) {
try {
Tracking tracking = trackingService.createTracking(request).block();
return BatchResult.success(request.getOrderId(), tracking);
} catch (Exception e) {
log.error("Batch tracking failed for order: {}", request.getOrderId(), e);
return BatchResult.failed(request.getOrderId(), e.getMessage());
}
}
@Data
public static class BatchResult {
private String orderId;
private boolean success;
private Tracking tracking;
private String errorMessage;
public static BatchResult success(String orderId, Tracking tracking) {
BatchResult result = new BatchResult();
result.setOrderId(orderId);
result.setSuccess(true);
result.setTracking(tracking);
return result;
}
public static BatchResult failed(String orderId, String errorMessage) {
BatchResult result = new BatchResult();
result.setOrderId(orderId);
result.setSuccess(false);
result.setErrorMessage(errorMessage);
return result;
}
}
}

Best Practices for AfterShip Integration

  1. Webhook Security: Always verify webhook signatures to prevent spoofing
  2. Error Handling: Implement comprehensive error handling for API failures
  3. Rate Limiting: Respect AfterShip API rate limits with proper backoff strategies
  4. Data Validation: Validate tracking numbers and carrier codes before creating trackings
  5. Caching: Cache frequently accessed tracking data and courier information
  6. Monitoring: Monitor webhook delivery and tracking sync status
  7. Retry Logic: Implement retry mechanisms for failed tracking updates

Conclusion: Enterprise-Grade Package Visibility

AfterShip provides Java developers with a powerful platform for unifying package tracking across hundreds of carriers worldwide. By implementing comprehensive tracking management, real-time webhook processing, and proactive notification systems, businesses can deliver exceptional post-purchase experiences while maintaining operational visibility.

This integration demonstrates that modern package tracking doesn't require complex carrier-specific implementations—AfterShip's unified API combined with Java's reactive capabilities creates a robust foundation for scalable logistics visibility, enabling businesses to provide transparent shipping experiences while reducing customer service overhead.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper