Prerequisites and safety
Use a sandbox key with catalog:read and watchlists:write in an isolated workspace with no existing watches, plus bash, curl and jq. Set your approved WATCH_EMAIL and a unique WATCH_REQUEST_KEY privately. Reuse that key only when retrying the identical creation. Sandbox watches never deliver email. These exercises deliberately test a stale revision; do not run them against live watches.
Run the isolated exercise
- 1Find product identities
- 2Create a synthetic watch
- 3Change the target price
- 4Detect a stale revision
- 5Retry with the current revision
- 6Remove the watch
- 7Verify cleanup
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}"
: "${WATCH_EMAIL:?Set WATCH_EMAIL privately before running}"
: "${WATCH_REQUEST_KEY:?Set WATCH_REQUEST_KEY privately before running}"
DOCS_VARIABLES=$(jq -cn --arg watchEmail "$WATCH_EMAIL" --arg requestKey "$WATCH_REQUEST_KEY" '{watchEmail:$watchEmail,requestKey:$requestKey}')
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'
# create: expected HTTP 201
jq -e 'has("requestKey")' <<< "$DOCS_VARIABLES" >/dev/null
jq -e 'has("productId")' <<< "$DOCS_VARIABLES" >/dev/null
jq -e 'has("watchEmail")' <<< "$DOCS_VARIABLES" >/dev/null
DOCS_PATH="$(jq -cnr --argjson vars "$DOCS_VARIABLES" '"/v1/watchlists"')"
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 "Idempotency-Key: $(jq -cnr --argjson vars "$DOCS_VARIABLES" '$vars["requestKey"]')" -H 'Content-Type: application/json' --data "$(jq -cnr --argjson vars "$DOCS_VARIABLES" '{"canonical_product_id":$vars["productId"],"condition":"target_price","target_price":199,"channel":"email","destination":$vars["watchEmail"]}')")
[[ "$DOCS_STATUS" == '201' ]] || { printf '%s\n' 'Unexpected HTTP status at create' >&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("revision") else false end))) and ($response["revision"] == (1))' >/dev/null
DOCS_VARIABLES=$(jq -cn --argjson vars "$DOCS_VARIABLES" --argjson response "$DOCS_RESPONSE" '$vars + {"watchId":$response["id"],"revision":$response["revision"]}')
printf '%s\n' 'create: verified'
# advance: expected HTTP 200
jq -e 'has("watchId")' <<< "$DOCS_VARIABLES" >/dev/null
jq -e 'has("revision")' <<< "$DOCS_VARIABLES" >/dev/null
DOCS_PATH="$(jq -cnr --argjson vars "$DOCS_VARIABLES" '"/v1/watchlists/" + ($vars["watchId"] | tostring | @uri) + ""')"
DOCS_STATUS=$(curl --silent --show-error --max-time 20 --output "$DOCS_RESPONSE_FILE" --write-out '%{http_code}' -X 'PATCH' "$DOCS_BASE_URL$DOCS_PATH" -H "Authorization: Bearer $HINTADATA_API_KEY" -H 'Content-Type: application/json' --data "$(jq -cnr --argjson vars "$DOCS_VARIABLES" '{"expected_revision":$vars["revision"],"target_price":198}')")
[[ "$DOCS_STATUS" == '200' ]] || { printf '%s\n' 'Unexpected HTTP status at advance' >&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("revision") else false end))) and ($response["revision"] == (2))' >/dev/null
DOCS_VARIABLES=$(jq -cn --argjson vars "$DOCS_VARIABLES" --argjson response "$DOCS_RESPONSE" '$vars + {"revision":$response["revision"]}')
printf '%s\n' 'advance: verified'
# conflict: expected HTTP 409
jq -e 'has("watchId")' <<< "$DOCS_VARIABLES" >/dev/null
DOCS_PATH="$(jq -cnr --argjson vars "$DOCS_VARIABLES" '"/v1/watchlists/" + ($vars["watchId"] | tostring | @uri) + ""')"
DOCS_STATUS=$(curl --silent --show-error --max-time 20 --output "$DOCS_RESPONSE_FILE" --write-out '%{http_code}' -X 'PATCH' "$DOCS_BASE_URL$DOCS_PATH" -H "Authorization: Bearer $HINTADATA_API_KEY" -H 'Content-Type: application/json' --data "$(jq -cnr --argjson vars "$DOCS_VARIABLES" '{"expected_revision":1,"status":"paused"}')")
[[ "$DOCS_STATUS" == '409' ]] || { printf '%s\n' 'Unexpected HTTP status at conflict' >&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("error") else false end) and ($response["error"] | if type == "object" or type == "array" then has("code") else false end))) and ($response["error"]["code"] == ("revision_conflict"))' >/dev/null
printf '%s\n' 'conflict: verified'
# pause: expected HTTP 200
jq -e 'has("watchId")' <<< "$DOCS_VARIABLES" >/dev/null
jq -e 'has("revision")' <<< "$DOCS_VARIABLES" >/dev/null
DOCS_PATH="$(jq -cnr --argjson vars "$DOCS_VARIABLES" '"/v1/watchlists/" + ($vars["watchId"] | tostring | @uri) + ""')"
DOCS_STATUS=$(curl --silent --show-error --max-time 20 --output "$DOCS_RESPONSE_FILE" --write-out '%{http_code}' -X 'PATCH' "$DOCS_BASE_URL$DOCS_PATH" -H "Authorization: Bearer $HINTADATA_API_KEY" -H 'Content-Type: application/json' --data "$(jq -cnr --argjson vars "$DOCS_VARIABLES" '{"expected_revision":$vars["revision"],"status":"paused"}')")
[[ "$DOCS_STATUS" == '200' ]] || { printf '%s\n' 'Unexpected HTTP status at pause' >&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("status") else false end))) and ($response["status"] == ("paused"))' >/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("revision") else false end))) and ($response["revision"] == (3))' >/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("status") else false end))) and ($response["status"] == ("paused"))' >/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("revision") else false end))) and ($response["revision"] == (3))' >/dev/null
printf '%s\n' 'pause: verified'
# delete: expected HTTP 200
jq -e 'has("watchId")' <<< "$DOCS_VARIABLES" >/dev/null
DOCS_PATH="$(jq -cnr --argjson vars "$DOCS_VARIABLES" '"/v1/watchlists/" + ($vars["watchId"] | tostring | @uri) + ""')"
DOCS_STATUS=$(curl --silent --show-error --max-time 20 --output "$DOCS_RESPONSE_FILE" --write-out '%{http_code}' -X 'DELETE' "$DOCS_BASE_URL$DOCS_PATH" -H "Authorization: Bearer $HINTADATA_API_KEY")
[[ "$DOCS_STATUS" == '200' ]] || { printf '%s\n' 'Unexpected HTTP status at delete' >&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("removed") else false end))) and ($response["removed"] == (true))' >/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("id") else false end))) and ($response["id"] == ($vars["watchId"]))' >/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("removed") else false end))) and ($response["removed"] == (true))' >/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("id") else false end))) and ($response["id"] == ($vars["watchId"]))' >/dev/null
printf '%s\n' 'delete: verified'
# verify: expected HTTP 200
DOCS_PATH="$(jq -cnr --argjson vars "$DOCS_VARIABLES" '"/v1/watchlists"')"
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 verify' >&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("total") else false end))) and ($response["total"] == (0))' >/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("total") else false end))) and ($response["total"] == (0))' >/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"] == ([]))' >/dev/null
printf '%s\n' 'verify: verified'Verify the result
POST returns 201 with id and revision 1. Updating the target advances revision to 2; a stale update returns 409. The corrected pause returns 200 and revision 3. DELETE returns 200 with removed: true; the final isolated list is empty. Ownership comes from the authenticated workspace and environment, never a client-supplied owner header.
Recovery and cleanup
409 revision_conflict: list watches, read the current revision, review concurrent changes, then retry the intended patch. 409 idempotency_conflict: do not change the body under an existing key; a genuinely new creation needs a new key. 503 delivery_unavailable: configure live delivery or use a sandbox key. On interruption, list and delete only the watch ID you created.
For 429 or temporary 5xx, respect Retry-After and use bounded retries. Keep API keys and response bodies out of logs and support tickets.