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.
<!-- 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();
}
}
}- Java 8 or higher
- Maven 3.6+ or Gradle 6.0+
- Jackson 2.15+ (for JSON processing)
- OkHttp 4.11+ (for HTTP requests)
<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>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'
}- Sign up: Visit shopsavvy.com/data
- Choose plan: Select based on your usage needs
- Get API key: Copy from your dashboard
- Test: Run the 30-second example above
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);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());
}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");
}
}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...
}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());
}
}
}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*/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()));
}
}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
}
}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
}
}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
);
}# 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 publishToMavenLocalimportorg.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.");
}
}All models are implemented as Java POJOs with Jackson annotations and null-safety:
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 + '\'' +
'}';
}
}@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 + '\'' +
'}';
}
}@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()) +
'}';
}
}- ShopSavvy Data API Documentation - Complete API reference
- API Dashboard - Manage your API keys and usage
- GitHub Repository - Source code and issues
- Maven Central - Package releases
- Spring Boot Documentation - Spring Boot framework guide
- Java Documentation - Java language reference
- Support - Get help from our team
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
This project is licensed under the MIT License - see the LICENSE file for details.
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.
- 🛒 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
- ✅ 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
- 🚀 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 key • Need help?Contact us