Recipes: complete scripts for common Data API tasks
Last updated · Changelog · OpenAPI spec · Markdown
Five tasks from start to finish, each as a complete script in three languages: bash with curl and jq, Python 3 with requests, and TypeScript for Node 18+ (run it with npx tsx) or Bun. Every script on these pages was run against the live API before it was published. Each one reads your key from the SHOPSAVVY_API_KEY environment variable.
The scripts call the REST API directly rather than through an SDK so they can tell the two kinds of HTTP 429 apart: ERR_RATE_LIMIT means wait the Retry-After seconds and try again, ERR_CREDITS_EXHAUSTED means stop, because retrying will not help until the period resets (see Errors). Save the helper for your language next to the recipe scripts; every recipe uses it.
Helper: bash (shopsavvy-call.sh)
# Source this file: . ./shopsavvy-call.sh (needs curl and jq; SHOPSAVVY_API_KEY in the environment)
# api METHOD PATH [JSON_BODY]: prints the response body. Waits out ERR_RATE_LIMIT (Retry-After),
# stops on ERR_CREDITS_EXHAUSTED and on any other error except a 404 (no product matched).
api() {
local attempt status
for attempt in 1 2 3 4 5; do
if [ $# -ge 3 ]; then
status=$(curl -sS -o "$SS_TMP/body" -D "$SS_TMP/headers" -w '%{http_code}' -X "$1" "https://api.shopsavvy.com/v1$2" \
-H "Authorization: Bearer $SHOPSAVVY_API_KEY" -H 'Content-Type: application/json' -d "$3")
else
status=$(curl -sS -o "$SS_TMP/body" -D "$SS_TMP/headers" -w '%{http_code}' -X "$1" "https://api.shopsavvy.com/v1$2" \
-H "Authorization: Bearer $SHOPSAVVY_API_KEY")
fi
if [ "$status" = 429 ]; then
if [ "$(jq -r '.error_detail.code' "$SS_TMP/body")" = ERR_CREDITS_EXHAUSTED ]; then
jq -r '.error_detail.action' "$SS_TMP/body" >&2
return 2
fi
sleep "$(awk 'tolower($1) == "retry-after:" { print $2 + 0 }' "$SS_TMP/headers")"
continue
fi
if [ "$status" -ge 400 ] && [ "$status" != 404 ]; then
jq -r '"\(.error_detail.code): \(.error_detail.message) \(.error_detail.action)"' "$SS_TMP/body" >&2
return 1
fi
cat "$SS_TMP/body"
return 0
done
echo "Still rate limited after 5 attempts" >&2
return 1
}
SS_TMP=$(mktemp -d)Helper: Python (shopsavvy_call.py)
pip install requests
import os
import time
import requests
API = "https://api.shopsavvy.com/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['SHOPSAVVY_API_KEY']}"}
class CreditsExhausted(Exception):
"""This month's credits are used up: waiting will not help until the period resets."""
def call(method, path, not_found_ok=False, **kwargs):
"""One Data API request. Waits out ERR_RATE_LIMIT (Retry-After) and stops on ERR_CREDITS_EXHAUSTED."""
for _ in range(5):
response = requests.request(method, API + path, headers=HEADERS, timeout=120, **kwargs)
if response.status_code == 429:
detail = response.json().get("error_detail", {})
if detail.get("code") == "ERR_CREDITS_EXHAUSTED":
raise CreditsExhausted(detail.get("action"))
time.sleep(int(response.headers.get("Retry-After", "60")))
continue
if response.status_code == 404 and not_found_ok:
return response
if response.status_code >= 400:
detail = response.json().get("error_detail", {})
raise RuntimeError(f"{detail.get('code')}: {detail.get('message')} {detail.get('action', '')}")
return response
raise RuntimeError("Still rate limited after 5 attempts")Helper: TypeScript (shopsavvy-call.ts)
No dependencies: it uses the built-in fetch.
const API = 'https://api.shopsavvy.com/v1'
const KEY = process.env.SHOPSAVVY_API_KEY
export class CreditsExhausted extends Error {}
// One Data API request. Waits out ERR_RATE_LIMIT (Retry-After) and stops on ERR_CREDITS_EXHAUSTED.
export async function call(method: string, path: string, options: { body?: unknown, notFoundOk?: boolean } = {}): Promise<Response> {
for (let attempt = 0; attempt < 5; attempt++) {
const response = await fetch(API + path, {
method,
headers: { Authorization: `Bearer ${KEY}`, 'Content-Type': 'application/json' },
body: options.body === undefined ? undefined : JSON.stringify(options.body),
})
if (response.status === 429) {
const detail = (await response.json()).error_detail ?? {}
if (detail.code === 'ERR_CREDITS_EXHAUSTED') throw new CreditsExhausted(detail.action)
await new Promise((resolve) => setTimeout(resolve, Number(response.headers.get('Retry-After') ?? 60) * 1000))
continue
}
if (response.status === 404 && options.notFoundOk) return response
if (response.status >= 400) {
const detail = (await response.json()).error_detail ?? {}
throw new Error(`${detail.code}: ${detail.message} ${detail.action ?? ''}`)
}
return response
}
throw new Error('Still rate limited after 5 attempts')
}
// One CSV line, quoting any field that needs it.
export function csvLine(fields: unknown[]): string {
return fields.map((field) => {
const text = field === undefined || field === null ? '' : String(field)
return /[",\n]/.test(text) ? `"${text.replace(/"/g, '""')}"` : text
}).join(',') + '\n'
}