Turn a barcode list into product details
Last updated · Changelog · OpenAPI spec · Markdown
Turn a CSV of barcodes into a CSV of product records: title, brand, model, ShopSavvy ID, and how each barcode matched. The input file has a barcode header, then one barcode per line. Output rows keep the input order, and barcodes that match nothing are listed as not_found.
| Step | REST endpoint | MCP tool | Credits |
|---|---|---|---|
| Look up 100 barcodes at a time | POST /v1/products/batch | product_lookup_batch | 1 per product found |
| Poll a batch of more than 20 until it completes | GET /v1/batch/{id} | none | 0 |
Cost for N barcodes: at most N credits, since barcodes that match nothing cost 0. 10,000 barcodes take 100 batch requests and at most 10,000 credits. Add "include": ["offers"] to the request body for current prices as well, at 3 more credits per product that has offers. Batches of 21 to 100 identifiers run asynchronously; see Batch Product Lookup.
bash
#!/usr/bin/env bash
# Bulk barcode CSV -> product details CSV, 100 barcodes per POST /v1/products/batch.
# Usage: SHOPSAVVY_API_KEY=... ./barcodes-to-products.sh barcodes.csv products.csv
set -euo pipefail
. ./shopsavvy-call.sh
echo "barcode,status,title,brand,model,shopsavvy_id,matched_by,candidates" > "$2"
tail -n +2 "$1" | tr -d '\r' | grep -v '^$' | split -l 100 - "$SS_TMP/chunk_"
for chunk in "$SS_TMP"/chunk_*; do
response=$(api POST /products/batch "$(jq -R -s -c 'split("\n") | map(select(length > 0)) | {identifiers: .}' "$chunk")")
batch_id=$(jq -r '.batch.id // empty' <<< "$response")
if [ -n "$batch_id" ]; then
# More than 20 identifiers: poll until the batch is done.
sleep "$(jq '[.batch.estimated_seconds // 10, 30] | min' <<< "$response")"
until [ "$(jq -r '.batch.status' <<< "$response")" = completed ]; do
[ "$(jq -r '.batch.status' <<< "$response")" = failed ] && { jq -r '.error' <<< "$response" >&2; exit 1; }
sleep 5
response=$(api GET "/batch/$batch_id")
done
fi
# Results can come back in any order; write them in the order of the input file.
jq -r --rawfile chunk "$chunk" '
(.results | map({key: .identifier, value: .}) | from_entries) as $by
| $chunk | split("\n") | map(select(length > 0))[]
| . as $barcode | ($by[$barcode] // {status: "not_found"}) as $r
| [$barcode, $r.status, $r.product.title, $r.product.brand, $r.product.model, $r.product.shopsavvy, $r.product.match.by, $r.product.match.candidates]
| @csv' <<< "$response" >> "$2"
done
echo "Wrote $2"Python
"""Bulk barcode CSV -> product details CSV, 100 barcodes per POST /v1/products/batch.
Usage: SHOPSAVVY_API_KEY=... python barcodes_to_products.py barcodes.csv products.csv
"""
import csv
import sys
import time
from shopsavvy_call import call
def lookup(identifiers):
"""Up to 100 identifiers. 20 or fewer are answered at once; more are polled until done."""
body = call("POST", "/products/batch", json={"identifiers": identifiers}).json()
if "results" in body:
return body["results"]
batch_id = body["batch"]["id"]
time.sleep(min(body["batch"].get("estimated_seconds", 10), 30))
while True:
status = call("GET", f"/batch/{batch_id}").json()
if status["batch"]["status"] == "completed":
return status["results"]
if status["batch"]["status"] == "failed":
raise RuntimeError(status.get("error", "batch failed"))
time.sleep(5)
def main(source, destination):
with open(source, newline="") as f:
barcodes = [row["barcode"].strip() for row in csv.DictReader(f) if row["barcode"].strip()]
with open(destination, "w", newline="") as f:
out = csv.writer(f)
out.writerow(["barcode", "status", "title", "brand", "model", "shopsavvy_id", "matched_by", "candidates"])
for start in range(0, len(barcodes), 100):
chunk = barcodes[start:start + 100]
# Results can come back in any order; write them in the order of the input file.
by_identifier = {result["identifier"]: result for result in lookup(chunk)}
for barcode in chunk:
result = by_identifier.get(barcode, {"status": "not_found"})
product = result.get("product") or {}
match = product.get("match") or {}
out.writerow([barcode, result["status"], product.get("title", ""), product.get("brand", ""),
product.get("model", ""), product.get("shopsavvy", ""), match.get("by", ""), match.get("candidates", "")])
print(f"Looked up {len(barcodes)} barcodes; wrote {destination}")
if __name__ == "__main__":
main(sys.argv[1], sys.argv[2])TypeScript
// Bulk barcode CSV -> product details CSV, 100 barcodes per POST /v1/products/batch.
// Usage: SHOPSAVVY_API_KEY=... npx tsx barcodes-to-products.ts barcodes.csv products.csv
import { readFileSync, writeFileSync } from 'node:fs'
import { call, csvLine } from './shopsavvy-call'
const sleep = (seconds: number) => new Promise((resolve) => setTimeout(resolve, seconds * 1000))
// Up to 100 identifiers. 20 or fewer are answered at once; more are polled until done.
async function lookup(identifiers: string[]): Promise<any[]> {
const body = await (await call('POST', '/products/batch', { body: { identifiers } })).json()
if (body.results) return body.results
await sleep(Math.min(body.batch.estimated_seconds ?? 10, 30))
while (true) {
const status = await (await call('GET', `/batch/${body.batch.id}`)).json()
if (status.batch.status === 'completed') return status.results
if (status.batch.status === 'failed') throw new Error(status.error ?? 'batch failed')
await sleep(5)
}
}
async function main(source: string, destination: string) {
const barcodes = readFileSync(source, 'utf8').split(/\r?\n/).slice(1).map((line) => line.trim()).filter(Boolean)
let csv = csvLine(['barcode', 'status', 'title', 'brand', 'model', 'shopsavvy_id', 'matched_by', 'candidates'])
for (let start = 0; start < barcodes.length; start += 100) {
const chunk = barcodes.slice(start, start + 100)
// Results can come back in any order; write them in the order of the input file.
const byIdentifier = new Map((await lookup(chunk)).map((result) => [result.identifier, result]))
for (const barcode of chunk) {
const result = byIdentifier.get(barcode) ?? { status: 'not_found' }
const product = result.product ?? {}
csv += csvLine([barcode, result.status, product.title, product.brand, product.model, product.shopsavvy, product.match?.by, product.match?.candidates])
}
}
writeFileSync(destination, csv)
console.log(`Looked up ${barcodes.length} barcodes; wrote ${destination}`)
}
main(process.argv[2], process.argv[3])Was this page helpful?