cURL
Submit, poll, and retrieve a RunHarvester request from Bash.
This complete async scrape example uses Bash 5+, cURL, Python 3, and uuidgen. Set RUNHARVESTER_API_KEY in your shell; the base URL defaults to https://api.runharvester.com/api/v1/data. The request has a 120-second deadline by default, with a one-second poll interval. Both can be overridden with RUNHARVESTER_REQUEST_TIMEOUT_MS and RUNHARVESTER_POLL_INTERVAL_MS.
# Async scrape example. Requires Bash 5+, cURL, Python 3, and uuidgen.
# Export RUNHARVESTER_API_KEY before running. Keep the printed key and body for a manual retry.
set -euo pipefail
API_BASE_URL="${RUNHARVESTER_API_BASE_URL:-https://api.runharvester.com/api/v1/data}"
readonly API_KEY="${RUNHARVESTER_API_KEY:?Set RUNHARVESTER_API_KEY before running this example}"
readonly REQUEST_TIMEOUT_ENV="RUNHARVESTER_REQUEST_TIMEOUT_MS"
readonly POLL_INTERVAL_ENV="RUNHARVESTER_POLL_INTERVAL_MS"
readonly IDEMPOTENCY_KEY_ENV="RUNHARVESTER_IDEMPOTENCY_KEY"
readonly DEFAULT_TIMEOUT_MS=120000
readonly DEFAULT_POLL_INTERVAL_MS=1000
readonly CREATE_ATTEMPT_LIMIT=2
readonly RETRY_DELAY_MS=250
while [[ "$API_BASE_URL" == */ ]]; do API_BASE_URL="${API_BASE_URL%/}"; done
request_timeout_ms="${!REQUEST_TIMEOUT_ENV:-$DEFAULT_TIMEOUT_MS}"
poll_interval_ms="${!POLL_INTERVAL_ENV:-$DEFAULT_POLL_INTERVAL_MS}"
if [[ ! "$request_timeout_ms" =~ ^[1-9][0-9]*$ || ! "$poll_interval_ms" =~ ^[1-9][0-9]*$ ]]; then
printf 'Timeout and poll interval must be positive integers in milliseconds.\n' >&2
exit 1
fi
idempotency_key="${!IDEMPOTENCY_KEY_ENV:-$(uuidgen)}"
request_body='{"url":"https://example.com","mode":"auto"}'
printf 'Idempotency-Key: %s\n' "$idempotency_key"
now_ms() {
local value="${EPOCHREALTIME/./}"
printf '%s' "${value:0:13}"
}
deadline_ms=$(( $(now_ms) + request_timeout_ms ))
remaining_ms() {
local remaining=$(( deadline_ms - $(now_ms) ))
(( remaining > 0 )) || return 1
printf '%s' "$remaining"
}
sleep_ms() {
local duration_ms="$1"
sleep "$(printf '%d.%03d' "$((duration_ms / 1000))" "$((duration_ms % 1000))")"
}
retry_delay() {
local remaining
remaining="$(remaining_ms)" || return 1
if (( remaining < RETRY_DELAY_MS )); then
sleep_ms "$remaining"
else
sleep_ms "$RETRY_DELAY_MS"
fi
}
json_field() {
python3 -c 'import json, sys; print(json.load(sys.stdin).get(sys.argv[1], ""))' "$1" <<< "$2"
}
send_request() {
local method="$1"
local path="$2"
local body="${3:-}"
local remaining
local request_timeout
local output
local -a curl_args
remaining="$(remaining_ms)" || return 2
request_timeout="$remaining"
if (( request_timeout > 15000 )); then request_timeout=15000; fi
curl_args=(
--silent --show-error
--max-time "$(printf '%d.%03d' "$((request_timeout / 1000))" "$((request_timeout % 1000))")"
--request "$method"
--header 'Accept: application/json'
--header "X-API-Key: $API_KEY"
)
if [[ "$method" == "POST" ]]; then
curl_args+=(
--header 'Content-Type: application/json'
--header "Idempotency-Key: $idempotency_key"
--data "$body"
)
fi
if ! output="$(curl "${curl_args[@]}" --write-out $'\n%{http_code}' "$API_BASE_URL$path")"; then
return 1
fi
HTTP_STATUS="${output##*$'\n'}"
HTTP_BODY="${output%$'\n'*}"
}
create_response_received=false
for ((attempt = 1; attempt <= CREATE_ATTEMPT_LIMIT; attempt += 1)); do
if ! send_request POST /requests "$request_body"; then
if (( attempt < CREATE_ATTEMPT_LIMIT )); then
if ! retry_delay; then
printf 'Request exceeded its deadline.\n' >&2
exit 1
fi
continue
fi
printf 'Could not create request after retrying with the same key and body.\n' >&2
exit 1
fi
if [[ "$HTTP_STATUS" =~ ^(502|503|504)$ ]] && (( attempt < CREATE_ATTEMPT_LIMIT )); then
if ! retry_delay; then
printf 'Request exceeded its deadline.\n' >&2
exit 1
fi
continue
fi
if [[ "$HTTP_STATUS" != "202" ]]; then
printf 'Create request failed with HTTP %s: %s\n' "$HTTP_STATUS" "$HTTP_BODY" >&2
exit 1
fi
job_id="$(json_field id "$HTTP_BODY")"
if [[ -z "$job_id" ]]; then
printf 'Create response did not include a request id.\n' >&2
exit 1
fi
create_response_received=true
break
done
if [[ "$create_response_received" != true ]]; then
printf 'Could not create request.\n' >&2
exit 1
fi
while true; do
if ! send_request GET "/requests/$job_id"; then
if ! remaining_ms >/dev/null; then
printf 'Request exceeded its deadline.\n' >&2
exit 1
fi
printf 'Could not read request status.\n' >&2
exit 1
fi
if [[ "$HTTP_STATUS" != "200" ]]; then
printf 'Status request failed with HTTP %s: %s\n' "$HTTP_STATUS" "$HTTP_BODY" >&2
exit 1
fi
request_status="$(json_field status "$HTTP_BODY")"
case "$request_status" in
completed)
break
;;
failed|cancelled)
printf 'Request ended with status %s: %s\n' "$request_status" "$HTTP_BODY" >&2
exit 1
;;
pending|running|solving_captcha)
remaining="$(remaining_ms)" || {
printf 'Request exceeded its deadline.\n' >&2
exit 1
}
if (( remaining < poll_interval_ms )); then
sleep_ms "$remaining"
else
sleep_ms "$poll_interval_ms"
fi
;;
*)
printf 'Unknown request status: %s\n' "$request_status" >&2
exit 1
;;
esac
done
if ! send_request GET "/requests/$job_id/result"; then
if ! remaining_ms >/dev/null; then
printf 'Request exceeded its deadline.\n' >&2
exit 1
fi
printf 'Could not retrieve request result.\n' >&2
exit 1
fi
if [[ "$HTTP_STATUS" != "200" ]]; then
printf 'Result request failed with HTTP %s: %s\n' "$HTTP_STATUS" "$HTTP_BODY" >&2
exit 1
fi
printf '%s\n' "$HTTP_BODY"The create retry uses the same UUID and serialized body. Keep the printed key and body if you need to retry manually. For retry classification, see Retries and idempotency; for exact headers and response fields, see the API reference.
Last updated on