Official Rust SDK for the ShopSavvy Data API. Access comprehensive product data, real-time pricing, and historical price trends across tens of thousands of retailers and 30M+ products. Built for high-performance, async/await, and zero-cost abstractions.
# Cargo.toml
[dependencies]
shopsavvy-sdk = "1.4"
tokio = { version = "1.0", features = ["full"] }use shopsavvy_sdk::{Client, Result};
#[tokio::main]
async fn main() -> Result<()> {
let client = Client::new("ss_live_your_api_key_here")?;
let product = client.get_product_details("012345678901").await?;
let offers = client.get_current_offers("012345678901").await?;
let best_offer = offers.data.iter().min_by(|a, b| a.price.total_cmp(&b.price))?;
println!("{} - Best price: ${:.2} at {}", product.data.name, best_offer.price, best_offer.retailer);
Ok(())
}- Rust 1.70+ (2021 edition)
- Tokio runtime for async operations
- TLS support (automatic with
reqwest)
[dependencies]
shopsavvy-sdk = "1.4"
tokio = { version = "1.0", features = ["full"] }
serde = { version = "1.0", features = ["derive"] } # For custom serialization[dependencies]
shopsavvy-sdk = { version = "1.4", features = ["metrics", "tracing"] }- 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
use shopsavvy_sdk::{Client, Config, Result};
use std::time::Duration;
use std::env;
// Basic configuration
let client = Client::new("ss_live_your_api_key_here")?;
// Advanced configuration
let config = Config::builder()
.api_key("ss_live_your_api_key_here")
.base_url("https://api.shopsavvy.com/v1")
.timeout(Duration::from_secs(60))
.max_retries(3)
.user_agent("MyApp/1.0.0")
.build()?;
let client = Client::with_config(config)?;
// Environment variable configuration
let api_key = env::var("SHOPSAVVY_API_KEY")?;
let client = Client::new(&api_key)?;use shopsavvy_sdk::{Client, Result};
async fn lookup_product() -> Result<()> {
let client = Client::new("ss_live_your_api_key_here")?;
// Look up by barcode, ASIN, URL, model number, or ShopSavvy ID
let product = client.get_product_details("012345678901").await?;
let amazon_product = client.get_product_details("B08N5WRWNW").await?;
let url_product = client.get_product_details("https://www.amazon.com/dp/B08N5WRWNW").await?;
let model_product = client.get_product_details("MQ023LL/A").await?; // iPhone model number
println!("π¦ Product: {}", product.data.name);
println!("π·οΈ Brand: {}", product.data.brand.as_deref().unwrap_or("N/A"));
println!("π Category: {}", product.data.category.as_deref().unwrap_or("N/A"));
println!("π’ Product ID: {}", product.data.id);
if let Some(asin) = &product.data.asin {
println!("π¦ ASIN: {}", asin);
}
if let Some(model) = &product.data.model_number {
println!("π§ Model: {}", model);
}
Ok(())
}use shopsavvy_sdk::{Client, Result};
use futures::future;
use std::collections::HashMap;
async fn lookup_multiple_products() -> Result<()> {
let client = Client::new("ss_live_your_api_key_here")?;
// Process up to 100 products per request
let identifiers = vec![
"012345678901".to_string(),
"B08N5WRWNW".to_string(),
"045496590048".to_string(),
"https://www.bestbuy.com/site/product/123456".to_string(),
"MQ023LL/A".to_string(), // iPhone model number
"SM-S911U".to_string(), // Samsung model number
];
// Method 1: Batch API (more efficient)
let products = client.get_product_details_batch(&identifiers).await?;
for (i, product) in products.data.iter().enumerate() {
match product {
Some(product) => {
println!("β Found: {} by {}",
product.name,
product.brand.as_deref().unwrap_or("Unknown")
);
}
None => {
println!("β Failed to find product: {}", identifiers[i]);
}
}
}
// Method 2: Concurrent individual requests (for custom logic)
let futures: Vec<_> = identifiers.iter()
.map(|id| client.get_product_details(id))
.collect();
let results = future::join_all(futures).await;
let mut product_map = HashMap::new();
for (id, result) in identifiers.iter().zip(results.iter()) {
match result {
Ok(product) => {
product_map.insert(id.clone(), product.data.clone());
println!("β Concurrent lookup: {}", product.data.name);
}
Err(e) => {
eprintln!("β Error for {}: {}", id, e);
}
}
}
Ok(())
}use shopsavvy_sdk::{Client, Result};
use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
struct PriceAnalysis {
best_price: f64,
worst_price: f64,
average_price: f64,
median_price: f64,
price_variance: f64,
total_offers: usize,
in_stock_offers: usize,
new_condition_offers: usize,
retailer_rankings: BTreeMap<String, f64>,
}
async fn analyze_offers(client: &Client, identifier: &str) -> Result<PriceAnalysis> {
let response = client.get_current_offers(identifier).await?;
let offers = &response.data;
println!("Found {} offers across retailers", offers.len());
// Filter valid prices and sort
let valid_offers: Vec<_> = offers.iter()
.filter(|offer| offer.price > 0.0)
.collect();
if valid_offers.is_empty() {
return Err("No valid offers found".into());
}
let mut prices: Vec<f64> = valid_offers.iter().map(|offer| offer.price).collect();
prices.sort_by(|a, b| a.total_cmp(b));
let best_price = prices[0];
let worst_price = prices[prices.len() - 1];
let average_price = prices.iter().sum::<f64>() / prices.len() as f64;
// Calculate median
let median_price = if prices.len() % 2 == 0 {
(prices[prices.len() / 2 - 1] + prices[prices.len() / 2]) / 2.0
} else {
prices[prices.len() / 2]
};
// Calculate variance
let variance = prices.iter()
.map(|price| (price - average_price).powi(2))
.sum::<f64>() / prices.len() as f64;
println!("π° Best price: ${:.2}", best_price);
println!("πΈ Highest price: ${:.2}", worst_price);
println!("π Average price: ${:.2}", average_price);
println!("π Median price: ${:.2}", median_price);
println!("π‘ Potential savings: ${:.2}", worst_price - best_price);
// Additional analysis
let in_stock_offers = offers.iter()
.filter(|offer| offer.is_in_stock()) // availability == "in"
.count();
let new_condition_offers = offers.iter()
.filter(|offer| offer.condition.as_deref() == Some("new"))
.count();
println!("β
In-stock offers: {}", in_stock_offers);
println!("π New condition: {}", new_condition_offers);
// Retailer rankings
let mut retailer_rankings = BTreeMap::new();
for offer in valid_offers {
retailer_rankings.insert(offer.retailer.clone(), offer.price);
}
Ok(PriceAnalysis {
best_price,
worst_price,
average_price,
median_price,
price_variance: variance,
total_offers: offers.len(),
in_stock_offers,
new_condition_offers,
retailer_rankings,
})
}use shopsavvy_sdk::{Client, Result};
use std::collections::HashMap;
use futures::future;
async fn compare_retailer_prices(client: &Client, identifier: &str) -> Result<()> {
let retailers = vec!["amazon", "walmart", "target", "bestbuy"];
// Concurrent retailer queries
let futures: Vec<_> = retailers.iter()
.map(|retailer| client.get_current_offers_with_retailer(identifier, retailer))
.collect();
let results = future::join_all(futures).await;
let mut retailer_prices = HashMap::new();
for (retailer, result) in retailers.iter().zip(results.iter()) {
match result {
Ok(offers) if !offers.data.is_empty() => {
let best_price = offers.data.iter()
.filter(|offer| offer.price > 0.0)
.map(|offer| offer.price)
.fold(f64::INFINITY, f64::min);
if best_price != f64::INFINITY {
retailer_prices.insert(retailer.to_string(), best_price);
}
}
Ok(_) => {
println!("No offers found for {}", retailer);
}
Err(e) => {
eprintln!("Error fetching {} prices: {}", retailer, e);
}
}
}
// Sort by price
let mut sorted_prices: Vec<_> = retailer_prices.iter().collect();
sorted_prices.sort_by(|a, b| a.1.total_cmp(b.1));
println!("Retailer price comparison:");
for (retailer, price) in sorted_prices {
println!(" {}: ${:.2}",
retailer.chars().next().unwrap().to_uppercase().collect::<String>()
+ &retailer[1..],
price
);
}
Ok(())
}get_price_history returns one entry per product that resolved. Each carries the
product's fields (product.title, product.barcode, ...) and its offers; each offer
carries its history β newest point first, each with timestamp, price, an optional
currency (ISO 4217, None when the archived point recorded none) and an optional
availability. Dates are YYYY-MM-DD; the window may span at most 366 days.
use shopsavvy_sdk::{Client, Result};
#[tokio::main]
async fn main() -> Result<()> {
let client = Client::new("ss_live_your_api_key_here")?;
// Optionally pass Some("amazon.com") to restrict to one retailer.
let history = client
.get_price_history("012345678901", "2024-01-01", "2024-01-31", None, None)
.await?;
for product in &history.data {
println!("{}", product.product.title);
for offer in &product.offers {
let retailer = offer.retailer.as_deref().unwrap_or("unknown");
if let Some(low) = offer.history.iter().map(|p| p.price).reduce(f64::min) {
println!(" {retailer}: {} points, low ${low:.2}", offer.history.len());
}
for point in &offer.history {
println!(
" {} ${:.2} {} {}",
point.timestamp,
point.price,
point.currency.as_deref().unwrap_or("?"),
point.availability.as_deref().unwrap_or("")
);
}
}
}
println!("Credits used: {}", history.credits_used());
Ok(())
}Schedule products to be refreshed hourly, daily or weekly, optionally limited to one retailer domain. Credit costs for each call are in the API documentation.
use shopsavvy_sdk::{Client, MonitoringFrequency, Result};
async fn manage_schedule(client: &Client) -> Result<()> {
// `data` holds one ScheduledProduct per product found: the product's fields
// (flattened into `.product`) plus the schedule that was set and the retailer, if any.
let scheduled = client
.schedule_product_monitoring_batch(&["012345678901", "B07GV2S1GS"], MonitoringFrequency::Daily, Some("amazon.com"))
.await?;
for item in &scheduled.data {
println!("{} ({}) -> {:?}", item.product.title, item.product.shopsavvy, item.schedule);
}
println!("Credits used: {}", scheduled.credits_used());
// Everything scheduled under this API key, oldest first
let list = client.get_scheduled_products().await?;
for item in &list.data {
// `schedule` is None for an interval with no Data API label (e.g. set through
// ShopSavvy Business); `retailer` is None when refreshes cover every retailer.
println!(
"{}: {} at {}",
item.product.title,
item.schedule.as_deref().unwrap_or("custom interval"),
item.retailer.as_deref().unwrap_or("all retailers")
);
}
// Stop refreshing. The API returns success/message/meta and no data.
let removed = client.remove_product_from_schedule("012345678901").await?;
println!("{} {:?}", removed.success, removed.message);
Ok(())
}// Cargo.toml
[dependencies]
shopsavvy-sdk = "1.4"
axum = "0.7"
tokio = { version = "1.0", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
tower = "0.4"
tower-http = { version = "0.5", features = ["cors", "trace"] }
tracing = "0.1"
tracing-subscriber = "0.3"
// src/main.rs
use axum::{
extract::{Path, Query, State},
http::StatusCode,
response::Json,
routing::{get, post},
Router,
};
use serde::{Deserialize, Serialize};
use shopsavvy_sdk::{Client, Result as SdkResult};
use std::{collections::HashMap, sync::Arc};
use tower_http::cors::CorsLayer;
#[derive(Clone)]
struct AppState {
client: Arc<Client>,
}
#[derive(Deserialize)]
struct OffersQuery {
retailer: Option<String>,
sort_by: Option<String>,
}
#[derive(Serialize)]
struct ApiResponse<T> {
success: bool,
data: Option<T>,
error: Option<String>,
credits_remaining: Option<i32>,
}
impl<T> ApiResponse<T> {
fn ok(data: T, credits: Option<i32>) -> Self {
Self {
success: true,
data: Some(data),
error: None,
credits_remaining: credits,
}
}
fn error(message: String) -> Self {
Self {
success: false,
data: None,
error: Some(message),
credits_remaining: None,
}
}
}
async fn get_product_offers(
State(state): State<AppState>,
Path(identifier): Path<String>,
Query(params): Query<OffersQuery>,
) -> Result<Json<ApiResponse<serde_json::Value>>, StatusCode> {
match state.client.get_current_offers(&identifier).await {
Ok(response) => {
let mut offers = response.data;
// Filter by retailer if specified
if let Some(retailer) = ¶ms.retailer {
offers.retain(|offer| offer.retailer.to_lowercase() == retailer.to_lowercase());
}
// Sort offers
match params.sort_by.as_deref() {
Some("price") => offers.sort_by(|a, b| a.price.total_cmp(&b.price)),
Some("retailer") => offers.sort_by(|a, b| a.retailer.cmp(&b.retailer)),
_ => {} // Default order
}
let analysis = serde_json::json!({
"offers": offers,
"summary": {
"total_offers": offers.len(),
"best_price": offers.iter().map(|o| o.price).fold(f64::INFINITY, f64::min),
"average_price": offers.iter().map(|o| o.price).sum::<f64>() / offers.len() as f64,
"in_stock_count": offers.iter().filter(|o| o.is_in_stock()).count(),
}
});
Ok(Json(ApiResponse::ok(analysis, response.credits_remaining)))
}
Err(e) => {
eprintln!("Error fetching offers: {}", e);
Ok(Json(ApiResponse::error(e.to_string())))
}
}
}
async fn compare_products(
State(state): State<AppState>,
Json(identifiers): Json<Vec<String>>,
) -> Result<Json<ApiResponse<HashMap<String, serde_json::Value>>>, StatusCode> {
if identifiers.len() > 50 {
return Ok(Json(ApiResponse::error("Maximum 50 products allowed".to_string())));
}
let futures: Vec<_> = identifiers.iter()
.map(|id| async {
let product_result = state.client.get_product_details(id).await;
let offers_result = state.client.get_current_offers(id).await;
match (product_result, offers_result) {
(Ok(product), Ok(offers)) => {
let best_offer = offers.data.iter()
.min_by(|a, b| a.price.total_cmp(&b.price));
Some((id.clone(), serde_json::json!({
"product": product.data,
"best_offer": best_offer,
"total_offers": offers.data.len(),
})))
}
_ => None,
}
})
.collect();
let results = futures::future::join_all(futures).await;
let comparison: HashMap<String, serde_json::Value> = results
.into_iter()
.filter_map(|result| result)
.collect();
Ok(Json(ApiResponse::ok(comparison, None)))
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
tracing_subscriber::init();
let client = Client::new(&std::env::var("SHOPSAVVY_API_KEY")?)?;
let state = AppState {
client: Arc::new(client),
};
let app = Router::new()
.route("/products/:identifier/offers", get(get_product_offers))
.route("/products/compare", post(compare_products))
.layer(CorsLayer::permissive())
.with_state(state);
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?;
println!("π Server running on http://localhost:3000");
axum::serve(listener, app).await?;
Ok(())
}use shopsavvy_sdk::{Client, Result};
use tokio::{time::{sleep, Duration}, task, sync::mpsc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
struct PriceAlert {
product_id: String,
product_name: String,
target_price: f64,
current_price: f64,
retailer: String,
user_id: String,
}
#[derive(Debug, Clone)]
struct MonitoredProduct {
identifier: String,
target_price: f64,
user_id: String,
}
struct PriceMonitoringService {
client: Client,
products: Vec<MonitoredProduct>,
alert_sender: mpsc::UnboundedSender<PriceAlert>,
}
impl PriceMonitoringService {
fn new(client: Client) -> (Self, mpsc::UnboundedReceiver<PriceAlert>) {
let (alert_sender, alert_receiver) = mpsc::unbounded_channel();
(
Self {
client,
products: Vec::new(),
alert_sender,
},
alert_receiver,
)
}
fn add_product(&mut self, identifier: String, target_price: f64, user_id: String) {
self.products.push(MonitoredProduct {
identifier,
target_price,
user_id,
});
}
async fn start_monitoring(&self) -> Result<()> {
let mut interval = tokio::time::interval(Duration::from_secs(3600)); // Check hourly
loop {
interval.tick().await;
if let Err(e) = self.check_all_prices().await {
eprintln!("Error during price check: {}", e);
}
}
}
async fn check_all_prices(&self) -> Result<()> {
// Process products in batches to respect rate limits
const BATCH_SIZE: usize = 10;
for batch in self.products.chunks(BATCH_SIZE) {
let futures: Vec<_> = batch.iter()
.map(|product| self.check_product_price(product))
.collect();
let results = futures::future::join_all(futures).await;
for result in results {
if let Err(e) = result {
eprintln!("Price check error: {}", e);
}
}
// Rate limiting between batches
sleep(Duration::from_secs(1)).await;
}
Ok(())
}
async fn check_product_price(&self, product: &MonitoredProduct) -> Result<()> {
let offers = self.client.get_current_offers(&product.identifier).await?;
if let Some(best_offer) = offers.data.iter()
.filter(|offer| offer.price > 0.0)
.min_by(|a, b| a.price.total_cmp(&b.price))
{
if best_offer.price <= product.target_price {
// Get product details for the alert
if let Ok(product_details) = self.client.get_product_details(&product.identifier).await {
let alert = PriceAlert {
product_id: product.identifier.clone(),
product_name: product_details.data.name,
target_price: product.target_price,
current_price: best_offer.price,
retailer: best_offer.retailer.clone(),
user_id: product.user_id.clone(),
};
if let Err(e) = self.alert_sender.send(alert) {
eprintln!("Failed to send alert: {}", e);
}
}
}
}
Ok(())
}
}
async fn handle_alerts(mut alert_receiver: mpsc::UnboundedReceiver<PriceAlert>) {
while let Some(alert) = alert_receiver.recv().await {
println!("π¨ PRICE ALERT: {} is now ${:.2} at {} (target: ${:.2})",
alert.product_name,
alert.current_price,
alert.retailer,
alert.target_price
);
// Here you would:
// - Send email notification
// - Send push notification
// - Update database
// - Send webhook to external service
// Example: Send to webhook
if let Err(e) = send_webhook_alert(&alert).await {
eprintln!("Failed to send webhook: {}", e);
}
}
}
async fn send_webhook_alert(alert: &PriceAlert) -> Result<()> {
let webhook_url = std::env::var("WEBHOOK_URL")?;
let client = reqwest::Client::new();
let payload = serde_json::json!({
"type": "price_alert",
"data": alert,
"timestamp": chrono::Utc::now().to_rfc3339(),
});
client
.post(&webhook_url)
.json(&payload)
.send()
.await?;
Ok(())
}
#[tokio::main]
async fn main() -> Result<()> {
let client = Client::new(&std::env::var("SHOPSAVVY_API_KEY")?)?;
let (mut monitor, alert_receiver) = PriceMonitoringService::new(client);
// Add products to monitor
monitor.add_product("012345678901".to_string(), 99.99, "user123".to_string());
monitor.add_product("B08N5WRWNW".to_string(), 199.99, "user456".to_string());
// Start alert handler
let alert_handler = task::spawn(handle_alerts(alert_receiver));
// Start monitoring
let monitoring = task::spawn(async move {
if let Err(e) = monitor.start_monitoring().await {
eprintln!("Monitoring error: {}", e);
}
});
println!("π Price monitoring service started");
// Wait for either task to complete (they shouldn't in normal operation)
tokio::select! {
_ = alert_handler => println!("Alert handler stopped"),
_ = monitoring => println!("Monitoring stopped"),
}
Ok(())
}// Cargo.toml
[lib]
crate-type = ["cdylib"]
[dependencies]
shopsavvy-sdk = "1.4"
wasm-bindgen = "0.2"
wasm-bindgen-futures = "0.4"
js-sys = "0.3"
serde-wasm-bindgen = "0.6"
// src/lib.rs
use wasm_bindgen::prelude::*;
use shopsavvy_sdk::{Client, Result};
use serde::{Deserialize, Serialize};
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(js_namespace = console)]
fn log(s: &str);
#[wasm_bindgen(js_namespace = console, js_name = log)]
fn log_u32(a: u32);
}
macro_rules! console_log {
($($t:tt)*) => (log(&format_args!($($t)*).to_string()))
}
#[derive(Serialize, Deserialize)]
pub struct ProductComparison {
pub product_name: String,
pub best_price: f64,
pub best_retailer: String,
pub total_offers: usize,
pub savings_opportunity: f64,
}
#[wasm_bindgen]
pub struct ShopSavvyWasm {
client: Client,
}
#[wasm_bindgen]
impl ShopSavvyWasm {
#[wasm_bindgen(constructor)]
pub fn new(api_key: &str) -> Result<ShopSavvyWasm, JsValue> {
console_error_panic_hook::set_once();
let client = Client::new(api_key)
.map_err(|e| JsValue::from_str(&e.to_string()))?;
Ok(ShopSavvyWasm { client })
}
#[wasm_bindgen]
pub async fn compare_product_prices(
&self,
identifier: &str
) -> Result<JsValue, JsValue> {
let offers = self.client.get_current_offers(identifier)
.await
.map_err(|e| JsValue::from_str(&e.to_string()))?;
let product = self.client.get_product_details(identifier)
.await
.map_err(|e| JsValue::from_str(&e.to_string()))?;
if offers.data.is_empty() {
return Err(JsValue::from_str("No offers found"));
}
let best_offer = offers.data.iter()
.min_by(|a, b| a.price.total_cmp(&b.price))
.unwrap();
let worst_offer = offers.data.iter()
.max_by(|a, b| a.price.total_cmp(&b.price))
.unwrap();
let comparison = ProductComparison {
product_name: product.data.name,
best_price: best_offer.price,
best_retailer: best_offer.retailer.clone(),
total_offers: offers.data.len(),
savings_opportunity: worst_offer.price - best_offer.price,
};
Ok(serde_wasm_bindgen::to_value(&comparison)?)
}
}
// JavaScript usage:
// import init, { ShopSavvyWasm } from './pkg/my_wasm_app.js';
//
// async function run() {
// await init();
// const client = new ShopSavvyWasm('your_api_key');
// const comparison = await client.compare_product_prices('012345678901');
// console.log(comparison);
// }The SDK provides comprehensive error handling with Result types and custom error variants:
use shopsavvy_sdk::{Client, Error, Result};
async fn handle_product_lookup(identifier: &str) -> Result<()> {
let client = Client::new("ss_live_your_api_key_here")?;
match client.get_product_details(identifier).await {
Ok(product) => {
println!("β
Found product: {}", product.data.name);
}
Err(Error::Authentication { message, .. }) => {
eprintln!("π Authentication failed: {}", message);
// Redirect to login or refresh token
}
Err(Error::NotFound { message, .. }) => {
eprintln!("β Product not found: {}", message);
// Show "not found" UI
}
Err(Error::Validation { message, .. }) => {
eprintln!("β οΈ Invalid parameters: {}", message);
// Show validation error to user
}
Err(Error::RateLimit { message, retry_after, .. }) => {
eprintln!("π¦ Rate limit exceeded: {}", message);
if let Some(delay) = retry_after {
eprintln!("Retry after: {} seconds", delay);
}
// Implement exponential backoff
}
Err(Error::Network { message, .. }) => {
eprintln!("π Network error: {}", message);
// Show offline mode or retry option
}
Err(Error::Timeout { .. }) => {
eprintln!("β° Request timed out");
// Retry with exponential backoff
}
Err(e) => {
eprintln!("π₯ Unexpected error: {}", e);
// Log to crash reporting service
}
}
Ok(())
}use shopsavvy_sdk::{Client, Error, Result};
use tokio::time::{sleep, Duration};
use std::cmp;
async fn retry_with_backoff<F, T>(
mut operation: F,
max_attempts: u32,
initial_delay: Duration,
max_delay: Duration,
) -> Result<T>
where
F: FnMut() -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<T>> + Send>>,
{
let mut attempt = 1;
let mut delay = initial_delay;
loop {
match operation().await {
Ok(result) => return Ok(result),
Err(Error::RateLimit { retry_after, .. }) => {
if attempt >= max_attempts {
return Err(Error::RateLimit {
message: "Max retry attempts exceeded".to_string(),
retry_after,
});
}
// Use server-specified retry delay if available
let retry_delay = retry_after
.map(Duration::from_secs)
.unwrap_or(delay);
eprintln!("Rate limited, retrying in {:?}...", retry_delay);
sleep(retry_delay).await;
}
Err(Error::Network { .. }) | Err(Error::Timeout { .. }) => {
if attempt >= max_attempts {
return Err(Error::Network {
message: "Max retry attempts exceeded".to_string()
});
}
eprintln!("Network error, retrying in {:?}...", delay);
sleep(delay).await;
}
Err(e) => return Err(e), // Don't retry other errors
}
attempt += 1;
delay = cmp::min(delay * 2, max_delay); // Exponential backoff with cap
}
}
// Usage
async fn robust_product_lookup(client: &Client, identifier: &str) -> Result<Product> {
retry_with_backoff(
|| Box::pin(client.get_product_details(identifier)),
3,
Duration::from_secs(1),
Duration::from_secs(30),
).await
}# Clone the repository
git clone https://github.com/shopsavvy/sdk-rust.git
cd sdk-rust
# Build the project
cargo build
# Run tests
cargo test
# Run tests with output
cargo test -- --nocapture
# Check formatting
cargo fmt --check
# Run Clippy lints
cargo clippy -- -D warnings
# Generate documentation
cargo doc --openuse shopsavvy_sdk::{Client, Result};
#[tokio::test]
async fn test_sdk_integration() -> Result<()> {
// Use test API key (starts with ss_test_)
let client = Client::new("ss_test_your_test_key_here")?;
// Test product lookup
let product = client.get_product_details("012345678901").await?;
println!("β
Product lookup: {}", product.data.name);
assert!(!product.data.name.is_empty());
// Test current offers
let offers = client.get_current_offers("012345678901").await?;
println!("β
Current offers: {} found", offers.data.len());
assert!(!offers.data.is_empty());
// Test usage info
let usage = client.get_usage().await?;
println!("β
API usage: {} credits remaining",
usage.data.credits_remaining.unwrap_or(0));
println!("\nπ All tests passed! SDK is working correctly.");
Ok(())
}
#[tokio::main]
async fn main() -> Result<()> {
test_sdk_integration().await
}These mirror src/types.rs. Fields the API may send as null or omit are Option.
pub struct ProductDetails {
pub title: String,
pub shopsavvy: String, // ShopSavvy product ID
pub brand: Option<String>,
pub category: Option<String>,
pub images: Option<Vec<String>>,
pub barcode: Option<String>,
pub amazon: Option<String>, // ASIN
pub model: Option<String>,
pub mpn: Option<String>,
pub color: Option<String>,
pub title_short: Option<String>,
pub slug: Option<String>,
pub description: Option<String>,
pub categories: Option<Vec<String>>,
pub attributes: Option<HashMap<String, String>>,
pub rating: Option<serde_json::Value>, // {"value": 4.6, "count": 18234}
pub score: Option<serde_json::Value>, // 0-1 scale: overall, customer, professional, aspects
pub keywords: Option<Vec<String>>,
pub identifiers: Option<serde_json::Value>,
}pub struct Offer {
pub id: String,
pub retailer: Option<String>,
pub price: Option<f64>,
pub currency: Option<String>,
/// "in", "out", "limited", "pre-order", "coming-soon" or "discontinued";
/// None when unknown.
pub availability: Option<String>,
pub condition: Option<String>,
pub url: Option<String>, // "URL" on the wire
pub seller: Option<String>,
pub timestamp: Option<String>,
pub history: Option<Vec<PriceHistoryEntry>>,
}
impl Offer {
/// true when availability is "in"
pub fn is_in_stock(&self) -> bool;
}/// Element of `data` from schedule_product_monitoring[_batch]() and get_scheduled_products()
pub struct ScheduledProduct {
#[serde(flatten)]
pub product: ProductDetails,
pub schedule: Option<String>, // "hourly" | "daily" | "weekly"
pub retailer: Option<String>,
}
/// remove_product_from_schedule() / remove_products_from_schedule(): no `data`
pub struct ScheduleRemovalResponse {
pub success: bool,
pub message: Option<String>,
pub meta: Option<ApiMeta>,
}pub struct ApiResponse<T> {
pub success: bool,
pub data: T,
pub message: Option<String>,
pub meta: Option<ApiMeta>,
}
pub struct ApiMeta {
pub request_id: Option<String>,
pub credits_used: i32,
pub credits_remaining: i32,
pub rate_limit_remaining: Option<i32>,
}
impl<T> ApiResponse<T> {
pub fn credits_used(&self) -> i32; // 0 when meta is absent
pub fn credits_remaining(&self) -> i32;
}- ShopSavvy Data API Documentation - Complete API reference
- API Dashboard - Manage your API keys and usage
- GitHub Repository - Source code and issues
- Crates.io - Package releases and stats
- Rust Documentation - Rust language reference
- Tokio Documentation - Async runtime documentation
- 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
- Rust best practices and performance optimization
This project is licensed under the MIT License - see the LICENSE file for details.
ShopSavvy has been comparing prices since 2008, and the app has 40M+ downloads. The ShopSavvy Data API serves the same product and price data over REST: send a barcode (UPC/EAN/ISBN), ASIN, product URL, model number or MPN, and get back the product record, current offers from tens of thousands of retailers, and price history back to 2011 (dense from 2019).
- Lookup by any identifier: barcode, ASIN, URL, model number, MPN or ShopSavvy ID, up to 100 per request
- Current offers: price, availability, condition, seller and when each offer was last seen
- Price history: quality-filtered price points per retailer over any date range
- Scheduled refreshes: hourly, daily or weekly re-checks for the products you care about, at retailers that can be re-checked through an official retailer API (others update as ShopSavvy sees them)
- JSON or CSV responses, with credits used and remaining on every response
- Open-source SDKs in 10 languages: Python, TypeScript, Go, Rust, Java, Kotlin, Swift, PHP, Ruby and C#
Plans and the 14-day trial are at shopsavvy.com/data; full reference at shopsavvy.com/data/documentation.
- π Zero-cost abstractions - Maximum performance with minimal overhead
- β‘ Async/await first - Built on Tokio for high-concurrency applications
- π‘οΈ Memory safety - Leverage Rust's ownership system for bulletproof code
- π§ Rich type system - Compile-time guarantees and excellent IDE support
- π WebAssembly ready - Compile to WASM for browser and edge deployment
- π High performance - Ideal for data-intensive and real-time applications
Ready to get started? Sign up for your API key β’ Need help? Contact us