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.
<!-- Add to pom.xml: -->
<!-- <dependency>
<groupId>com.shopsavvy</groupId>
<artifactId>shopsavvy-sdk-java</artifactId>
<version>1.0.0</version>
</dependency> -->
// Use in Java/Spring Boot:
import com.shopsavvy.sdk.*;
import com.shopsavvy.sdk.models.*;
public class QuickExample {
public static void main(String[] args) {
ShopSavvyClient client = new ShopSavvyClient("ss_live_your_api_key_here");
try {
ApiResponse<ProductDetails> product = client.getProductDetails("012345678901");
ApiResponse<List<Offer>> offers = client.getCurrentOffers("012345678901");
Offer bestOffer = 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 (ShopSavvyApiException e) {
System.err.println("Error: " + e.getMessage());
} finally {
client.close();
}
}
}| Feature | Free Tier | Pro | Enterprise |
|---|---|---|---|
| API Calls/Month | 1,000 | 100,000 | Unlimited |
| Product Details | β | β | β |
| Real-time Pricing | β | β | β |
| Price History | 30 days | 1 year | 5+ years |
| Bulk Operations | 10/batch | 100/batch | 1000/batch |
| Retailer Coverage | 50+ | 500+ | 1000+ |
| Rate Limiting | 60/hour | 1000/hour | Custom |
| Support | Community | Phone + Dedicated |
- 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
import com.shopsavvy.sdk.*;
import com.shopsavvy.sdk.exceptions.*;
// Basic configuration
ShopSavvyClient client = new ShopSavvyClient("ss_live_your_api_key_here");
// Advanced configuration
ShopSavvyClient client = new ShopSavvyClient.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 configuration
String apiKey = System.getenv("SHOPSAVVY_API_KEY");
if (apiKey == null) {
throw new IllegalStateException("SHOPSAVVY_API_KEY environment variable not set");
}
ShopSavvyClient client = new ShopSavvyClient(apiKey);import com.shopsavvy.sdk.models.*;
// Look up by barcode, ASIN, URL, model number, or ShopSavvy ID
ApiResponse<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 number
ProductDetails productData = 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());
}import java.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 (int i = 0; i < identifiers.size(); i++) {
ProductDetails product = 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");
}
}import org.springframework.web.bind.annotation.*;
import org.springframework.stereotype.Service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.cache.annotation.Cacheable;
@RestController
@RequestMapping("/api/v1/products")
public class ProductController {
@Autowired
private ProductService productService;
@GetMapping("/{identifier}/compare")
public ResponseEntity<PriceComparisonResponse> comparePrice(@PathVariable String identifier) {
try {
PriceComparisonResponse comparison = productService.comparePrice(identifier);
return ResponseEntity.ok(comparison);
} catch (ShopSavvyApiException e) {
return ResponseEntity.status(500).body(null);
}
}
@GetMapping("/{identifier}/history")
public ResponseEntity<List<OfferWithHistory>> getPriceHistory(
@PathVariable String identifier,
@RequestParam String startDate,
@RequestParam String endDate) {
try {
List<OfferWithHistory> history = productService.getPriceHistory(identifier, startDate, endDate);
return ResponseEntity.ok(history);
} catch (ShopSavvyApiException e) {
return ResponseEntity.status(500).body(null);
}
}
@PostMapping("/{identifier}/alerts")
public ResponseEntity<String> setPriceAlert(
@PathVariable String identifier,
@RequestBody PriceAlertRequest request) {
try {
productService.setPriceAlert(identifier, request.getTargetPrice(), request.getEmail());
return ResponseEntity.ok("Price alert set successfully");
} catch (ShopSavvyApiException e) {
return ResponseEntity.status(500).body("Failed to set price alert");
}
}
}
@Service
public class ProductService {
private final ShopSavvyClient client;
private final EmailService emailService;
public ProductService(ShopSavvyClient client, EmailService emailService) {
this.client = client;
this.emailService = emailService;
}
@Cacheable(value = "priceComparisons", key = "#identifier")
public PriceComparisonResponse comparePrice(String identifier) throws ShopSavvyApiException {
ApiResponse<List<Offer>> response = client.getCurrentOffers(identifier);
List<Offer> offers = response.getData();
if (offers.isEmpty()) {
throw new ShopSavvyNotFoundException("No offers found for product: " + identifier);
}
// Sort by price
List<Offer> sortedOffers = offers.stream()
.filter(offer -> offer.getPrice() != null)
.sorted((o1, o2) -> Double.compare(o1.getPrice(), o2.getPrice()))
.collect(Collectors.toList());
Offer cheapest = sortedOffers.get(0);
Offer mostExpensive = sortedOffers.get(sortedOffers.size() - 1);
double averagePrice = sortedOffers.stream()
.mapToDouble(Offer::getPrice)
.average()
.orElse(0.0);
double potentialSavings = mostExpensive.getPrice() - cheapest.getPrice();
// Filter by availability and condition
long inStockCount = offers.stream()
.filter(offer -> "in_stock".equals(offer.getAvailability()))
.count();
long newConditionCount = offers.stream()
.filter(offer -> "new".equals(offer.getCondition()))
.count();
return new PriceComparisonResponse(
sortedOffers,
cheapest,
mostExpensive,
averagePrice,
potentialSavings,
(int) inStockCount,
(int) newConditionCount
);
}
public List<OfferWithHistory> getPriceHistory(String identifier, String startDate, String endDate) throws ShopSavvyApiException {
ApiResponse<List<OfferWithHistory>> response = client.getPriceHistory(identifier, startDate, endDate);
return response.getData();
}
public void setPriceAlert(String identifier, double targetPrice, String email) throws ShopSavvyApiException {
// Schedule monitoring with API
client.scheduleProductMonitoring(identifier, "daily");
// Set up local alert tracking (you would implement this with your database)
PriceAlert alert = new PriceAlert(identifier, targetPrice, email, true);
// saveToDatabase(alert);
// Send confirmation email
emailService.sendPriceAlertConfirmation(email, identifier, targetPrice);
}
}
// Response DTOs
public class PriceComparisonResponse {
private List<Offer> offers;
private Offer bestOffer;
private Offer worstOffer;
private double averagePrice;
private double potentialSavings;
private int inStockOffers;
private int newConditionOffers;
// Constructor and getters/setters
public PriceComparisonResponse(List<Offer> offers, Offer bestOffer, Offer worstOffer,
double averagePrice, double potentialSavings,
int inStockOffers, int newConditionOffers) {
this.offers = offers;
this.bestOffer = bestOffer;
this.worstOffer = worstOffer;
this.averagePrice = averagePrice;
this.potentialSavings = potentialSavings;
this.inStockOffers = inStockOffers;
this.newConditionOffers = newConditionOffers;
}
// Getters and setters...
}
public class PriceAlertRequest {
private double targetPrice;
private String email;
// Getters and setters...
}import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import java.util.concurrent.*;
import java.util.List;
@Component
public class PriceMonitoringService {
private final ShopSavvyClient client;
private final PriceAlertRepository alertRepository;
private final NotificationService notificationService;
private final ExecutorService executorService;
public PriceMonitoringService(ShopSavvyClient client,
PriceAlertRepository alertRepository,
NotificationService notificationService) {
this.client = client;
this.alertRepository = alertRepository;
this.notificationService = notificationService;
this.executorService = Executors.newFixedThreadPool(10);
}
@Scheduled(fixedRate = 3600000) // Run every hour
@Transactional
public void monitorPrices() {
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 complete
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]))
.thenRun(() -> System.out.println("Completed monitoring " + activeAlerts.size() + " price alerts"))
.exceptionally(throwable -> {
System.err.println("Error during price monitoring: " + throwable.getMessage());
return null;
});
}
private void checkPriceAlert(PriceAlert alert) {
try {
ApiResponse<List<Offer>> response = client.getCurrentOffers(alert.getProductId());
List<Offer> offers = response.getData();
Offer bestOffer = 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 notification
notificationService.sendPriceAlert(alert, bestOffer);
// Update alert
alert.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 history
savePriceHistory(alert.getProductId(), bestOffer);
// Rate limiting
Thread.sleep(1000);
} catch (ShopSavvyApiException e) {
System.err.println("Failed to check price for " + alert.getProductId() + ": " + e.getMessage());
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
private void savePriceHistory(String productId, Offer offer) {
if (offer != null) {
PriceHistory history = new PriceHistory(
productId,
offer.getRetailer(),
offer.getPrice(),
Instant.now()
);
// Save to database
// priceHistoryRepository.save(history);
}
}
}
// Notification Service
@Service
public class NotificationService {
@Autowired
private JavaMailSender mailSender;
public void sendPriceAlert(PriceAlert alert, Offer offer) {
try {
SimpleMailMessage message = new SimpleMailMessage();
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 (Exception e) {
System.err.println("Failed to send price alert email: " + e.getMessage());
}
}
}import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.retry.annotation.EnableRetry;
@Configuration
@EnableCaching
@EnableScheduling
@EnableRetry
@ConfigurationProperties(prefix = "shopsavvy")
public class ShopSavvyConfig {
private String apiKey;
private String baseUrl = "https://api.shopsavvy.com/v1";
private int timeoutSeconds = 60;
private int retryAttempts = 3;
private int connectionPoolSize = 20;
@Bean
public ShopSavvyClient shopSavvyClient() {
return new ShopSavvyClient.Builder()
.apiKey(apiKey)
.baseUrl(baseUrl)
.timeoutSeconds(timeoutSeconds)
.retryAttempts(retryAttempts)
.connectionPoolSize(connectionPoolSize)
.build();
}
// Getters and setters for configuration properties
public String getApiKey() { return apiKey; }
public void setApiKey(String apiKey) { this.apiKey = apiKey; }
public String getBaseUrl() { return baseUrl; }
public void setBaseUrl(String baseUrl) { this.baseUrl = baseUrl; }
public int getTimeoutSeconds() { return timeoutSeconds; }
public void setTimeoutSeconds(int timeoutSeconds) { this.timeoutSeconds = timeoutSeconds; }
public int getRetryAttempts() { return retryAttempts; }
public void setRetryAttempts(int retryAttempts) { this.retryAttempts = retryAttempts; }
public int getConnectionPoolSize() { return connectionPoolSize; }
public void setConnectionPoolSize(int connectionPoolSize) { 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
*/import io.reactivex.rxjava3.core.*;
import io.reactivex.rxjava3.schedulers.Schedulers;
import java.time.Duration;
import java.util.concurrent.TimeUnit;
@Service
public class ReactiveProductService {
private final ShopSavvyClient client;
public ReactiveProductService(ShopSavvyClient client) {
this.client = client;
}
public Observable<PriceUpdate> monitorPriceChanges(String productId, Duration interval) {
return Observable.interval(interval.toSeconds(), TimeUnit.SECONDS)
.map(tick -> getCurrentPrice(productId))
.filter(priceUpdate -> priceUpdate != null)
.distinctUntilChanged(PriceUpdate::getPrice)
.observeOn(Schedulers.io());
}
public Single<List<ProductDetails>> searchProductsReactive(List<String> identifiers) {
return Single.fromCallable(() -> {
try {
ApiResponse<List<ProductDetails>> response = client.getProductDetailsBatch(identifiers);
return response.getData();
} catch (ShopSavvyApiException e) {
throw new RuntimeException(e);
}
}).subscribeOn(Schedulers.io());
}
public Observable<PriceComparison> streamPriceComparisons(List<String> productIds) {
return Observable.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());
return Observable.empty();
})
);
}
private PriceUpdate getCurrentPrice(String productId) {
try {
ApiResponse<List<Offer>> response = client.getCurrentOffers(productId);
Offer bestOffer = response.getData().stream()
.min((o1, o2) -> Double.compare(o1.getPrice(), o2.getPrice()))
.orElse(null);
if (bestOffer != null) {
return new PriceUpdate(productId, bestOffer.getPrice(), bestOffer.getRetailer(), Instant.now());
}
} catch (ShopSavvyApiException e) {
System.err.println("Error getting price for " + productId + ": " + e.getMessage());
}
return null;
}
private PriceComparison comparePrice(String productId) throws ShopSavvyApiException {
// Implementation similar to previous example
ApiResponse<List<Offer>> response = client.getCurrentOffers(productId);
// ... rest of implementation
return new PriceComparison(/* ... */);
}
}
// Usage in controller
@RestController
public class ReactiveProductController {
@Autowired
private ReactiveProductService productService;
@GetMapping(value = "/api/v1/products/{id}/price-stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Observable<PriceUpdate> streamPriceUpdates(@PathVariable String id) {
return productService.monitorPriceChanges(id, Duration.ofMinutes(5))
.doOnNext(update -> System.out.println("Price update: " + update))
.doOnError(error -> System.err.println("Stream error: " + error.getMessage()));
}
}import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.*;
// Feign client for microservices communication
@FeignClient(name = "product-service", url = "${product-service.url}")
public interface ProductServiceClient {
@GetMapping("/api/v1/products/{id}")
ResponseEntity<ProductDetails> getProduct(@PathVariable String id);
@GetMapping("/api/v1/products/{id}/offers")
ResponseEntity<List<Offer>> getOffers(@PathVariable String id);
}
// Circuit breaker integration
@Component
public class ProductServiceFallback {
private final ShopSavvyClient client;
public ProductServiceFallback(ShopSavvyClient client) {
this.client = client;
}
@CircuitBreaker(name = "product-service", fallbackMethod = "fallbackGetProduct")
public ProductDetails getProductWithFallback(String productId) throws ShopSavvyApiException {
// Primary service call
return callPrimaryProductService(productId);
}
public ProductDetails fallbackGetProduct(String productId, Exception ex) {
try {
// Fallback to ShopSavvy API
ApiResponse<ProductDetails> response = client.getProductDetails(productId);
return response.getData();
} catch (ShopSavvyApiException e) {
throw new RuntimeException("Both primary service and fallback failed", e);
}
}
private ProductDetails callPrimaryProductService(String productId) {
// Implementation to call primary service
return null; // placeholder
}
}The SDK provides comprehensive exception handling with Java-specific patterns:
import com.shopsavvy.sdk.exceptions.*;
public void handleProductLookup(String identifier) {
try {
ApiResponse<ProductDetails> response = client.getProductDetails(identifier);
System.out.println("β
Found product: " + response.getData().getName());
} catch (ShopSavvyAuthenticationException e) {
System.err.println("π Authentication failed: " + e.getMessage());
// Refresh API key or redirect to login
} catch (ShopSavvyNotFoundException e) {
System.err.println("β Product not found: " + e.getMessage());
// Show "not found" UI
} catch (ShopSavvyValidationException e) {
System.err.println("β οΈ Invalid parameters: " + e.getMessage());
// Show validation error to user
} catch (ShopSavvyRateLimitException e) {
System.err.println("π¦ Rate limit exceeded: " + e.getMessage());
// Implement exponential backoff
try {
Thread.sleep(e.getRetryAfterSeconds() * 1000L);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
}
} catch (ShopSavvyNetworkException e) {
System.err.println("π Network error: " + e.getMessage());
// Show offline mode or retry option
} catch (ShopSavvyApiException e) {
System.err.println("β API error: " + e.getMessage());
// Generic error handling
} catch (Exception e) {
System.err.println("π₯ Unexpected error: " + e.getMessage());
// Log to crash reporting service
}
}import java.time.Duration;
import java.util.function.Supplier;
public class RetryUtil {
public static <T> T retryWithBackoff(Supplier<T> operation,
int maxAttempts,
Duration initialDelay,
Duration maxDelay,
double multiplier) throws Exception {
Exception lastException = null;
Duration currentDelay = initialDelay;
for (int attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return operation.get();
} catch (ShopSavvyRateLimitException e) {
lastException = e;
if (attempt == maxAttempts) break;
// Use server-specified retry delay if available
Duration retryDelay = e.getRetryAfterSeconds() != null
? Duration.ofSeconds(e.getRetryAfterSeconds())
: currentDelay;
try {
Thread.sleep(retryDelay.toMillis());
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
throw new RuntimeException(ie);
}
currentDelay = Duration.ofMillis(Math.min(
(long) (currentDelay.toMillis() * multiplier),
maxDelay.toMillis()
));
} catch (ShopSavvyNetworkException e) {
lastException = e;
if (attempt == maxAttempts) break;
try {
Thread.sleep(currentDelay.toMillis());
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
throw new RuntimeException(ie);
}
currentDelay = Duration.ofMillis(Math.min(
(long) (currentDelay.toMillis() * multiplier),
maxDelay.toMillis()
));
}
}
throw new RuntimeException("Operation failed after " + maxAttempts + " attempts", lastException);
}
}
// Usage
public ProductDetails getProductWithRetry(String identifier) throws Exception {
return RetryUtil.retryWithBackoff(
() -> {
try {
return client.getProductDetails(identifier).getData();
} catch (ShopSavvyApiException e) {
throw new RuntimeException(e);
}
},
3, // max attempts
Duration.ofSeconds(1), // initial delay
Duration.ofSeconds(30), // max delay
2.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 publishToMavenLocalimport org.junit.jupiter.api.Test;
import org.junit.jupiter.api.BeforeEach;
import static org.junit.jupiter.api.Assertions.*;
public class SDKIntegrationTest {
private ShopSavvyClient client;
@BeforeEach
void setUp() {
client = new ShopSavvyClient("ss_test_your_test_key_here");
}
@Test
void testProductLookup() {
try {
ApiResponse<ProductDetails> response = client.getProductDetails("012345678901");
assertNotNull(response.getData());
assertNotNull(response.getData().getName());
System.out.println("β
Product lookup: " + response.getData().getName());
} catch (ShopSavvyApiException e) {
fail("Product lookup failed: " + e.getMessage());
}
}
@Test
void testCurrentOffers() {
try {
ApiResponse<List<Offer>> response = client.getCurrentOffers("012345678901");
assertNotNull(response.getData());
System.out.println("β
Current offers: " + response.getData().size() + " found");
} catch (ShopSavvyApiException e) {
fail("Current offers failed: " + e.getMessage());
}
}
@Test
void testUsageInfo() {
try {
ApiResponse<UsageInfo> response = client.getUsage();
assertNotNull(response.getData());
System.out.println("β
API usage: " + response.getData().getCreditsRemaining() + " credits remaining");
} catch (ShopSavvyApiException e) {
fail("Usage info failed: " + e.getMessage());
}
}
@AfterEach
void tearDown() {
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:
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.NotNull;
@JsonIgnoreProperties(ignoreUnknown = true)
public class ProductDetails {
@JsonProperty("id")
@NotNull
private String id;
@JsonProperty("name")
@NotNull
private String name;
@JsonProperty("description")
@Nullable
private String description;
@JsonProperty("brand")
@Nullable
private String brand;
@JsonProperty("category")
@Nullable
private String category;
@JsonProperty("upc")
@Nullable
private String upc;
@JsonProperty("asin")
@Nullable
private String asin;
@JsonProperty("model_number")
@Nullable
private String modelNumber;
@JsonProperty("images")
@NotNull
private List<String> images = new ArrayList<>();
@JsonProperty("specifications")
@NotNull
private Map<String, String> specifications = new HashMap<>();
@JsonProperty("created_at")
@Nullable
private String createdAt;
@JsonProperty("updated_at")
@Nullable
private String updatedAt;
// Constructors
public ProductDetails() {}
public ProductDetails(@NotNull String id, @NotNull String name) {
this.id = id;
this.name = name;
}
// Computed properties for convenience
public boolean hasImages() {
return images != null && !images.isEmpty();
}
public boolean hasSpecifications() {
return specifications != null && !specifications.isEmpty();
}
public String getDisplayName() {
if (brand != null && !brand.trim().isEmpty()) {
return brand.trim() + " " + name.trim();
}
return name.trim();
}
// Standard getters and setters
@NotNull
public String getId() { return id; }
public void setId(@NotNull String id) { this.id = id; }
@NotNull
public String getName() { return name; }
public void setName(@NotNull String name) { this.name = name; }
@Nullable
public String getDescription() { return description; }
public void setDescription(@Nullable String description) { this.description = description; }
@Nullable
public String getBrand() { return brand; }
public void setBrand(@Nullable String brand) { this.brand = brand; }
@Nullable
public String getCategory() { return category; }
public void setCategory(@Nullable String category) { this.category = category; }
@Nullable
public String getUpc() { return upc; }
public void setUpc(@Nullable String upc) { this.upc = upc; }
@Nullable
public String getAsin() { return asin; }
public void setAsin(@Nullable String asin) { this.asin = asin; }
@Nullable
public String getModelNumber() { return modelNumber; }
public void setModelNumber(@Nullable String modelNumber) { this.modelNumber = modelNumber; }
@NotNull
public List<String> getImages() { return images; }
public void setImages(@NotNull List<String> images) { this.images = images; }
@NotNull
public Map<String, String> getSpecifications() { return specifications; }
public void setSpecifications(@NotNull Map<String, String> specifications) { this.specifications = specifications; }
@Nullable
public String getCreatedAt() { return createdAt; }
public void setCreatedAt(@Nullable String createdAt) { this.createdAt = createdAt; }
@Nullable
public String getUpdatedAt() { return updatedAt; }
public void setUpdatedAt(@Nullable String updatedAt) { this.updatedAt = updatedAt; }
@Override
public String toString() {
return "ProductDetails{" +
"id='" + id + '\'' +
", name='" + name + '\'' +
", brand='" + brand + '\'' +
", category='" + category + '\'' +
'}';
}
}@JsonIgnoreProperties(ignoreUnknown = true)
public class Offer {
@JsonProperty("retailer")
@NotNull
private String retailer;
@JsonProperty("price")
@Nullable
private Double price;
@JsonProperty("currency")
@Nullable
private String currency = "USD";
@JsonProperty("availability")
@Nullable
private String availability;
@JsonProperty("condition")
@Nullable
private String condition;
@JsonProperty("shipping_cost")
@Nullable
private Double shippingCost;
@JsonProperty("url")
@Nullable
private String url;
@JsonProperty("last_updated")
@Nullable
private String lastUpdated;
// Constructors
public Offer() {}
public Offer(@NotNull String retailer, @Nullable Double price) {
this.retailer = retailer;
this.price = price;
}
// Computed properties
public boolean isInStock() {
return "in_stock".equals(availability);
}
public boolean isNew() {
return "new".equals(condition);
}
public double getTotalCost() {
double basePrice = price != null ? price : 0.0;
double shipping = shippingCost != null ? shippingCost : 0.0;
return basePrice + shipping;
}
public String getFormattedPrice() {
if (price == null) return "N/A";
return String.format("$%.2f", price);
}
// Standard getters and setters
@NotNull
public String getRetailer() { return retailer; }
public void setRetailer(@NotNull String retailer) { this.retailer = retailer; }
@Nullable
public Double getPrice() { return price; }
public void setPrice(@Nullable Double price) { this.price = price; }
@Nullable
public String getCurrency() { return currency; }
public void setCurrency(@Nullable String currency) { this.currency = currency; }
@Nullable
public String getAvailability() { return availability; }
public void setAvailability(@Nullable String availability) { this.availability = availability; }
@Nullable
public String getCondition() { return condition; }
public void setCondition(@Nullable String condition) { this.condition = condition; }
@Nullable
public Double getShippingCost() { return shippingCost; }
public void setShippingCost(@Nullable Double shippingCost) { this.shippingCost = shippingCost; }
@Nullable
public String getUrl() { return url; }
public void setUrl(@Nullable String url) { this.url = url; }
@Nullable
public String getLastUpdated() { return lastUpdated; }
public void setLastUpdated(@Nullable String lastUpdated) { this.lastUpdated = lastUpdated; }
@Override
public String toString() {
return "Offer{" +
"retailer='" + retailer + '\'' +
", price=" + price +
", availability='" + availability + '\'' +
", condition='" + condition + '\'' +
'}';
}
}@JsonIgnoreProperties(ignoreUnknown = true)
public class UsageInfo {
@JsonProperty("credits_used")
@Nullable
private Integer creditsUsed;
@JsonProperty("credits_remaining")
@Nullable
private Integer creditsRemaining;
@JsonProperty("credits_limit")
@Nullable
private Integer creditsLimit;
@JsonProperty("reset_date")
@Nullable
private String resetDate;
@JsonProperty("current_period_start")
@Nullable
private String currentPeriodStart;
@JsonProperty("current_period_end")
@Nullable
private String currentPeriodEnd;
// Constructors
public UsageInfo() {}
// Computed properties
public double getUsagePercentage() {
int used = creditsUsed != null ? creditsUsed : 0;
int limit = creditsLimit != null ? creditsLimit : 1;
return ((double) used / limit) * 100.0;
}
public boolean isNearLimit() {
return getUsagePercentage() > 80.0;
}
// Standard getters and setters
@Nullable
public Integer getCreditsUsed() { return creditsUsed; }
public void setCreditsUsed(@Nullable Integer creditsUsed) { this.creditsUsed = creditsUsed; }
@Nullable
public Integer getCreditsRemaining() { return creditsRemaining; }
public void setCreditsRemaining(@Nullable Integer creditsRemaining) { this.creditsRemaining = creditsRemaining; }
@Nullable
public Integer getCreditsLimit() { return creditsLimit; }
public void setCreditsLimit(@Nullable Integer creditsLimit) { this.creditsLimit = creditsLimit; }
@Nullable
public String getResetDate() { return resetDate; }
public void setResetDate(@Nullable String resetDate) { this.resetDate = resetDate; }
@Nullable
public String getCurrentPeriodStart() { return currentPeriodStart; }
public void setCurrentPeriodStart(@Nullable String currentPeriodStart) { this.currentPeriodStart = currentPeriodStart; }
@Nullable
public String getCurrentPeriodEnd() { return currentPeriodEnd; }
public void setCurrentPeriodEnd(@Nullable String currentPeriodEnd) { this.currentPeriodEnd = currentPeriodEnd; }
@Override
public String toString() {
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