EasyPost Shipping API Integration in Java: Complete Guide

EasyPost provides a simple REST API for shipping integration with carriers like USPS, UPS, FedEx, and more. This guide covers complete integration for shipping rates, label generation, tracking, and more.


1. Project Setup and Dependencies

Maven Dependencies (pom.xml)

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>easypost-shipping</artifactId>
<version>1.0.0</version>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<spring.boot.version>2.7.14</spring.boot.version>
<jackson.version>2.15.2</jackson.version>
<okhttp.version>4.11.0</okhttp.version>
<bouncycastle.version>1.70</bouncycastle.version>
</properties>
<dependencies>
<!-- Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>${spring.boot.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
<version>${spring.boot.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
<version>${spring.boot.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
<version>${spring.boot.version}</version>
</dependency>
<!-- HTTP Client -->
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>${okhttp.version}</version>
</dependency>
<!-- JSON Processing -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>${jackson.version}</version>
</dependency>
<!-- Database -->
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- Cache -->
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>3.1.6</version>
</dependency>
<!-- PDF Generation -->
<dependency>
<groupId>org.apache.pdfbox</groupId>
<artifactId>pdfbox</artifactId>
<version>2.0.29</version>
</dependency>
<!-- Barcode Generation -->
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId>
<version>${bouncycastle.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring.boot.version}</version>
</plugin>
</plugins>
</build>
</project>

2. Configuration Classes

Application Properties (application.yml)

# EasyPost Configuration
easypost:
api:
base-url: ${EASYPOST_BASE_URL:https://api.easypost.com/v2}
api-key: ${EASYPOST_API_KEY:}
timeout: 30000
max-retries: 3
webhook-secret: ${EASYPOST_WEBHOOK_SECRET:}
test-mode: ${EASYPOST_TEST_MODE:true}
# Shipping Configuration
shipping:
default:
carrier: USPS
service: Priority
packaging: Package
options:
insurance-threshold: 100.00
signature-confirmation-threshold: 500.00
carbon-neutral: true
labels:
format: PNG
size: 4x6
# Server Configuration
server:
port: 8080
# Database
spring:
datasource:
url: jdbc:postgresql://localhost:5432/shipping_db
username: shipping_user
password: shipping_pass
driver-class-name: org.postgresql.Driver
jpa:
hibernate:
ddl-auto: update
properties:
hibernate:
dialect: org.hibernate.dialect.PostgreSQLDialect
format_sql: true
show-sql: true
# Logging
logging:
level:
com.example.easypost: DEBUG

EasyPost Configuration Class

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Data
@Component
@ConfigurationProperties(prefix = "easypost.api")
public class EasyPostConfig {
private String baseUrl = "https://api.easypost.com/v2";
private String apiKey;
private int timeout = 30000;
private int maxRetries = 3;
private String webhookSecret;
private boolean testMode = true;
}
@Data
@Component
@ConfigurationProperties(prefix = "shipping")
public class ShippingConfig {
private DefaultSettings defaultSettings = new DefaultSettings();
private Options options = new Options();
private Labels labels = new Labels();
@Data
public static class DefaultSettings {
private String carrier = "USPS";
private String service = "Priority";
private String packaging = "Package";
}
@Data
public static class Options {
private Double insuranceThreshold = 100.00;
private Double signatureConfirmationThreshold = 500.00;
private Boolean carbonNeutral = true;
}
@Data
public static class Labels {
private String format = "PNG";
private String size = "4x6";
}
}

3. Database Entities

Shipment Entity

import lombok.Data;
import javax.persistence.*;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
@Entity
@Table(name = "shipments")
@Data
public class Shipment {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "easypost_shipment_id", unique = true)
private String easypostShipmentId;
@Column(name = "reference_number")
private String referenceNumber;
@Column(name = "order_number")
private String orderNumber;
@Column(name = "status")
@Enumerated(EnumType.STRING)
private ShipmentStatus status;
@Embedded
private Address fromAddress;
@Embedded
private Address toAddress;
@Embedded
private Address returnAddress;
@Embedded
private Parcel parcel;
@Column(name = "carrier")
private String carrier;
@Column(name = "service")
private String service;
@Column(name = "rate_amount", precision = 10, scale = 2)
private BigDecimal rateAmount;
@Column(name = "rate_currency")
private String rateCurrency = "USD";
@Column(name = "insurance_amount", precision = 10, scale = 2)
private BigDecimal insuranceAmount = BigDecimal.ZERO;
@Column(name = "tracking_code")
private String trackingCode;
@Column(name = "tracking_url")
private String trackingUrl;
@Column(name = "label_url")
private String labelUrl;
@Column(name = "label_pdf_url")
private String labelPdfUrl;
@Column(name = "label_data") // Store label image as base64
@Lob
private String labelData;
@Column(name = "commercial_invoice_url")
private String commercialInvoiceUrl;
@Column(name = "messages")
@Lob
private String messages; // JSON array of carrier messages
@Column(name = "created_at")
private LocalDateTime createdAt;
@Column(name = "updated_at")
private LocalDateTime updatedAt;
@OneToMany(mappedBy = "shipment", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private List<ShipmentEvent> events = new ArrayList<>();
@PrePersist
protected void onCreate() {
createdAt = LocalDateTime.now();
updatedAt = LocalDateTime.now();
}
@PreUpdate
protected void onUpdate() {
updatedAt = LocalDateTime.now();
}
public enum ShipmentStatus {
DRAFT, RATES_FETCHED, PURCHASED, TRANSIT, DELIVERED, 
RETURNED, FAILURE, CANCELLED, UNKNOWN
}
}
@Embeddable
@Data
public class Address {
@Column(name = "name")
private String name;
@Column(name = "company")
private String company;
@Column(name = "street1")
private String street1;
@Column(name = "street2")
private String street2;
@Column(name = "city")
private String city;
@Column(name = "state")
private String state;
@Column(name = "zip")
private String zip;
@Column(name = "country")
private String country;
@Column(name = "phone")
private String phone;
@Column(name = "email")
private String email;
@Column(name = "residential")
private Boolean residential = false;
}
@Embeddable
@Data
public class Parcel {
@Column(name = "length")
private Double length;
@Column(name = "width")
private Double width;
@Column(name = "height")
private Double height;
@Column(name = "weight")
private Double weight;
@Column(name = "predefined_package")
private String predefinedPackage;
@Column(name = "weight_unit")
private String weightUnit = "oz";
@Column(name = "dimension_unit")
private String dimensionUnit = "in";
}

Shipment Event Entity

import lombok.Data;
import javax.persistence.*;
import java.time.LocalDateTime;
@Entity
@Table(name = "shipment_events")
@Data
public class ShipmentEvent {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "shipment_id", nullable = false)
private Shipment shipment;
@Column(name = "easypost_event_id")
private String easypostEventId;
@Column(name = "event_type")
private String eventType;
@Column(name = "description")
private String description;
@Column(name = "status")
private String status;
@Column(name = "carrier")
private String carrier;
@Column(name = "tracking_code")
private String trackingCode;
@Column(name = "event_date")
private LocalDateTime eventDate;
@Column(name = "estimated_delivery_date")
private LocalDateTime estimatedDeliveryDate;
@Column(name = "city")
private String city;
@Column(name = "state")
private String state;
@Column(name = "zip")
private String zip;
@Column(name = "country")
private String country;
@Column(name = "latitude")
private Double latitude;
@Column(name = "longitude")
private Double longitude;
@Column(name = "created_at")
private LocalDateTime createdAt;
@PrePersist
protected void onCreate() {
createdAt = LocalDateTime.now();
}
}

Carrier Account Entity

import lombok.Data;
import javax.persistence.*;
import java.time.LocalDateTime;
@Entity
@Table(name = "carrier_accounts")
@Data
public class CarrierAccount {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "easypost_account_id", unique = true)
private String easypostAccountId;
@Column(name = "carrier")
private String carrier;
@Column(name = "carrier_name")
private String carrierName;
@Column(name = "account_type")
private String accountType;
@Column(name = "description")
private String description;
@Column(name = "billing_type")
private String billingType;
@Column(name = "credentials", length = 2000) // Store as JSON
private String credentials;
@Column(name = "test_credentials", length = 2000)
private String testCredentials;
@Column(name = "is_active")
private Boolean isActive = true;
@Column(name = "is_production")
private Boolean isProduction = false;
@Column(name = "auto_connect")
private Boolean autoConnect = false;
@Column(name = "created_at")
private LocalDateTime createdAt;
@Column(name = "updated_at")
private LocalDateTime updatedAt;
@PrePersist
protected void onCreate() {
createdAt = LocalDateTime.now();
updatedAt = LocalDateTime.now();
}
@PreUpdate
protected void onUpdate() {
updatedAt = LocalDateTime.now();
}
}

4. Data Transfer Objects (DTOs)

Shipment Request DTOs

import lombok.Data;
import javax.validation.constraints.*;
import java.math.BigDecimal;
import java.util.List;
@Data
public class ShipmentRequestDTO {
@NotBlank(message = "Reference number is required")
private String referenceNumber;
private String orderNumber;
@NotNull(message = "From address is required")
private AddressDTO fromAddress;
@NotNull(message = "To address is required")
private AddressDTO toAddress;
private AddressDTO returnAddress;
@NotNull(message = "Parcel is required")
private ParcelDTO parcel;
private List<String> carriers;
private List<String> services;
private InsuranceOptionsDTO insurance;
private CustomsInfoDTO customsInfo;
private OptionsDTO options;
@Data
public static class AddressDTO {
@NotBlank(message = "Name is required")
private String name;
private String company;
@NotBlank(message = "Street1 is required")
private String street1;
private String street2;
@NotBlank(message = "City is required")
private String city;
@NotBlank(message = "State is required")
private String state;
@NotBlank(message = "ZIP code is required")
private String zip;
@NotBlank(message = "Country is required")
private String country = "US";
private String phone;
private String email;
private Boolean residential = false;
}
@Data
public static class ParcelDTO {
private Double length;
private Double width;
private Double height;
@NotNull(message = "Weight is required")
@Positive(message = "Weight must be positive")
private Double weight;
private String predefinedPackage;
private String weightUnit = "oz";
private String dimensionUnit = "in";
}
@Data
public static class InsuranceOptionsDTO {
private BigDecimal amount;
private String provider;
private String currency = "USD";
}
@Data
public static class CustomsInfoDTO {
private String contentsExplanation;
private String contentsType;
private Boolean customsCertify;
private String customsSigner;
private String restrictionType;
private String restrictionComments;
private String nonDeliveryOption;
private List<CustomsItemDTO> customsItems;
}
@Data
public static class CustomsItemDTO {
private String description;
private Integer quantity;
private BigDecimal value;
private Double weight;
private String code;
private String originCountry;
private String currency = "USD";
}
@Data
public static class OptionsDTO {
private Boolean carbonNeutral;
private Boolean deliveryConfirmation;
private Boolean signatureConfirmation;
private Boolean certifiedMail;
private Boolean registeredMail;
private Boolean returnReceipt;
private String labelFormat = "PNG";
private String invoiceNumber;
private Boolean isReturn = false;
}
}
@Data
public class RateRequestDTO {
@NotBlank(message = "Shipment ID is required")
private String shipmentId;
private List<String> carriers;
private List<String> services;
}
@Data
public class PurchaseShipmentDTO {
@NotBlank(message = "Shipment ID is required")
private String shipmentId;
@NotBlank(message = "Rate ID is required")
private String rateId;
private String insuranceAmount;
private String endShipperId;
}

Tracking and Label DTOs

import lombok.Data;
import java.time.LocalDateTime;
import java.util.List;
@Data
public class ShipmentResponseDTO {
private String shipmentId;
private String referenceNumber;
private String orderNumber;
private String status;
private AddressDTO fromAddress;
private AddressDTO toAddress;
private ParcelDTO parcel;
private List<RateDTO> rates;
private String selectedRateId;
private String trackingCode;
private String trackingUrl;
private String labelUrl;
private String labelPdfUrl;
private List<String> messages;
private LocalDateTime createdAt;
@Data
public static class RateDTO {
private String rateId;
private String carrier;
private String service;
private Double rate;
private String currency;
private String deliveryDays;
private LocalDateTime deliveryDate;
private Boolean deliveryDateGuaranteed;
private String estDeliveryDays;
}
}
@Data
public class TrackingResponseDTO {
private String trackingCode;
private String status;
private String carrier;
private List<TrackingEventDTO> events;
private LocalDateTime estimatedDeliveryDate;
private Double weight;
private String trackingUrl;
@Data
public static class TrackingEventDTO {
private String description;
private String status;
private LocalDateTime eventDate;
private String city;
private String state;
private String zip;
private String country;
private Double latitude;
private Double longitude;
}
}
@Data
public class LabelRequestDTO {
private String fileFormat = "PNG";
private String labelSize = "4x6";
}

5. EasyPost API Client Service

Main EasyPost Service

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import okhttp3.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
@Slf4j
@Service
public class EasyPostApiClient {
private final EasyPostConfig config;
private final OkHttpClient httpClient;
private final ObjectMapper objectMapper;
@Autowired
public EasyPostApiClient(EasyPostConfig config, ObjectMapper objectMapper) {
this.config = config;
this.objectMapper = objectMapper;
this.httpClient = new OkHttpClient.Builder()
.connectTimeout(config.getTimeout(), TimeUnit.MILLISECONDS)
.readTimeout(config.getTimeout(), TimeUnit.MILLISECONDS)
.writeTimeout(config.getTimeout(), TimeUnit.MILLISECONDS)
.addInterceptor(new AuthInterceptor(config.getApiKey()))
.addInterceptor(new RetryInterceptor(config.getMaxRetries(), 1000))
.build();
}
/**
* Create a shipment
*/
public Map<String, Object> createShipment(Map<String, Object> shipmentRequest) throws EasyPostException {
try {
String jsonBody = objectMapper.writeValueAsString(shipmentRequest);
RequestBody body = RequestBody.create(jsonBody, MediaType.parse("application/json"));
Request request = new Request.Builder()
.url(config.getBaseUrl() + "/shipments")
.post(body)
.build();
String responseBody = executeRequest(request);
return objectMapper.readValue(responseBody, new TypeReference<Map<String, Object>>() {});
} catch (IOException e) {
log.error("Error creating shipment: {}", e.getMessage(), e);
throw new EasyPostException("Failed to create shipment", e);
}
}
/**
* Get shipment by ID
*/
@Cacheable(value = "shipment", key = "#shipmentId")
public Map<String, Object> getShipment(String shipmentId) throws EasyPostException {
try {
Request request = new Request.Builder()
.url(config.getBaseUrl() + "/shipments/" + shipmentId)
.get()
.build();
String responseBody = executeRequest(request);
return objectMapper.readValue(responseBody, new TypeReference<Map<String, Object>>() {});
} catch (IOException e) {
log.error("Error getting shipment: {}", e.getMessage(), e);
throw new EasyPostException("Failed to get shipment", e);
}
}
/**
* Purchase a shipment
*/
public Map<String, Object> purchaseShipment(String shipmentId, Map<String, Object> purchaseRequest) throws EasyPostException {
try {
String jsonBody = objectMapper.writeValueAsString(purchaseRequest);
RequestBody body = RequestBody.create(jsonBody, MediaType.parse("application/json"));
Request request = new Request.Builder()
.url(config.getBaseUrl() + "/shipments/" + shipmentId + "/buy")
.post(body)
.build();
String responseBody = executeRequest(request);
return objectMapper.readValue(responseBody, new TypeReference<Map<String, Object>>() {});
} catch (IOException e) {
log.error("Error purchasing shipment: {}", e.getMessage(), e);
throw new EasyPostException("Failed to purchase shipment", e);
}
}
/**
* Get shipment rates
*/
public Map<String, Object> getShipmentRates(String shipmentId) throws EasyPostException {
try {
Request request = new Request.Builder()
.url(config.getBaseUrl() + "/shipments/" + shipmentId + "/rates")
.get()
.build();
String responseBody = executeRequest(request);
return objectMapper.readValue(responseBody, new TypeReference<Map<String, Object>>() {});
} catch (IOException e) {
log.error("Error getting shipment rates: {}", e.getMessage(), e);
throw new EasyPostException("Failed to get shipment rates", e);
}
}
/**
* Create a tracker
*/
public Map<String, Object> createTracker(String trackingCode, String carrier) throws EasyPostException {
try {
Map<String, Object> trackerRequest = new HashMap<>();
trackerRequest.put("tracking_code", trackingCode);
trackerRequest.put("carrier", carrier);
String jsonBody = objectMapper.writeValueAsString(trackerRequest);
RequestBody body = RequestBody.create(jsonBody, MediaType.parse("application/json"));
Request request = new Request.Builder()
.url(config.getBaseUrl() + "/trackers")
.post(body)
.build();
String responseBody = executeRequest(request);
return objectMapper.readValue(responseBody, new TypeReference<Map<String, Object>>() {});
} catch (IOException e) {
log.error("Error creating tracker: {}", e.getMessage(), e);
throw new EasyPostException("Failed to create tracker", e);
}
}
/**
* Get tracker by ID
*/
@Cacheable(value = "tracker", key = "#trackerId")
public Map<String, Object> getTracker(String trackerId) throws EasyPostException {
try {
Request request = new Request.Builder()
.url(config.getBaseUrl() + "/trackers/" + trackerId)
.get()
.build();
String responseBody = executeRequest(request);
return objectMapper.readValue(responseBody, new TypeReference<Map<String, Object>>() {});
} catch (IOException e) {
log.error("Error getting tracker: {}", e.getMessage(), e);
throw new EasyPostException("Failed to get tracker", e);
}
}
/**
* Get tracker by tracking code
*/
public Map<String, Object> getTrackerByCode(String trackingCode, String carrier) throws EasyPostException {
try {
HttpUrl url = HttpUrl.parse(config.getBaseUrl() + "/trackers").newBuilder()
.addQueryParameter("tracking_code", trackingCode)
.addQueryParameter("carrier", carrier)
.build();
Request request = new Request.Builder()
.url(url)
.get()
.build();
String responseBody = executeRequest(request);
return objectMapper.readValue(responseBody, new TypeReference<Map<String, Object>>() {});
} catch (IOException e) {
log.error("Error getting tracker by code: {}", e.getMessage(), e);
throw new EasyPostException("Failed to get tracker by code", e);
}
}
/**
* Create an address
*/
public Map<String, Object> createAddress(Map<String, Object> addressRequest) throws EasyPostException {
try {
String jsonBody = objectMapper.writeValueAsString(addressRequest);
RequestBody body = RequestBody.create(jsonBody, MediaType.parse("application/json"));
Request request = new Request.Builder()
.url(config.getBaseUrl() + "/addresses")
.post(body)
.build();
String responseBody = executeRequest(request);
return objectMapper.readValue(responseBody, new TypeReference<Map<String, Object>>() {});
} catch (IOException e) {
log.error("Error creating address: {}", e.getMessage(), e);
throw new EasyPostException("Failed to create address", e);
}
}
/**
* Verify an address
*/
public Map<String, Object> verifyAddress(String addressId) throws EasyPostException {
try {
Request request = new Request.Builder()
.url(config.getBaseUrl() + "/addresses/" + addressId + "/verify")
.get()
.build();
String responseBody = executeRequest(request);
return objectMapper.readValue(responseBody, new TypeReference<Map<String, Object>>() {});
} catch (IOException e) {
log.error("Error verifying address: {}", e.getMessage(), e);
throw new EasyPostException("Failed to verify address", e);
}
}
/**
* Create a parcel
*/
public Map<String, Object> createParcel(Map<String, Object> parcelRequest) throws EasyPostException {
try {
String jsonBody = objectMapper.writeValueAsString(parcelRequest);
RequestBody body = RequestBody.create(jsonBody, MediaType.parse("application/json"));
Request request = new Request.Builder()
.url(config.getBaseUrl() + "/parcels")
.post(body)
.build();
String responseBody = executeRequest(request);
return objectMapper.readValue(responseBody, new TypeReference<Map<String, Object>>() {});
} catch (IOException e) {
log.error("Error creating parcel: {}", e.getMessage(), e);
throw new EasyPostException("Failed to create parcel", e);
}
}
/**
* Get insurance
*/
public Map<String, Object> createInsurance(Map<String, Object> insuranceRequest) throws EasyPostException {
try {
String jsonBody = objectMapper.writeValueAsString(insuranceRequest);
RequestBody body = RequestBody.create(jsonBody, MediaType.parse("application/json"));
Request request = new Request.Builder()
.url(config.getBaseUrl() + "/insurances")
.post(body)
.build();
String responseBody = executeRequest(request);
return objectMapper.readValue(responseBody, new TypeReference<Map<String, Object>>() {});
} catch (IOException e) {
log.error("Error creating insurance: {}", e.getMessage(), e);
throw new EasyPostException("Failed to create insurance", e);
}
}
/**
* Get carrier accounts
*/
@Cacheable(value = "carrierAccounts")
public Map<String, Object> getCarrierAccounts() throws EasyPostException {
try {
Request request = new Request.Builder()
.url(config.getBaseUrl() + "/carrier_accounts")
.get()
.build();
String responseBody = executeRequest(request);
return objectMapper.readValue(responseBody, new TypeReference<Map<String, Object>>() {});
} catch (IOException e) {
log.error("Error getting carrier accounts: {}", e.getMessage(), e);
throw new EasyPostException("Failed to get carrier accounts", e);
}
}
/**
* Download label as base64
*/
public String downloadLabel(String labelUrl) throws EasyPostException {
try {
Request request = new Request.Builder()
.url(labelUrl)
.get()
.build();
try (Response response = httpClient.newCall(request).execute()) {
if (!response.isSuccessful()) {
throw new EasyPostException("Failed to download label: " + response.code());
}
byte[] labelBytes = response.body().bytes();
return Base64.getEncoder().encodeToString(labelBytes);
}
} catch (IOException e) {
log.error("Error downloading label: {}", e.getMessage(), e);
throw new EasyPostException("Failed to download label", e);
}
}
private String executeRequest(Request request) throws IOException, EasyPostException {
try (Response response = httpClient.newCall(request).execute()) {
if (!response.isSuccessful()) {
String errorBody = response.body().string();
log.error("EasyPost API error: {} - {}", response.code(), errorBody);
throw new EasyPostException("EasyPost API error: " + response.code() + " - " + errorBody);
}
return response.body().string();
}
}
}

Authentication Interceptor

import okhttp3.*;
import org.jetbrains.annotations.NotNull;
import java.io.IOException;
import java.util.Base64;
public class AuthInterceptor implements Interceptor {
private final String apiKey;
public AuthInterceptor(String apiKey) {
this.apiKey = apiKey;
}
@NotNull
@Override
public Response intercept(@NotNull Chain chain) throws IOException {
Request originalRequest = chain.request();
String credentials = Base64.getEncoder().encodeToString((apiKey + ":").getBytes());
Request authenticatedRequest = originalRequest.newBuilder()
.header("Authorization", "Basic " + credentials)
.build();
return chain.proceed(authenticatedRequest);
}
}

Retry Interceptor

import okhttp3.*;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
public class RetryInterceptor implements Interceptor {
private static final Logger logger = LoggerFactory.getLogger(RetryInterceptor.class);
private final int maxRetries;
private final long backoffDelay;
public RetryInterceptor(int maxRetries, long backoffDelay) {
this.maxRetries = maxRetries;
this.backoffDelay = backoffDelay;
}
@NotNull
@Override
public Response intercept(@NotNull Chain chain) throws IOException {
Request request = chain.request();
Response response = null;
IOException exception = null;
for (int attempt = 1; attempt <= maxRetries; attempt++) {
try {
response = chain.proceed(request);
if (response.isSuccessful()) {
return response;
}
// Only retry on server errors (5xx) or rate limiting
if (response.code() < 500 && response.code() != 429) {
return response;
}
logger.warn("EasyPost API request failed with status {} on attempt {}/{}", 
response.code(), attempt, maxRetries);
} catch (IOException e) {
exception = e;
logger.warn("EasyPost API request failed with IOException on attempt {}/{}: {}", 
attempt, maxRetries, e.getMessage());
}
if (attempt < maxRetries) {
try {
long delay = backoffDelay * (long) Math.pow(2, attempt - 1);
TimeUnit.MILLISECONDS.sleep(delay);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new IOException("Request interrupted", e);
}
}
if (response != null) {
response.close();
}
}
if (exception != null) {
throw exception;
}
return response;
}
}

6. Shipping Service

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.*;
@Slf4j
@Service
@Transactional
public class ShippingService {
@Autowired
private EasyPostApiClient easyPostClient;
@Autowired
private ShipmentRepository shipmentRepository;
@Autowired
private ShipmentEventRepository eventRepository;
@Autowired
private ShippingConfig shippingConfig;
@Autowired
private ObjectMapper objectMapper;
/**
* Create a shipment with rates
*/
public Shipment createShipment(ShipmentRequestDTO shipmentRequest) throws EasyPostException {
try {
// Prepare EasyPost shipment request
Map<String, Object> easypostRequest = prepareShipmentRequest(shipmentRequest);
// Create shipment in EasyPost
Map<String, Object> easypostResponse = easyPostClient.createShipment(easypostRequest);
// Save shipment to database
Shipment shipment = saveShipmentToDatabase(shipmentRequest, easypostResponse);
log.info("Shipment created successfully: {}", shipment.getEasypostShipmentId());
return shipment;
} catch (Exception e) {
log.error("Error creating shipment: {}", e.getMessage(), e);
throw new EasyPostException("Failed to create shipment: " + e.getMessage(), e);
}
}
/**
* Purchase a shipment
*/
public Shipment purchaseShipment(String shipmentId, PurchaseShipmentDTO purchaseRequest) throws EasyPostException {
try {
// Purchase shipment in EasyPost
Map<String, Object> purchaseData = new HashMap<>();
purchaseData.put("rate", Map.of("id", purchaseRequest.getRateId()));
if (purchaseRequest.getInsuranceAmount() != null) {
purchaseData.put("insurance", purchaseRequest.getInsuranceAmount());
}
Map<String, Object> easypostResponse = easyPostClient.purchaseShipment(shipmentId, purchaseData);
// Update shipment in database
Shipment shipment = updateShipmentAfterPurchase(shipmentId, easypostResponse);
// Download and store label
downloadAndStoreLabel(shipment);
log.info("Shipment purchased successfully: {}", shipmentId);
return shipment;
} catch (Exception e) {
log.error("Error purchasing shipment: {}", e.getMessage(), e);
throw new EasyPostException("Failed to purchase shipment: " + e.getMessage(), e);
}
}
/**
* Get shipment rates
*/
public List<Map<String, Object>> getShipmentRates(String shipmentId) throws EasyPostException {
try {
Map<String, Object> ratesResponse = easyPostClient.getShipmentRates(shipmentId);
@SuppressWarnings("unchecked")
List<Map<String, Object>> rates = (List<Map<String, Object>>) ratesResponse.get("rates");
// Filter and sort rates
return rates.stream()
.sorted(Comparator.comparingDouble(rate -> Double.parseDouble(rate.get("rate").toString())))
.toList();
} catch (Exception e) {
log.error("Error getting shipment rates: {}", e.getMessage(), e);
throw new EasyPostException("Failed to get shipment rates: " + e.getMessage(), e);
}
}
/**
* Track a shipment
*/
public TrackingResponseDTO trackShipment(String trackingCode, String carrier) throws EasyPostException {
try {
// Create or get tracker
Map<String, Object> trackerResponse = easyPostClient.createTracker(trackingCode, carrier);
@SuppressWarnings("unchecked")
Map<String, Object> tracker = (Map<String, Object>) trackerResponse.get("tracker");
// Convert to DTO
TrackingResponseDTO trackingResponse = convertToTrackingDTO(tracker);
// Update shipment tracking if exists
updateShipmentTracking(trackingCode, trackingResponse);
return trackingResponse;
} catch (Exception e) {
log.error("Error tracking shipment: {}", e.getMessage(), e);
throw new EasyPostException("Failed to track shipment: " + e.getMessage(), e);
}
}
/**
* Verify an address
*/
public Map<String, Object> verifyAddress(ShipmentRequestDTO.AddressDTO addressDTO) throws EasyPostException {
try {
Map<String, Object> addressRequest = convertAddressToMap(addressDTO);
Map<String, Object> addressResponse = easyPostClient.createAddress(addressRequest);
@SuppressWarnings("unchecked")
Map<String, Object> address = (Map<String, Object>) addressResponse.get("address");
// Verify address
return easyPostClient.verifyAddress((String) address.get("id"));
} catch (Exception e) {
log.error("Error verifying address: {}", e.getMessage(), e);
throw new EasyPostException("Failed to verify address: " + e.getMessage(), e);
}
}
/**
* Get carrier accounts
*/
public List<Map<String, Object>> getCarrierAccounts() throws EasyPostException {
try {
Map<String, Object> accountsResponse = easyPostClient.getCarrierAccounts();
@SuppressWarnings("unchecked")
List<Map<String, Object>> accounts = (List<Map<String, Object>>) accountsResponse.get("carrier_accounts");
return accounts.stream()
.filter(account -> Boolean.TRUE.equals(account.get("readable")))
.toList();
} catch (Exception e) {
log.error("Error getting carrier accounts: {}", e.getMessage(), e);
throw new EasyPostException("Failed to get carrier accounts: " + e.getMessage(), e);
}
}
/**
* Create return shipment
*/
public Shipment createReturnShipment(String originalShipmentId, ShipmentRequestDTO returnRequest) throws EasyPostException {
try {
// Get original shipment
Shipment originalShipment = shipmentRepository.findByEasypostShipmentId(originalShipmentId)
.orElseThrow(() -> new EasyPostException("Original shipment not found"));
// Swap from and to addresses for return
ShipmentRequestDTO returnShipmentRequest = new ShipmentRequestDTO();
returnShipmentRequest.setReferenceNumber(returnRequest.getReferenceNumber() + "-RETURN");
returnShipmentRequest.setFromAddress(originalShipment.getToAddress());
returnShipmentRequest.setToAddress(originalShipment.getFromAddress());
returnShipmentRequest.setParcel(returnRequest.getParcel());
returnShipmentRequest.setOptions(returnRequest.getOptions());
// Set return specific options
if (returnShipmentRequest.getOptions() == null) {
returnShipmentRequest.setOptions(new ShipmentRequestDTO.OptionsDTO());
}
returnShipmentRequest.getOptions().setIsReturn(true);
return createShipment(returnShipmentRequest);
} catch (Exception e) {
log.error("Error creating return shipment: {}", e.getMessage(), e);
throw new EasyPostException("Failed to create return shipment: " + e.getMessage(), e);
}
}
private Map<String, Object> prepareShipmentRequest(ShipmentRequestDTO shipmentRequest) {
Map<String, Object> request = new HashMap<>();
// Addresses
request.put("from_address", convertAddressToMap(shipmentRequest.getFromAddress()));
request.put("to_address", convertAddressToMap(shipmentRequest.getToAddress()));
if (shipmentRequest.getReturnAddress() != null) {
request.put("return_address", convertAddressToMap(shipmentRequest.getReturnAddress()));
}
// Parcel
request.put("parcel", convertParcelToMap(shipmentRequest.getParcel()));
// Options
if (shipmentRequest.getOptions() != null) {
request.put("options", convertOptionsToMap(shipmentRequest.getOptions()));
}
// Customs info
if (shipmentRequest.getCustomsInfo() != null) {
request.put("customs_info", convertCustomsInfoToMap(shipmentRequest.getCustomsInfo()));
}
// Carrier and service filters
if (shipmentRequest.getCarriers() != null && !shipmentRequest.getCarriers().isEmpty()) {
request.put("carrier_accounts", shipmentRequest.getCarriers());
}
if (shipmentRequest.getServices() != null && !shipmentRequest.getServices().isEmpty()) {
request.put("services", shipmentRequest.getServices());
}
// Reference
request.put("reference", shipmentRequest.getReferenceNumber());
return request;
}
private Map<String, Object> convertAddressToMap(ShipmentRequestDTO.AddressDTO 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("residential", address.getResidential());
return addressMap;
}
private Map<String, Object> convertParcelToMap(ShipmentRequestDTO.ParcelDTO parcel) {
Map<String, Object> parcelMap = new HashMap<>();
parcelMap.put("length", parcel.getLength());
parcelMap.put("width", parcel.getWidth());
parcelMap.put("height", parcel.getHeight());
parcelMap.put("weight", parcel.getWeight());
parcelMap.put("predefined_package", parcel.getPredefinedPackage());
return parcelMap;
}
private Map<String, Object> convertOptionsToMap(ShipmentRequestDTO.OptionsDTO options) {
Map<String, Object> optionsMap = new HashMap<>();
optionsMap.put("carbon_neutral", options.getCarbonNeutral());
optionsMap.put("delivery_confirmation", options.getDeliveryConfirmation());
optionsMap.put("signature_confirmation", options.getSignatureConfirmation());
optionsMap.put("label_format", options.getLabelFormat());
optionsMap.put("invoice_number", options.getInvoiceNumber());
optionsMap.put("is_return", options.getIsReturn());
return optionsMap;
}
private Map<String, Object> convertCustomsInfoToMap(ShipmentRequestDTO.CustomsInfoDTO customsInfo) {
Map<String, Object> customsMap = new HashMap<>();
customsMap.put("contents_explanation", customsInfo.getContentsExplanation());
customsMap.put("contents_type", customsInfo.getContentsType());
customsMap.put("customs_certify", customsInfo.getCustomsCertify());
customsMap.put("customs_signer", customsInfo.getCustomsSigner());
if (customsInfo.getCustomsItems() != null) {
List<Map<String, Object>> items = customsInfo.getCustomsItems().stream()
.map(this::convertCustomsItemToMap)
.toList();
customsMap.put("customs_items", items);
}
return customsMap;
}
private Map<String, Object> convertCustomsItemToMap(ShipmentRequestDTO.CustomsItemDTO item) {
Map<String, Object> itemMap = new HashMap<>();
itemMap.put("description", item.getDescription());
itemMap.put("quantity", item.getQuantity());
itemMap.put("value", item.getValue());
itemMap.put("weight", item.getWeight());
itemMap.put("code", item.getCode());
itemMap.put("origin_country", item.getOriginCountry());
itemMap.put("currency", item.getCurrency());
return itemMap;
}
@SuppressWarnings("unchecked")
private Shipment saveShipmentToDatabase(ShipmentRequestDTO request, Map<String, Object> easypostResponse) {
Map<String, Object> shipmentData = (Map<String, Object>) easypostResponse.get("shipment");
Shipment shipment = new Shipment();
shipment.setEasypostShipmentId((String) shipmentData.get("id"));
shipment.setReferenceNumber(request.getReferenceNumber());
shipment.setOrderNumber(request.getOrderNumber());
shipment.setStatus(Shipment.ShipmentStatus.RATES_FETCHED);
// Set addresses
shipment.setFromAddress(convertToAddressEntity((Map<String, Object>) shipmentData.get("from_address")));
shipment.setToAddress(convertToAddressEntity((Map<String, Object>) shipmentData.get("to_address")));
if (shipmentData.get("return_address") != null) {
shipment.setReturnAddress(convertToAddressEntity((Map<String, Object>) shipmentData.get("return_address")));
}
// Set parcel
shipment.setParcel(convertToParcelEntity((Map<String, Object>) shipmentData.get("parcel")));
// Store rates as JSON in messages for now (in real implementation, you might want a separate table)
if (shipmentData.get("rates") != null) {
try {
shipment.setMessages(objectMapper.writeValueAsString(shipmentData.get("rates")));
} catch (Exception e) {
log.warn("Failed to store rates as JSON: {}", e.getMessage());
}
}
return shipmentRepository.save(shipment);
}
private Address convertToAddressEntity(Map<String, Object> addressData) {
Address address = new Address();
address.setName((String) addressData.get("name"));
address.setCompany((String) addressData.get("company"));
address.setStreet1((String) addressData.get("street1"));
address.setStreet2((String) addressData.get("street2"));
address.setCity((String) addressData.get("city"));
address.setState((String) addressData.get("state"));
address.setZip((String) addressData.get("zip"));
address.setCountry((String) addressData.get("country"));
address.setPhone((String) addressData.get("phone"));
address.setEmail((String) addressData.get("email"));
address.setResidential(Boolean.TRUE.equals(addressData.get("residential")));
return address;
}
private Parcel convertToParcelEntity(Map<String, Object> parcelData) {
Parcel parcel = new Parcel();
parcel.setLength(parcelData.get("length") != null ? Double.parseDouble(parcelData.get("length").toString()) : null);
parcel.setWidth(parcelData.get("width") != null ? Double.parseDouble(parcelData.get("width").toString()) : null);
parcel.setHeight(parcelData.get("height") != null ? Double.parseDouble(parcelData.get("height").toString()) : null);
parcel.setWeight(parcelData.get("weight") != null ? Double.parseDouble(parcelData.get("weight").toString()) : null);
parcel.setPredefinedPackage((String) parcelData.get("predefined_package"));
return parcel;
}
@SuppressWarnings("unchecked")
private Shipment updateShipmentAfterPurchase(String shipmentId, Map<String, Object> easypostResponse) {
Map<String, Object> shipmentData = (Map<String, Object>) easypostResponse.get("shipment");
Shipment shipment = shipmentRepository.findByEasypostShipmentId(shipmentId)
.orElseThrow(() -> new EasyPostException("Shipment not found in database"));
Map<String, Object> selectedRate = (Map<String, Object>) shipmentData.get("selected_rate");
Map<String, Object> tracker = (Map<String, Object>) shipmentData.get("tracker");
Map<String, Object> postageLabel = (Map<String, Object>) shipmentData.get("postage_label");
shipment.setStatus(Shipment.ShipmentStatus.PURCHASED);
shipment.setCarrier((String) selectedRate.get("carrier"));
shipment.setService((String) selectedRate.get("service"));
shipment.setRateAmount(new BigDecimal(selectedRate.get("rate").toString()));
shipment.setRateCurrency((String) selectedRate.get("currency"));
shipment.setTrackingCode((String) tracker.get("tracking_code"));
shipment.setTrackingUrl((String) tracker.get("public_url"));
shipment.setLabelUrl((String) postageLabel.get("url"));
shipment.setLabelPdfUrl((String) postageLabel.get("pdf_url"));
return shipmentRepository.save(shipment);
}
private void downloadAndStoreLabel(Shipment shipment) {
try {
String labelData = easyPostClient.downloadLabel(shipment.getLabelUrl());
shipment.setLabelData(labelData);
shipmentRepository.save(shipment);
} catch (Exception e) {
log.warn("Failed to download and store label: {}", e.getMessage());
}
}
@SuppressWarnings("unchecked")
private TrackingResponseDTO convertToTrackingDTO(Map<String, Object> tracker) {
TrackingResponseDTO trackingResponse = new TrackingResponseDTO();
trackingResponse.setTrackingCode((String) tracker.get("tracking_code"));
trackingResponse.setStatus((String) tracker.get("status"));
trackingResponse.setCarrier((String) tracker.get("carrier"));
trackingResponse.setTrackingUrl((String) tracker.get("public_url"));
if (tracker.get("est_delivery_date") != null) {
trackingResponse.setEstimatedDeliveryDate(
LocalDateTime.parse(tracker.get("est_delivery_date").toString().replace("Z", ""))
);
}
if (tracker.get("weight") != null) {
trackingResponse.setWeight(Double.parseDouble(tracker.get("weight").toString()));
}
// Convert tracking events
if (tracker.get("tracking_details") != null) {
List<Map<String, Object>> trackingDetails = (List<Map<String, Object>>) tracker.get("tracking_details");
List<TrackingResponseDTO.TrackingEventDTO> events = trackingDetails.stream()
.map(this::convertToTrackingEventDTO)
.toList();
trackingResponse.setEvents(events);
}
return trackingResponse;
}
private TrackingResponseDTO.TrackingEventDTO convertToTrackingEventDTO(Map<String, Object> eventData) {
TrackingResponseDTO.TrackingEventDTO event = new TrackingResponseDTO.TrackingEventDTO();
event.setDescription((String) eventData.get("message"));
event.setStatus((String) eventData.get("status"));
if (eventData.get("datetime") != null) {
event.setEventDate(LocalDateTime.parse(eventData.get("datetime").toString().replace("Z", "")));
}
event.setCity((String) eventData.get("tracking_location").get("city"));
event.setState((String) eventData.get("tracking_location").get("state"));
event.setZip((String) eventData.get("tracking_location").get("zip"));
event.setCountry((String) eventData.get("tracking_location").get("country"));
return event;
}
private void updateShipmentTracking(String trackingCode, TrackingResponseDTO trackingResponse) {
try {
Optional<Shipment> shipmentOpt = shipmentRepository.findByTrackingCode(trackingCode);
if (shipmentOpt.isPresent()) {
Shipment shipment = shipmentOpt.get();
// Update shipment status based on tracking
switch (trackingResponse.getStatus().toUpperCase()) {
case "DELIVERED":
shipment.setStatus(Shipment.ShipmentStatus.DELIVERED);
break;
case "IN_TRANSIT":
shipment.setStatus(Shipment.ShipmentStatus.TRANSIT);
break;
case "FAILURE":
shipment.setStatus(Shipment.ShipmentStatus.FAILURE);
break;
case "RETURNED":
shipment.setStatus(Shipment.ShipmentStatus.RETURNED);
break;
}
shipmentRepository.save(shipment);
// Create tracking events
for (TrackingResponseDTO.TrackingEventDTO eventDTO : trackingResponse.getEvents()) {
ShipmentEvent event = new ShipmentEvent();
event.setShipment(shipment);
event.setEventType(eventDTO.getStatus());
event.setDescription(eventDTO.getDescription());
event.setStatus(eventDTO.getStatus());
event.setCarrier(trackingResponse.getCarrier());
event.setTrackingCode(trackingCode);
event.setEventDate(eventDTO.getEventDate());
event.setCity(eventDTO.getCity());
event.setState(eventDTO.getState());
event.setZip(eventDTO.getZip());
event.setCountry(eventDTO.getCountry());
eventRepository.save(event);
}
}
} catch (Exception e) {
log.warn("Failed to update shipment tracking: {}", e.getMessage());
}
}
}

7. REST Controllers

Shipping Controller

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
import java.util.HashMap;
import java.util.Map;
@Slf4j
@RestController
@RequestMapping("/api/shipping")
public class ShippingController {
@Autowired
private ShippingService shippingService;
/**
* Create a shipment
*/
@PostMapping("/shipments")
public ResponseEntity<?> createShipment(@Valid @RequestBody ShipmentRequestDTO shipmentRequest) {
try {
Shipment shipment = shippingService.createShipment(shipmentRequest);
return ResponseEntity.ok(shipment);
} catch (EasyPostException e) {
log.error("Error creating shipment: {}", e.getMessage());
return ResponseEntity.badRequest().body(createErrorResponse(e.getMessage()));
}
}
/**
* Get shipment rates
*/
@GetMapping("/shipments/{shipmentId}/rates")
public ResponseEntity<?> getShipmentRates(@PathVariable String shipmentId) {
try {
var rates = shippingService.getShipmentRates(shipmentId);
return ResponseEntity.ok(rates);
} catch (EasyPostException e) {
log.error("Error getting shipment rates: {}", e.getMessage());
return ResponseEntity.badRequest().body(createErrorResponse(e.getMessage()));
}
}
/**
* Purchase a shipment
*/
@PostMapping("/shipments/{shipmentId}/purchase")
public ResponseEntity<?> purchaseShipment(
@PathVariable String shipmentId,
@Valid @RequestBody PurchaseShipmentDTO purchaseRequest) {
try {
Shipment shipment = shippingService.purchaseShipment(shipmentId, purchaseRequest);
return ResponseEntity.ok(shipment);
} catch (EasyPostException e) {
log.error("Error purchasing shipment: {}", e.getMessage());
return ResponseEntity.badRequest().body(createErrorResponse(e.getMessage()));
}
}
/**
* Track a shipment
*/
@GetMapping("/track")
public ResponseEntity<?> trackShipment(
@RequestParam String trackingCode,
@RequestParam(required = false) String carrier) {
try {
TrackingResponseDTO tracking = shippingService.trackShipment(trackingCode, carrier);
return ResponseEntity.ok(tracking);
} catch (EasyPostException e) {
log.error("Error tracking shipment: {}", e.getMessage());
return ResponseEntity.badRequest().body(createErrorResponse(e.getMessage()));
}
}
/**
* Verify an address
*/
@PostMapping("/addresses/verify")
public ResponseEntity<?> verifyAddress(@Valid @RequestBody ShipmentRequestDTO.AddressDTO address) {
try {
var verificationResult = shippingService.verifyAddress(address);
return ResponseEntity.ok(verificationResult);
} catch (EasyPostException e) {
log.error("Error verifying address: {}", e.getMessage());
return ResponseEntity.badRequest().body(createErrorResponse(e.getMessage()));
}
}
/**
* Get carrier accounts
*/
@GetMapping("/carriers")
public ResponseEntity<?> getCarrierAccounts() {
try {
var carriers = shippingService.getCarrierAccounts();
return ResponseEntity.ok(carriers);
} catch (EasyPostException e) {
log.error("Error getting carrier accounts: {}", e.getMessage());
return ResponseEntity.badRequest().body(createErrorResponse(e.getMessage()));
}
}
/**
* Create return shipment
*/
@PostMapping("/shipments/{originalShipmentId}/return")
public ResponseEntity<?> createReturnShipment(
@PathVariable String originalShipmentId,
@Valid @RequestBody ShipmentRequestDTO returnRequest) {
try {
Shipment returnShipment = shippingService.createReturnShipment(originalShipmentId, returnRequest);
return ResponseEntity.ok(returnShipment);
} catch (EasyPostException e) {
log.error("Error creating return shipment: {}", e.getMessage());
return ResponseEntity.badRequest().body(createErrorResponse(e.getMessage()));
}
}
private Map<String, Object> createErrorResponse(String message) {
Map<String, Object> errorResponse = new HashMap<>();
errorResponse.put("error", true);
errorResponse.put("message", message);
return errorResponse;
}
}

Webhook Controller

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import java.util.stream.Collectors;
@Slf4j
@RestController
@RequestMapping("/api/webhooks/easypost")
public class EasyPostWebhookController {
@Autowired
private ShippingService shippingService;
@Autowired
private EasyPostConfig easyPostConfig;
/**
* Handle EasyPost webhooks
*/
@PostMapping
public ResponseEntity<?> handleWebhook(HttpServletRequest request, @RequestBody String payload) {
try {
// Verify webhook signature (simplified - in production, verify HMAC signature)
String webhookSignature = request.getHeader("X-Hmac-Signature");
if (!isValidWebhookSignature(payload, webhookSignature)) {
log.warn("Invalid webhook signature received");
return ResponseEntity.badRequest().body("Invalid signature");
}
// Parse webhook payload
Map<String, Object> webhookData = objectMapper.readValue(payload, new TypeReference<Map<String, Object>>() {});
String eventType = (String) webhookData.get("description");
Map<String, Object> eventData = (Map<String, Object>) webhookData.get("result");
log.info("Received EasyPost webhook: {}", eventType);
// Process different event types
switch (eventType) {
case "tracker.updated":
handleTrackerUpdated(eventData);
break;
case "shipment.updated":
handleShipmentUpdated(eventData);
break;
case "insurance.purchased":
handleInsurancePurchased(eventData);
break;
default:
log.info("Unhandled webhook event: {}", eventType);
}
return ResponseEntity.ok().body("Webhook processed successfully");
} catch (Exception e) {
log.error("Error processing webhook: {}", e.getMessage(), e);
return ResponseEntity.status(500).body("Error processing webhook");
}
}
@SuppressWarnings("unchecked")
private void handleTrackerUpdated(Map<String, Object> eventData) {
try {
Map<String, Object> tracker = (Map<String, Object>) eventData.get("tracker");
String trackingCode = (String) tracker.get("tracking_code");
String carrier = (String) tracker.get("carrier");
// Update tracking information
shippingService.trackShipment(trackingCode, carrier);
log.info("Tracker updated for: {}", trackingCode);
} catch (Exception e) {
log.error("Error handling tracker update: {}", e.getMessage(), e);
}
}
private void handleShipmentUpdated(Map<String, Object> eventData) {
try {
// Handle shipment updates
log.info("Shipment updated: {}", eventData.get("id"));
} catch (Exception e) {
log.error("Error handling shipment update: {}", e.getMessage(), e);
}
}
private void handleInsurancePurchased(Map<String, Object> eventData) {
try {
// Handle insurance purchase
log.info("Insurance purchased: {}", eventData.get("id"));
} catch (Exception e) {
log.error("Error handling insurance purchase: {}", e.getMessage(), e);
}
}
private boolean isValidWebhookSignature(String payload, String signature) {
// In production, verify HMAC signature using webhook secret
if (easyPostConfig.getWebhookSecret() == null) {
return true; // Skip verification if no secret configured
}
// Implement HMAC verification here
return true; // Simplified for example
}
}

8. Custom Exception

public class EasyPostException extends Exception {
public EasyPostException(String message) {
super(message);
}
public EasyPostException(String message, Throwable cause) {
super(message, cause);
}
}

9. Repository Interfaces

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
@Repository
public interface ShipmentRepository extends JpaRepository<Shipment, Long> {
Optional<Shipment> findByEasypostShipmentId(String easypostShipmentId);
Optional<Shipment> findByTrackingCode(String trackingCode);
List<Shipment> findByReferenceNumber(String referenceNumber);
List<Shipment> findByOrderNumber(String orderNumber);
List<Shipment> findByStatus(Shipment.ShipmentStatus status);
List<Shipment> findByCarrier(String carrier);
List<Shipment> findByCreatedAtBetween(LocalDateTime start, LocalDateTime end);
@Query("SELECT s FROM Shipment s WHERE s.toAddress.country = :country")
List<Shipment> findByDestinationCountry(@Param("country") String country);
@Query("SELECT s.carrier, COUNT(s), SUM(s.rateAmount) FROM Shipment s WHERE s.createdAt BETWEEN :start AND :end GROUP BY s.carrier")
List<Object[]> getShippingStatsByCarrier(@Param("start") LocalDateTime start, @Param("end") LocalDateTime end);
}
@Repository
public interface ShipmentEventRepository extends JpaRepository<ShipmentEvent, Long> {
List<ShipmentEvent> findByShipmentId(Long shipmentId);
List<ShipmentEvent> findByTrackingCode(String trackingCode);
List<ShipmentEvent> findByEventType(String eventType);
List<ShipmentEvent> findByEventDateBetween(LocalDateTime start, LocalDateTime end);
}

This comprehensive EasyPost Shipping API integration provides complete functionality for shipping operations including shipment creation, rate comparison, label generation, tracking, address verification, and webhook handling for real-time updates.

Java Observability, Logging Intelligence & AI-Driven Monitoring (APM, Tracing, Logs & Anomaly Detection)

https://macronepal.com/blog/beyond-metrics-observing-serverless-and-traditional-java-applications-with-thundra-apm/
Explains using Thundra APM to observe both serverless and traditional Java applications by combining tracing, metrics, and logs into a unified observability platform for faster debugging and performance insights.

https://macronepal.com/blog/dynatrace-oneagent-in-java-2/
Explains Dynatrace OneAgent for Java, which automatically instruments JVM applications to capture metrics, traces, and logs, enabling full-stack monitoring and root-cause analysis with minimal configuration.

https://macronepal.com/blog/lightstep-java-sdk-distributed-tracing-and-observability-implementation/
Explains Lightstep Java SDK for distributed tracing, helping developers track requests across microservices and identify latency issues using OpenTelemetry-based observability.

https://macronepal.com/blog/honeycomb-io-beeline-for-java-complete-guide-2/
Explains Honeycomb Beeline for Java, which provides high-cardinality observability and deep query capabilities to understand complex system behavior and debug distributed systems efficiently.

https://macronepal.com/blog/lumigo-for-serverless-in-java-complete-distributed-tracing-guide-2/
Explains Lumigo for Java serverless applications, offering automatic distributed tracing, log correlation, and error tracking to simplify debugging in cloud-native environments. (Lumigo Docs)

https://macronepal.com/blog/from-noise-to-signals-implementing-log-anomaly-detection-in-java-applications/
Explains how to detect anomalies in Java logs using behavioral patterns and machine learning techniques to separate meaningful incidents from noisy log data and improve incident response.

https://macronepal.com/blog/ai-powered-log-analysis-in-java-from-reactive-debugging-to-proactive-insights/
Explains AI-driven log analysis for Java applications, shifting from manual debugging to predictive insights that identify issues early and improve system reliability using intelligent log processing.

https://macronepal.com/blog/titliel-java-logging-best-practices/
Explains best practices for Java logging, focusing on structured logs, proper log levels, performance optimization, and ensuring logs are useful for debugging and observability systems.

https://macronepal.com/blog/seeking-a-loguru-for-java-the-quest-for-elegant-and-simple-logging/
Explains the search for simpler, more elegant logging frameworks in Java, comparing modern logging approaches that aim to reduce complexity while improving readability and developer experience.

Leave a Reply

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


Macro Nepal Helper