Prerequisites and safety
Use an isolated sandbox key with catalog:read, bash, curl and jq. Each batch accepts 1–100 inputs. Resolve a known product and a deliberately missing canonical ID, then read both offer results. Production batches consume one comparison credit per input, including unmatched inputs.
Run the isolated exercise
- 1Find product identities
- 2Resolve known and missing identities
- 3Preserve partial results and missing evidence
Run from your server
set -euo pipefail
# Requires bash, curl and jq. Use an isolated sandbox API key.
DOCS_BASE_URL="${DOCS_BASE_URL:-https://hintadata.fi}"
: "${HINTADATA_API_KEY:?Set a sandbox API key privately}"
DOCS_VARIABLES=$(jq -cn '{}')
DOCS_RESPONSE_FILE=$(mktemp)
trap 'rm -f -- "$DOCS_RESPONSE_FILE"' EXIT
# search: expected HTTP 200
DOCS_PATH="$(jq -cnr --argjson vars "$DOCS_VARIABLES" '"/v1/search?q=sony&limit=2"')"
DOCS_STATUS=$(curl --silent --show-error --max-time 20 --output "$DOCS_RESPONSE_FILE" --write-out '%{http_code}' -X 'GET' "$DOCS_BASE_URL$DOCS_PATH" -H "Authorization: Bearer $HINTADATA_API_KEY")
[[ "$DOCS_STATUS" == '200' ]] || { printf '%s\n' 'Unexpected HTTP status at search' >&2; exit 1; }
DOCS_RESPONSE=$(jq -c . "$DOCS_RESPONSE_FILE")
jq -en --argjson vars "$DOCS_VARIABLES" --argjson response "$DOCS_RESPONSE" --argjson status "$DOCS_STATUS" '((($response | if type == "object" or type == "array" then has("query") else false end))) and ($response["query"] == ("sony"))' >/dev/null
jq -en --argjson vars "$DOCS_VARIABLES" --argjson response "$DOCS_RESPONSE" --argjson status "$DOCS_STATUS" '((($response | if type == "object" or type == "array" then has("items") else false end) and ($response["items"] | if type == "object" or type == "array" then has(0) else false end) and ($response["items"][0] | if type == "object" or type == "array" then has("canonical_product_id") else false end)) == true)' >/dev/null
DOCS_VARIABLES=$(jq -cn --argjson vars "$DOCS_VARIABLES" --argjson response "$DOCS_RESPONSE" '$vars + {"productId":$response["items"][0]["canonical_product_id"],"secondId":$response["items"][1]["canonical_product_id"]}')
printf '%s\n' 'search: verified'
# resolve: expected HTTP 200
jq -e 'has("productId")' <<< "$DOCS_VARIABLES" >/dev/null
DOCS_PATH="$(jq -cnr --argjson vars "$DOCS_VARIABLES" '"/v1/products/resolve"')"
DOCS_STATUS=$(curl --silent --show-error --max-time 20 --output "$DOCS_RESPONSE_FILE" --write-out '%{http_code}' -X 'POST' "$DOCS_BASE_URL$DOCS_PATH" -H "Authorization: Bearer $HINTADATA_API_KEY" -H 'Content-Type: application/json' --data "$(jq -cnr --argjson vars "$DOCS_VARIABLES" '{"items":[{"canonical_product_id":$vars["productId"]},{"canonical_product_id":"docs-missing-product"}]}')")
[[ "$DOCS_STATUS" == '200' ]] || { printf '%s\n' 'Unexpected HTTP status at resolve' >&2; exit 1; }
DOCS_RESPONSE=$(jq -c . "$DOCS_RESPONSE_FILE")
jq -en --argjson vars "$DOCS_VARIABLES" --argjson response "$DOCS_RESPONSE" --argjson status "$DOCS_STATUS" '((($response | if type == "object" or type == "array" then has("items") else false end) and ($response["items"] | if type == "object" or type == "array" then has(0) else false end) and ($response["items"][0] | if type == "object" or type == "array" then has("status") else false end))) and ($response["items"][0]["status"] == ("matched"))' >/dev/null
jq -en --argjson vars "$DOCS_VARIABLES" --argjson response "$DOCS_RESPONSE" --argjson status "$DOCS_STATUS" '((($response | if type == "object" or type == "array" then has("items") else false end) and ($response["items"] | if type == "object" or type == "array" then has(0) else false end) and ($response["items"][0] | if type == "object" or type == "array" then has("candidates") else false end) and ($response["items"][0]["candidates"] | if type == "object" or type == "array" then has(0) else false end))) and ($response["items"][0]["candidates"][0] == ($vars["productId"]))' >/dev/null
jq -en --argjson vars "$DOCS_VARIABLES" --argjson response "$DOCS_RESPONSE" --argjson status "$DOCS_STATUS" '((($response | if type == "object" or type == "array" then has("items") else false end) and ($response["items"] | if type == "object" or type == "array" then has(1) else false end) and ($response["items"][1] | if type == "object" or type == "array" then has("status") else false end))) and ($response["items"][1]["status"] == ("unmatched"))' >/dev/null
jq -en --argjson vars "$DOCS_VARIABLES" --argjson response "$DOCS_RESPONSE" --argjson status "$DOCS_STATUS" '((($response | if type == "object" or type == "array" then has("items") else false end) and ($response["items"] | if type == "object" or type == "array" then has(1) else false end) and ($response["items"][1] | if type == "object" or type == "array" then has("candidates") else false end))) and ($response["items"][1]["candidates"] == ([]))' >/dev/null
printf '%s\n' 'resolve: verified'
# offers: expected HTTP 200
jq -e 'has("productId")' <<< "$DOCS_VARIABLES" >/dev/null
DOCS_PATH="$(jq -cnr --argjson vars "$DOCS_VARIABLES" '"/v1/offers/batch"')"
DOCS_STATUS=$(curl --silent --show-error --max-time 20 --output "$DOCS_RESPONSE_FILE" --write-out '%{http_code}' -X 'POST' "$DOCS_BASE_URL$DOCS_PATH" -H "Authorization: Bearer $HINTADATA_API_KEY" -H 'Content-Type: application/json' --data "$(jq -cnr --argjson vars "$DOCS_VARIABLES" '{"items":[{"canonical_product_id":$vars["productId"]},{"canonical_product_id":"docs-missing-product"}]}')")
[[ "$DOCS_STATUS" == '200' ]] || { printf '%s\n' 'Unexpected HTTP status at offers' >&2; exit 1; }
DOCS_RESPONSE=$(jq -c . "$DOCS_RESPONSE_FILE")
jq -en --argjson vars "$DOCS_VARIABLES" --argjson response "$DOCS_RESPONSE" --argjson status "$DOCS_STATUS" '((($response | if type == "object" or type == "array" then has("items") else false end) and ($response["items"] | if type == "object" or type == "array" then has(0) else false end) and ($response["items"][0] | if type == "object" or type == "array" then has("status") else false end))) and ($response["items"][0]["status"] == ("matched"))' >/dev/null
jq -en --argjson vars "$DOCS_VARIABLES" --argjson response "$DOCS_RESPONSE" --argjson status "$DOCS_STATUS" '((($response | if type == "object" or type == "array" then has("items") else false end) and ($response["items"] | if type == "object" or type == "array" then has(1) else false end) and ($response["items"][1] | if type == "object" or type == "array" then has("status") else false end))) and ($response["items"][1]["status"] == ("unmatched"))' >/dev/null
jq -en --argjson vars "$DOCS_VARIABLES" --argjson response "$DOCS_RESPONSE" --argjson status "$DOCS_STATUS" '((($response | if type == "object" or type == "array" then has("items") else false end) and ($response["items"] | if type == "object" or type == "array" then has(1) else false end) and ($response["items"][1] | if type == "object" or type == "array" then has("offers") else false end))) and ($response["items"][1]["offers"] == ([]))' >/dev/null
jq -en --argjson vars "$DOCS_VARIABLES" --argjson response "$DOCS_RESPONSE" --argjson status "$DOCS_STATUS" '((($response | if type == "object" or type == "array" then has("items") else false end) and ($response["items"] | if type == "object" or type == "array" then has(1) else false end) and ($response["items"][1] | if type == "object" or type == "array" then has("best_offer") else false end))) and ($response["items"][1]["best_offer"] == (null))' >/dev/null
printf '%s\n' 'offers: verified'Verify the result
Both batches return HTTP 200, but only the first row is matched. Input order is preserved. The missing row has no candidates or offers and best_offer is null: that is missing evidence, not a zero price or proof of sold-out stock. The commands verify these values explicitly.
Recovery and cleanup
Handle every row status independently. For unmatched, fix the identifier; for ambiguous, review the candidates and select an exact canonical ID. An invalid GTIN or multiple identifiers in one row rejects the request. For 503 source_stale, wait for the source owner to refresh evidence; do not convert it into an empty successful result. These read-only POSTs need no Idempotency-Key.
For 429 or temporary 5xx, respect Retry-After and use bounded retries. Keep API keys and response bodies out of logs and support tickets.