Skip to content

Repository files navigation

ShopSavvy Data API - Java SDK

Maven CentralJavaSpring BootLicense: MITDocumentation

Official Java SDK for the ShopSavvy Data API. Access comprehensive product data, real-time pricing, and historical price trends across thousands of retailers and millions of products. Built for enterprise Java, Spring Boot, Android, and microservices architectures.

⚡ 30-Second Quick Start

<!-- Addtopom.xml: -->
<!-- <dependency>
<groupId>com.shopsavvy</groupId>
<artifactId>shopsavvy-sdk-java</artifactId>
<version>1.0.0</version>
</dependency> -->
// Use in Java/Spring Boot:importcom.shopsavvy.sdk.*;
importcom.shopsavvy.sdk.models.*;
publicclassQuickExample {
publicstaticvoidmain(String[] args) {
ShopSavvyClientclient = newShopSavvyClient("ss_live_your_api_key_here");
try {
ApiResponse<ProductDetails> product = client.getProductDetails("012345678901");
ApiResponse<List<Offer>> offers = client.getCurrentOffers("012345678901");
OfferbestOffer = offers.getData().stream()
.min((o1, o2) -> Double.compare(o1.getPrice(), o2.getPrice()))
.orElse(null);
System.out.printf("%s - Best price: $%.2f at %s%n",
product.getData().getName(),
bestOffer.getPrice(),
bestOffer.getRetailer());
} catch (ShopSavvyApiExceptione) {
System.err.println("Error: " + e.getMessage());
} finally {
client.close();
}
}
}

🚀 Installation & Setup

Requirements

  • Java 8 or higher
  • Maven 3.6+ or Gradle 6.0+
  • Jackson 2.15+ (for JSON processing)
  • OkHttp 4.11+ (for HTTP requests)

Maven

<dependencies>
<dependency>
<groupId>com.shopsavvy</groupId>
<artifactId>shopsavvy-sdk-java</artifactId>
<version>1.0.0</version>
</dependency>
<!-- For Spring Boot projects -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.7.14</version>
</dependency>
<!-- For reactive programming -->
<dependency>
<groupId>io.reactivex.rxjava3</groupId>
<artifactId>rxjava</artifactId>
<version>3.1.6</version>
</dependency>
</dependencies>

Gradle

dependencies {
implementation 'com.shopsavvy:shopsavvy-sdk-java:1.0.0'// For Spring Boot projects
implementation 'org.springframework.boot:spring-boot-starter-web:2.7.14'// For reactive programming
implementation 'io.reactivex.rxjava3:rxjava:3.1.6'
}

Get Your API Key

  1. Sign up: Visit shopsavvy.com/data
  2. Choose plan: Select based on your usage needs
  3. Get API key: Copy from your dashboard
  4. Test: Run the 30-second example above

📖 Complete API Reference

Client Configuration

importcom.shopsavvy.sdk.*;
importcom.shopsavvy.sdk.exceptions.*;
// Basic configurationShopSavvyClientclient = newShopSavvyClient("ss_live_your_api_key_here");
// Advanced configurationShopSavvyClientclient = newShopSavvyClient.Builder()
.apiKey("ss_live_your_api_key_here")
.baseUrl("https://api.shopsavvy.com/v1") // Custom base URL
.timeoutSeconds(60) // Request timeout
.retryAttempts(3) // Retry failed requests
.userAgent("MyApp/1.0.0") // Custom user agent
.connectionPoolSize(10) // Connection pool size
.build();
// Environment variable configurationStringapiKey = System.getenv("SHOPSAVVY_API_KEY");
if (apiKey == null) {
thrownewIllegalStateException("SHOPSAVVY_API_KEY environment variable not set");
}
ShopSavvyClientclient = newShopSavvyClient(apiKey);

Product Lookup

Single Product

importcom.shopsavvy.sdk.models.*;
// Look up by barcode, ASIN, URL, model number, or ShopSavvy IDApiResponse<ProductDetails> product = client.getProductDetails("012345678901");
ApiResponse<ProductDetails> amazonProduct = client.getProductDetails("B08N5WRWNW");
ApiResponse<ProductDetails> urlProduct = client.getProductDetails("https://www.amazon.com/dp/B08N5WRWNW");
ApiResponse<ProductDetails> modelProduct = client.getProductDetails("MQ023LL/A"); // iPhone model numberProductDetailsproductData = product.getData();
System.out.println("📦 Product: " + productData.getName());
System.out.println("🏷️ Brand: " + (productData.getBrand() != null ? productData.getBrand() : "N/A"));
System.out.println("📂 Category: " + (productData.getCategory() != null ? productData.getCategory() : "N/A"));
System.out.println("🔢 Product ID: " + productData.getId());
if (productData.getAsin() != null) {
System.out.println("📦 ASIN: " + productData.getAsin());
}
if (productData.getModelNumber() != null) {
System.out.println("🔧 Model: " + productData.getModelNumber());
}

Bulk Product Lookup

importjava.util.*;
// Process up to 100 products at once (Pro plan)List<String> identifiers = Arrays.asList(
"012345678901", "B08N5WRWNW", "045496590048",
"https://www.bestbuy.com/site/product/123456",
"MQ023LL/A", "SM-S911U"// iPhone and Samsung model numbers
);
ApiResponse<List<ProductDetails>> products = client.getProductDetailsBatch(identifiers);
for (inti = 0; i < identifiers.size(); i++) {
ProductDetailsproduct = products.getData().get(i);
if (product == null) {
System.out.println("❌ Failed to find product: " + identifiers.get(i));
} else {
System.out.printf("✓ Found: %s by %s%n", product.getName(), product.getBrand() != null ? product.getBrand() : "Unknown");
}
}

Real-Time Pricing

Spring Boot REST API Integration

importorg.springframework.web.bind.annotation.*;
importorg.springframework.stereotype.Service;
importorg.springframework.beans.factory.annotation.Autowired;
importorg.springframework.http.ResponseEntity;
importorg.springframework.cache.annotation.Cacheable;
@RestController@RequestMapping("/api/v1/products")
publicclassProductController {
@AutowiredprivateProductServiceproductService;
@GetMapping("/{identifier}/compare")
publicResponseEntity<PriceComparisonResponse> comparePrice(@PathVariableStringidentifier) {
try {
PriceComparisonResponsecomparison = productService.comparePrice(identifier);
returnResponseEntity.ok(comparison);
} catch (ShopSavvyApiExceptione) {
returnResponseEntity.status(500).body(null);
}
}
@GetMapping("/{identifier}/history")
publicResponseEntity<List<OfferWithHistory>> getPriceHistory(
@PathVariableStringidentifier,
@RequestParamStringstartDate,
@RequestParamStringendDate) {
try {
List<OfferWithHistory> history = productService.getPriceHistory(identifier, startDate, endDate);
returnResponseEntity.ok(history);
} catch (ShopSavvyApiExceptione) {
returnResponseEntity.status(500).body(null);
}
}
@PostMapping("/{identifier}/alerts")
publicResponseEntity<String> setPriceAlert(
@PathVariableStringidentifier,
@RequestBodyPriceAlertRequestrequest) {
try {
productService.setPriceAlert(identifier, request.getTargetPrice(), request.getEmail());
returnResponseEntity.ok("Price alert set successfully");
} catch (ShopSavvyApiExceptione) {
returnResponseEntity.status(500).body("Failed to set price alert");
}
}
}
@ServicepublicclassProductService {
privatefinalShopSavvyClientclient;
privatefinalEmailServiceemailService;
publicProductService(ShopSavvyClientclient, EmailServiceemailService) {
this.client = client;
this.emailService = emailService;
}
@Cacheable(value = "priceComparisons", key = "#identifier")
publicPriceComparisonResponsecomparePrice(Stringidentifier) throwsShopSavvyApiException {
ApiResponse<List<Offer>> response = client.getCurrentOffers(identifier);
List<Offer> offers = response.getData();
if (offers.isEmpty()) {
thrownewShopSavvyNotFoundException("No offers found for product: " + identifier);
}
// Sort by priceList<Offer> sortedOffers = offers.stream()
.filter(offer -> offer.getPrice() != null)
.sorted((o1, o2) -> Double.compare(o1.getPrice(), o2.getPrice()))
.collect(Collectors.toList());
Offercheapest = sortedOffers.get(0);
OffermostExpensive = sortedOffers.get(sortedOffers.size() - 1);
doubleaveragePrice = sortedOffers.stream()
.mapToDouble(Offer::getPrice)
.average()
.orElse(0.0);
doublepotentialSavings = mostExpensive.getPrice() - cheapest.getPrice();
// Filter by availability and conditionlonginStockCount = offers.stream()
.filter(offer -> "in_stock".equals(offer.getAvailability()))
.count();
longnewConditionCount = offers.stream()
.filter(offer -> "new".equals(offer.getCondition()))
.count();
returnnewPriceComparisonResponse(
sortedOffers,
cheapest,
mostExpensive,
averagePrice,
potentialSavings,
(int) inStockCount,
(int) newConditionCount
);
}
publicList<OfferWithHistory> getPriceHistory(Stringidentifier, StringstartDate, StringendDate) throwsShopSavvyApiException {
ApiResponse<List<OfferWithHistory>> response = client.getPriceHistory(identifier, startDate, endDate);
returnresponse.getData();
}
publicvoidsetPriceAlert(Stringidentifier, doubletargetPrice, Stringemail) throwsShopSavvyApiException {
// Schedule monitoring with APIclient.scheduleProductMonitoring(identifier, "daily");
// Set up local alert tracking (you would implement this with your database)PriceAlertalert = newPriceAlert(identifier, targetPrice, email, true);
// saveToDatabase(alert);// Send confirmation emailemailService.sendPriceAlertConfirmation(email, identifier, targetPrice);
}
}
// Response DTOspublicclassPriceComparisonResponse {
privateList<Offer> offers;
privateOfferbestOffer;
privateOfferworstOffer;
privatedoubleaveragePrice;
privatedoublepotentialSavings;
privateintinStockOffers;
privateintnewConditionOffers;
// Constructor and getters/setterspublicPriceComparisonResponse(List<Offer> offers, OfferbestOffer, OfferworstOffer,
doubleaveragePrice, doublepotentialSavings, intinStockOffers, intnewConditionOffers) {
this.offers = offers;
this.bestOffer = bestOffer;
this.worstOffer = worstOffer;
this.averagePrice = averagePrice;
this.potentialSavings = potentialSavings;
this.inStockOffers = inStockOffers;
this.newConditionOffers = newConditionOffers;
}
// Getters and setters...
}
publicclassPriceAlertRequest {
privatedoubletargetPrice;
privateStringemail;
// Getters and setters...
}

Enterprise Background Monitoring Service

importorg.springframework.scheduling.annotation.Scheduled;
importorg.springframework.stereotype.Component;
importorg.springframework.transaction.annotation.Transactional;
importjava.util.concurrent.*;
importjava.util.List;
@ComponentpublicclassPriceMonitoringService {
privatefinalShopSavvyClientclient;
privatefinalPriceAlertRepositoryalertRepository;
privatefinalNotificationServicenotificationService;
privatefinalExecutorServiceexecutorService;
publicPriceMonitoringService(ShopSavvyClientclient, PriceAlertRepositoryalertRepository,
NotificationServicenotificationService) {
this.client = client;
this.alertRepository = alertRepository;
this.notificationService = notificationService;
this.executorService = Executors.newFixedThreadPool(10);
}
@Scheduled(fixedRate = 3600000) // Run every hour@TransactionalpublicvoidmonitorPrices() {
List<PriceAlert> activeAlerts = alertRepository.findByActiveTrue();
List<CompletableFuture<Void>> futures = activeAlerts.stream()
.map(alert -> CompletableFuture.runAsync(() -> checkPriceAlert(alert), executorService))
.collect(Collectors.toList());
// Wait for all checks to completeCompletableFuture.allOf(futures.toArray(newCompletableFuture[0]))
.thenRun(() -> System.out.println("Completed monitoring " + activeAlerts.size() + " price alerts"))
.exceptionally(throwable -> {
System.err.println("Error during price monitoring: " + throwable.getMessage());
returnnull;
});
}
privatevoidcheckPriceAlert(PriceAlertalert) {
try {
ApiResponse<List<Offer>> response = client.getCurrentOffers(alert.getProductId());
List<Offer> offers = response.getData();
OfferbestOffer = offers.stream()
.filter(offer -> offer.getPrice() != null)
.min((o1, o2) -> Double.compare(o1.getPrice(), o2.getPrice()))
.orElse(null);
if (bestOffer != null && bestOffer.getPrice() <= alert.getTargetPrice()) {
// Send notificationnotificationService.sendPriceAlert(alert, bestOffer);
// Update alertalert.setLastTriggered(Instant.now());
alert.setTriggeredCount(alert.getTriggeredCount() + 1);
alertRepository.save(alert);
System.out.printf("🎯 Price alert triggered! Product %s reached target price $%.2f at %s%n",
alert.getProductId(), bestOffer.getPrice(), bestOffer.getRetailer());
}
// Update price historysavePriceHistory(alert.getProductId(), bestOffer);
// Rate limitingThread.sleep(1000);
} catch (ShopSavvyApiExceptione) {
System.err.println("Failed to check price for " + alert.getProductId() + ": " + e.getMessage());
} catch (InterruptedExceptione) {
Thread.currentThread().interrupt();
}
}
privatevoidsavePriceHistory(StringproductId, Offeroffer) {
if (offer != null) {
PriceHistoryhistory = newPriceHistory(
productId,
offer.getRetailer(),
offer.getPrice(),
Instant.now()
);
// Save to database// priceHistoryRepository.save(history);
}
}
}
// Notification Service@ServicepublicclassNotificationService {
@AutowiredprivateJavaMailSendermailSender;
publicvoidsendPriceAlert(PriceAlertalert, Offeroffer) {
try {
SimpleMailMessagemessage = newSimpleMailMessage();
message.setTo(alert.getEmail());
message.setSubject("🎯 Price Alert Triggered!");
message.setText(String.format(
"Great news! The product you're watching has reached your target price.\n\n" +
"Product ID: %s\n" +
"Target Price: $%.2f\n" +
"Current Price: $%.2f\n" +
"Retailer: %s\n" +
"Savings: $%.2f\n\n" +
"Shop now: %s\n\n" +
"Happy shopping!\n" +
"The ShopSavvy Team",
alert.getProductId(),
alert.getTargetPrice(),
offer.getPrice(),
offer.getRetailer(),
alert.getTargetPrice() - offer.getPrice(),
offer.getUrl() != null ? offer.getUrl() : "Visit retailer website"
));
mailSender.send(message);
} catch (Exceptione) {
System.err.println("Failed to send price alert email: " + e.getMessage());
}
}
}

🚀 Production Deployment

Enterprise Spring Boot Configuration

importorg.springframework.context.annotation.Bean;
importorg.springframework.context.annotation.Configuration;
importorg.springframework.boot.context.properties.ConfigurationProperties;
importorg.springframework.cache.annotation.EnableCaching;
importorg.springframework.scheduling.annotation.EnableScheduling;
importorg.springframework.retry.annotation.EnableRetry;
@Configuration@EnableCaching@EnableScheduling@EnableRetry@ConfigurationProperties(prefix = "shopsavvy")
publicclassShopSavvyConfig {
privateStringapiKey;
privateStringbaseUrl = "https://api.shopsavvy.com/v1";
privateinttimeoutSeconds = 60;
privateintretryAttempts = 3;
privateintconnectionPoolSize = 20;
@BeanpublicShopSavvyClientshopSavvyClient() {
returnnewShopSavvyClient.Builder()
.apiKey(apiKey)
.baseUrl(baseUrl)
.timeoutSeconds(timeoutSeconds)
.retryAttempts(retryAttempts)
.connectionPoolSize(connectionPoolSize)
.build();
}
// Getters and setters for configuration propertiespublicStringgetApiKey() { returnapiKey; }
publicvoidsetApiKey(StringapiKey) { this.apiKey = apiKey; }
publicStringgetBaseUrl() { returnbaseUrl; }
publicvoidsetBaseUrl(StringbaseUrl) { this.baseUrl = baseUrl; }
publicintgetTimeoutSeconds() { returntimeoutSeconds; }
publicvoidsetTimeoutSeconds(inttimeoutSeconds) { this.timeoutSeconds = timeoutSeconds; }
publicintgetRetryAttempts() { returnretryAttempts; }
publicvoidsetRetryAttempts(intretryAttempts) { this.retryAttempts = retryAttempts; }
publicintgetConnectionPoolSize() { returnconnectionPoolSize; }
publicvoidsetConnectionPoolSize(intconnectionPoolSize) { this.connectionPoolSize = connectionPoolSize; }
}
// Application properties// application.yml/*shopsavvy: api-key: ${SHOPSAVVY_API_KEY:ss_live_your_api_key_here} base-url: ${SHOPSAVVY_BASE_URL:https://api.shopsavvy.com/v1} timeout-seconds: ${SHOPSAVVY_TIMEOUT:60} retry-attempts: ${SHOPSAVVY_RETRY_ATTEMPTS:3} connection-pool-size: ${SHOPSAVVY_POOL_SIZE:20}spring: cache: type: caffeine caffeine: spec: maximumSize=1000,expireAfterWrite=5m mail: host: ${MAIL_HOST:smtp.gmail.com} port: ${MAIL_PORT:587} username: ${MAIL_USERNAME} password: ${MAIL_PASSWORD} properties: mail: smtp: auth: true starttls: enable: true*/

Reactive Programming with RxJava

importio.reactivex.rxjava3.core.*;
importio.reactivex.rxjava3.schedulers.Schedulers;
importjava.time.Duration;
importjava.util.concurrent.TimeUnit;
@ServicepublicclassReactiveProductService {
privatefinalShopSavvyClientclient;
publicReactiveProductService(ShopSavvyClientclient) {
this.client = client;
}
publicObservable<PriceUpdate> monitorPriceChanges(StringproductId, Durationinterval) {
returnObservable.interval(interval.toSeconds(), TimeUnit.SECONDS)
.map(tick -> getCurrentPrice(productId))
.filter(priceUpdate -> priceUpdate != null)
.distinctUntilChanged(PriceUpdate::getPrice)
.observeOn(Schedulers.io());
}
publicSingle<List<ProductDetails>> searchProductsReactive(List<String> identifiers) {
returnSingle.fromCallable(() -> {
try {
ApiResponse<List<ProductDetails>> response = client.getProductDetailsBatch(identifiers);
returnresponse.getData();
} catch (ShopSavvyApiExceptione) {
thrownewRuntimeException(e);
}
}).subscribeOn(Schedulers.io());
}
publicObservable<PriceComparison> streamPriceComparisons(List<String> productIds) {
returnObservable.fromIterable(productIds)
.flatMap(productId -> Observable.fromCallable(() -> comparePrice(productId))
.subscribeOn(Schedulers.io())
.retry(3)
.timeout(30, TimeUnit.SECONDS)
.onErrorResumeNext(throwable -> {
System.err.println("Failed to get price for " + productId + ": " + throwable.getMessage());
returnObservable.empty();
})
);
}
privatePriceUpdategetCurrentPrice(StringproductId) {
try {
ApiResponse<List<Offer>> response = client.getCurrentOffers(productId);
OfferbestOffer = response.getData().stream()
.min((o1, o2) -> Double.compare(o1.getPrice(), o2.getPrice()))
.orElse(null);
if (bestOffer != null) {
returnnewPriceUpdate(productId, bestOffer.getPrice(), bestOffer.getRetailer(), Instant.now());
}
} catch (ShopSavvyApiExceptione) {
System.err.println("Error getting price for " + productId + ": " + e.getMessage());
}
returnnull;
}
privatePriceComparisoncomparePrice(StringproductId) throwsShopSavvyApiException {
// Implementation similar to previous exampleApiResponse<List<Offer>> response = client.getCurrentOffers(productId);
// ... rest of implementationreturnnewPriceComparison(/* ... */);
}
}
// Usage in controller@RestControllerpublicclassReactiveProductController {
@AutowiredprivateReactiveProductServiceproductService;
@GetMapping(value = "/api/v1/products/{id}/price-stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
publicObservable<PriceUpdate> streamPriceUpdates(@PathVariableStringid) {
returnproductService.monitorPriceChanges(id, Duration.ofMinutes(5))
.doOnNext(update -> System.out.println("Price update: " + update))
.doOnError(error -> System.err.println("Stream error: " + error.getMessage()));
}
}

Microservices Integration

importorg.springframework.cloud.openfeign.FeignClient;
importorg.springframework.web.bind.annotation.*;
// Feign client for microservices communication@FeignClient(name = "product-service", url = "${product-service.url}")
publicinterfaceProductServiceClient {
@GetMapping("/api/v1/products/{id}")
ResponseEntity<ProductDetails> getProduct(@PathVariableStringid);
@GetMapping("/api/v1/products/{id}/offers")
ResponseEntity<List<Offer>> getOffers(@PathVariableStringid);
}
// Circuit breaker integration@ComponentpublicclassProductServiceFallback {
privatefinalShopSavvyClientclient;
publicProductServiceFallback(ShopSavvyClientclient) {
this.client = client;
}
@CircuitBreaker(name = "product-service", fallbackMethod = "fallbackGetProduct")
publicProductDetailsgetProductWithFallback(StringproductId) throwsShopSavvyApiException {
// Primary service callreturncallPrimaryProductService(productId);
}
publicProductDetailsfallbackGetProduct(StringproductId, Exceptionex) {
try {
// Fallback to ShopSavvy APIApiResponse<ProductDetails> response = client.getProductDetails(productId);
returnresponse.getData();
} catch (ShopSavvyApiExceptione) {
thrownewRuntimeException("Both primary service and fallback failed", e);
}
}
privateProductDetailscallPrimaryProductService(StringproductId) {
// Implementation to call primary servicereturnnull; // placeholder
}
}

Exception Handling

The SDK provides comprehensive exception handling with Java-specific patterns:

importcom.shopsavvy.sdk.exceptions.*;
publicvoidhandleProductLookup(Stringidentifier) {
try {
ApiResponse<ProductDetails> response = client.getProductDetails(identifier);
System.out.println("✅ Found product: " + response.getData().getName());
} catch (ShopSavvyAuthenticationExceptione) {
System.err.println("🔐 Authentication failed: " + e.getMessage());
// Refresh API key or redirect to login
} catch (ShopSavvyNotFoundExceptione) {
System.err.println("❌ Product not found: " + e.getMessage());
// Show "not found" UI
} catch (ShopSavvyValidationExceptione) {
System.err.println("⚠️ Invalid parameters: " + e.getMessage());
// Show validation error to user
} catch (ShopSavvyRateLimitExceptione) {
System.err.println("🚦 Rate limit exceeded: " + e.getMessage());
// Implement exponential backofftry {
Thread.sleep(e.getRetryAfterSeconds() * 1000L);
} catch (InterruptedExceptionie) {
Thread.currentThread().interrupt();
}
} catch (ShopSavvyNetworkExceptione) {
System.err.println("🌐 Network error: " + e.getMessage());
// Show offline mode or retry option
} catch (ShopSavvyApiExceptione) {
System.err.println("❌ API error: " + e.getMessage());
// Generic error handling
} catch (Exceptione) {
System.err.println("💥 Unexpected error: " + e.getMessage());
// Log to crash reporting service
}
}

Retry Logic with Exponential Backoff

importjava.time.Duration;
importjava.util.function.Supplier;
publicclassRetryUtil {
publicstatic <T> TretryWithBackoff(Supplier<T> operation, intmaxAttempts, DurationinitialDelay, DurationmaxDelay, doublemultiplier) throwsException {
ExceptionlastException = null;
DurationcurrentDelay = initialDelay;
for (intattempt = 1; attempt <= maxAttempts; attempt++) {
try {
returnoperation.get();
} catch (ShopSavvyRateLimitExceptione) {
lastException = e;
if (attempt == maxAttempts) break;
// Use server-specified retry delay if availableDurationretryDelay = e.getRetryAfterSeconds() != null ? Duration.ofSeconds(e.getRetryAfterSeconds())
: currentDelay;
try {
Thread.sleep(retryDelay.toMillis());
} catch (InterruptedExceptionie) {
Thread.currentThread().interrupt();
thrownewRuntimeException(ie);
}
currentDelay = Duration.ofMillis(Math.min(
(long) (currentDelay.toMillis() * multiplier),
maxDelay.toMillis()
));
} catch (ShopSavvyNetworkExceptione) {
lastException = e;
if (attempt == maxAttempts) break;
try {
Thread.sleep(currentDelay.toMillis());
} catch (InterruptedExceptionie) {
Thread.currentThread().interrupt();
thrownewRuntimeException(ie);
}
currentDelay = Duration.ofMillis(Math.min(
(long) (currentDelay.toMillis() * multiplier),
maxDelay.toMillis()
));
}
}
thrownewRuntimeException("Operation failed after " + maxAttempts + " attempts", lastException);
}
}
// UsagepublicProductDetailsgetProductWithRetry(Stringidentifier) throwsException {
returnRetryUtil.retryWithBackoff(
() -> {
try {
returnclient.getProductDetails(identifier).getData();
} catch (ShopSavvyApiExceptione) {
thrownewRuntimeException(e);
}
},
3, // max attemptsDuration.ofSeconds(1), // initial delayDuration.ofSeconds(30), // max delay2.0// multiplier
);
}

🛠️ Development & Testing

Local Development Setup

# Clone the repository
git clone https://github.com/shopsavvy/sdk-java.git
cd sdk-java
# Build with Maven
mvn clean compile
# Run tests
mvn test# Build JAR
mvn package
# Install to local repository
mvn install
# Build with Gradle (alternative)
./gradlew build
./gradlew test
./gradlew publishToMavenLocal

Testing Your Integration

importorg.junit.jupiter.api.Test;
importorg.junit.jupiter.api.BeforeEach;
importstaticorg.junit.jupiter.api.Assertions.*;
publicclassSDKIntegrationTest {
privateShopSavvyClientclient;
@BeforeEachvoidsetUp() {
client = newShopSavvyClient("ss_test_your_test_key_here");
}
@TestvoidtestProductLookup() {
try {
ApiResponse<ProductDetails> response = client.getProductDetails("012345678901");
assertNotNull(response.getData());
assertNotNull(response.getData().getName());
System.out.println("✅ Product lookup: " + response.getData().getName());
} catch (ShopSavvyApiExceptione) {
fail("Product lookup failed: " + e.getMessage());
}
}
@TestvoidtestCurrentOffers() {
try {
ApiResponse<List<Offer>> response = client.getCurrentOffers("012345678901");
assertNotNull(response.getData());
System.out.println("✅ Current offers: " + response.getData().size() + " found");
} catch (ShopSavvyApiExceptione) {
fail("Current offers failed: " + e.getMessage());
}
}
@TestvoidtestUsageInfo() {
try {
ApiResponse<UsageInfo> response = client.getUsage();
assertNotNull(response.getData());
System.out.println("✅ API usage: " + response.getData().getCreditsRemaining() + " credits remaining");
} catch (ShopSavvyApiExceptione) {
fail("Usage info failed: " + e.getMessage());
}
}
@AfterEachvoidtearDown() {
if (client != null) {
client.close();
}
System.out.println("\n🎉 All tests passed! SDK is working correctly.");
}
}

Data Models

All models are implemented as Java POJOs with Jackson annotations and null-safety:

ProductDetails

importcom.fasterxml.jackson.annotation.JsonProperty;
importcom.fasterxml.jackson.annotation.JsonIgnoreProperties;
importorg.jetbrains.annotations.Nullable;
importorg.jetbrains.annotations.NotNull;
@JsonIgnoreProperties(ignoreUnknown = true)
publicclassProductDetails {
@JsonProperty("id")
@NotNullprivateStringid;
@JsonProperty("name")
@NotNullprivateStringname;
@JsonProperty("description")
@NullableprivateStringdescription;
@JsonProperty("brand")
@NullableprivateStringbrand;
@JsonProperty("category")
@NullableprivateStringcategory;
@JsonProperty("upc")
@NullableprivateStringupc;
@JsonProperty("asin")
@NullableprivateStringasin;
@JsonProperty("model_number")
@NullableprivateStringmodelNumber;
@JsonProperty("images")
@NotNullprivateList<String> images = newArrayList<>();
@JsonProperty("specifications")
@NotNullprivateMap<String, String> specifications = newHashMap<>();
@JsonProperty("created_at")
@NullableprivateStringcreatedAt;
@JsonProperty("updated_at")
@NullableprivateStringupdatedAt;
// ConstructorspublicProductDetails() {}
publicProductDetails(@NotNullStringid, @NotNullStringname) {
this.id = id;
this.name = name;
}
// Computed properties for conveniencepublicbooleanhasImages() {
returnimages != null && !images.isEmpty();
}
publicbooleanhasSpecifications() {
returnspecifications != null && !specifications.isEmpty();
}
publicStringgetDisplayName() {
if (brand != null && !brand.trim().isEmpty()) {
returnbrand.trim() + " " + name.trim();
}
returnname.trim();
}
// Standard getters and setters@NotNullpublicStringgetId() { returnid; }
publicvoidsetId(@NotNullStringid) { this.id = id; }
@NotNullpublicStringgetName() { returnname; }
publicvoidsetName(@NotNullStringname) { this.name = name; }
@NullablepublicStringgetDescription() { returndescription; }
publicvoidsetDescription(@NullableStringdescription) { this.description = description; }
@NullablepublicStringgetBrand() { returnbrand; }
publicvoidsetBrand(@NullableStringbrand) { this.brand = brand; }
@NullablepublicStringgetCategory() { returncategory; }
publicvoidsetCategory(@NullableStringcategory) { this.category = category; }
@NullablepublicStringgetUpc() { returnupc; }
publicvoidsetUpc(@NullableStringupc) { this.upc = upc; }
@NullablepublicStringgetAsin() { returnasin; }
publicvoidsetAsin(@NullableStringasin) { this.asin = asin; }
@NullablepublicStringgetModelNumber() { returnmodelNumber; }
publicvoidsetModelNumber(@NullableStringmodelNumber) { this.modelNumber = modelNumber; }
@NotNullpublicList<String> getImages() { returnimages; }
publicvoidsetImages(@NotNullList<String> images) { this.images = images; }
@NotNullpublicMap<String, String> getSpecifications() { returnspecifications; }
publicvoidsetSpecifications(@NotNullMap<String, String> specifications) { this.specifications = specifications; }
@NullablepublicStringgetCreatedAt() { returncreatedAt; }
publicvoidsetCreatedAt(@NullableStringcreatedAt) { this.createdAt = createdAt; }
@NullablepublicStringgetUpdatedAt() { returnupdatedAt; }
publicvoidsetUpdatedAt(@NullableStringupdatedAt) { this.updatedAt = updatedAt; }
@OverridepublicStringtoString() {
return"ProductDetails{" +
"id='" + id + '\'' +
", name='" + name + '\'' +
", brand='" + brand + '\'' +
", category='" + category + '\'' +
'}';
}
}

Offer

@JsonIgnoreProperties(ignoreUnknown = true)
publicclassOffer {
@JsonProperty("retailer")
@NotNullprivateStringretailer;
@JsonProperty("price")
@NullableprivateDoubleprice;
@JsonProperty("currency")
@NullableprivateStringcurrency = "USD";
@JsonProperty("availability")
@NullableprivateStringavailability;
@JsonProperty("condition")
@NullableprivateStringcondition;
@JsonProperty("shipping_cost")
@NullableprivateDoubleshippingCost;
@JsonProperty("url")
@NullableprivateStringurl;
@JsonProperty("last_updated")
@NullableprivateStringlastUpdated;
// ConstructorspublicOffer() {}
publicOffer(@NotNullStringretailer, @NullableDoubleprice) {
this.retailer = retailer;
this.price = price;
}
// Computed propertiespublicbooleanisInStock() {
return"in_stock".equals(availability);
}
publicbooleanisNew() {
return"new".equals(condition);
}
publicdoublegetTotalCost() {
doublebasePrice = price != null ? price : 0.0;
doubleshipping = shippingCost != null ? shippingCost : 0.0;
returnbasePrice + shipping;
}
publicStringgetFormattedPrice() {
if (price == null) return"N/A";
returnString.format("$%.2f", price);
}
// Standard getters and setters@NotNullpublicStringgetRetailer() { returnretailer; }
publicvoidsetRetailer(@NotNullStringretailer) { this.retailer = retailer; }
@NullablepublicDoublegetPrice() { returnprice; }
publicvoidsetPrice(@NullableDoubleprice) { this.price = price; }
@NullablepublicStringgetCurrency() { returncurrency; }
publicvoidsetCurrency(@NullableStringcurrency) { this.currency = currency; }
@NullablepublicStringgetAvailability() { returnavailability; }
publicvoidsetAvailability(@NullableStringavailability) { this.availability = availability; }
@NullablepublicStringgetCondition() { returncondition; }
publicvoidsetCondition(@NullableStringcondition) { this.condition = condition; }
@NullablepublicDoublegetShippingCost() { returnshippingCost; }
publicvoidsetShippingCost(@NullableDoubleshippingCost) { this.shippingCost = shippingCost; }
@NullablepublicStringgetUrl() { returnurl; }
publicvoidsetUrl(@NullableStringurl) { this.url = url; }
@NullablepublicStringgetLastUpdated() { returnlastUpdated; }
publicvoidsetLastUpdated(@NullableStringlastUpdated) { this.lastUpdated = lastUpdated; }
@OverridepublicStringtoString() {
return"Offer{" +
"retailer='" + retailer + '\'' +
", price=" + price +
", availability='" + availability + '\'' +
", condition='" + condition + '\'' +
'}';
}
}

UsageInfo

@JsonIgnoreProperties(ignoreUnknown = true)
publicclassUsageInfo {
@JsonProperty("credits_used")
@NullableprivateIntegercreditsUsed;
@JsonProperty("credits_remaining")
@NullableprivateIntegercreditsRemaining;
@JsonProperty("credits_limit")
@NullableprivateIntegercreditsLimit;
@JsonProperty("reset_date")
@NullableprivateStringresetDate;
@JsonProperty("current_period_start")
@NullableprivateStringcurrentPeriodStart;
@JsonProperty("current_period_end")
@NullableprivateStringcurrentPeriodEnd;
// ConstructorspublicUsageInfo() {}
// Computed propertiespublicdoublegetUsagePercentage() {
intused = creditsUsed != null ? creditsUsed : 0;
intlimit = creditsLimit != null ? creditsLimit : 1;
return ((double) used / limit) * 100.0;
}
publicbooleanisNearLimit() {
returngetUsagePercentage() > 80.0;
}
// Standard getters and setters@NullablepublicIntegergetCreditsUsed() { returncreditsUsed; }
publicvoidsetCreditsUsed(@NullableIntegercreditsUsed) { this.creditsUsed = creditsUsed; }
@NullablepublicIntegergetCreditsRemaining() { returncreditsRemaining; }
publicvoidsetCreditsRemaining(@NullableIntegercreditsRemaining) { this.creditsRemaining = creditsRemaining; }
@NullablepublicIntegergetCreditsLimit() { returncreditsLimit; }
publicvoidsetCreditsLimit(@NullableIntegercreditsLimit) { this.creditsLimit = creditsLimit; }
@NullablepublicStringgetResetDate() { returnresetDate; }
publicvoidsetResetDate(@NullableStringresetDate) { this.resetDate = resetDate; }
@NullablepublicStringgetCurrentPeriodStart() { returncurrentPeriodStart; }
publicvoidsetCurrentPeriodStart(@NullableStringcurrentPeriodStart) { this.currentPeriodStart = currentPeriodStart; }
@NullablepublicStringgetCurrentPeriodEnd() { returncurrentPeriodEnd; }
publicvoidsetCurrentPeriodEnd(@NullableStringcurrentPeriodEnd) { this.currentPeriodEnd = currentPeriodEnd; }
@OverridepublicStringtoString() {
return"UsageInfo{" +
"creditsUsed=" + creditsUsed +
", creditsRemaining=" + creditsRemaining +
", creditsLimit=" + creditsLimit +
", usagePercentage=" + String.format("%.1f%%", getUsagePercentage()) +
'}';
}
}

📚 Additional Resources

🤝 Contributing

We welcome contributions! Please see our Contributing Guide for details on:

  • Reporting bugs and feature requests
  • Setting up development environment
  • Submitting pull requests
  • Code standards and testing
  • Java and Spring Boot best practices

📄 License

This project is licensed under the MIT License - see the LICENSE file for details.

🏢 About ShopSavvy

ShopSavvy is the world's first mobile shopping app, helping consumers find the best deals since 2008. With over 40 million downloads and millions of active users, ShopSavvy has saved consumers billions of dollars.

Our Data API Powers:

  • 🛒 E-commerce platforms with competitive intelligence
  • 📊 Market research with real-time pricing data
  • 🏪 Retailers with inventory and pricing optimization
  • 📱 Mobile apps with product lookup and price comparison
  • 🤖 Business intelligence with automated price monitoring

Why Choose ShopSavvy Data API?

  • Trusted by millions - Proven at scale since 2008
  • Comprehensive coverage - 1000+ retailers, millions of products
  • Real-time accuracy - Fresh data updated continuously
  • Developer-friendly - Easy integration, great documentation
  • Reliable infrastructure - 99.9% uptime, enterprise-grade
  • Flexible pricing - Plans for every use case and budget

Perfect for Java & Enterprise:

  • 🚀 Spring Boot ready - First-class Spring framework integration
  • 🏢 Enterprise patterns - Circuit breakers, retry logic, monitoring
  • 📊 Reactive programming - RxJava and reactive streams support
  • 🛡️ Type-safe - Comprehensive null-safety with annotations
  • High performance - Connection pooling and async operations
  • 🔧 Microservices - Perfect for distributed architectures

Ready to get started?Sign up for your API keyNeed help?Contact us

About

Official Java SDK for ShopSavvy Data API

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages