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.

StepREST endpointMCP toolCredits
Look up 100 barcodes at a timePOST /v1/products/batchproduct_lookup_batch1 per product found
Poll a batch of more than 20 until it completesGET /v1/batch/{id}none0

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?