ScriptHaul API
Log inGet API key

Errors

Errors are stable JSON, not prose-only HTTP bodies.

{
  "ok": false,
  "errorClass": "QuotaExceeded",
  "code": "cold_fetch_cap",
  "message": "This key reached its daily cold-fetch cap.",
  "retryable": false,
  "docs_url": "https://api.scripthaul.com/docs/errors#quota_exceeded",
  "request_id": "example-request-id",
  "retry_after": 86400
}

Keep request_id in logs and support reports; the same value is in the X-Request-Id header of every response. Respect Retry-After; retry only errors marked retryable and pending 202 operations. A 402 includes required, available, shortfall, and buy_url. A 405 lists the supported methods in allow and the Allow header. docs_url links to the errorClass section on this page, where the code values that class carries are listed.

Error reference

docs_url on every error links to its errorClass section below; the code field is listed there. A response may also carry a status the class does not default to (for example a 404 not_found or 405 method_not_allowed under InvalidInput).

IpBlocked

HTTP 502 · retryable. YouTube blocked the fetch route used for this cold request. The relay rotates routes on its own; retry with backoff. Cache reads are unaffected.

Codes: none beyond the class default.

PoTokenRequired

HTTP 502 · not retryable. YouTube changed what a fetch route requires. Nothing on your side can fix it; retry later and watch the status page.

Codes: none beyond the class default.

RateLimited

HTTP 502 · retryable. YouTube asked for a slower cadence, or refused an on-the-fly caption translation (translation_refused). Retry with backoff. A strict-mode refusal costs 0.

Codes: translation_refused.

Unavailable

HTTP 410 · not retryable. The video is private, deleted, members-only, or otherwise unavailable. The verdict is durable per video and costs 0.

Codes: transcript_not_ready.

NoCaptions

HTTP 404 · not retryable. No public caption track exists, or the requested language cannot be delivered in strict mode (language_unavailable, with available_languages). Costs 0.

Codes: language_unavailable, transcript_not_ready.

UpstreamError

HTTP 502 · retryable. An upstream system (YouTube, Stripe, or storage) answered in an unexpected way. Retry with backoff; quote request_id if it persists.

Codes: archive_failed, archive_object_unavailable, archive_source_unavailable, cache_object_missing, manifest_file_unavailable, relay_attempt_cap, request_expired, settled_cache_missing, stripe_purchase_pending, stripe_reconcile_page_invalid, stripe_refund_conflict, stripe_refund_debt_conflict, stripe_refund_invalid, stripe_request_failed, stripe_response_invalid, transcript_not_ready, unexpected_error, webhook_dns_failed.

RelayUnavailable

HTTP 502 · retryable. The transcript relay did not answer. Retry with backoff; the status page reports relay health.

Codes: archive_failed.

BudgetExhausted

HTTP 503 · not retryable. New cold fetches are paused while the shared proxy budget refills; cached reads keep working. Bulk jobs pause and resume without intervention.

Codes: none beyond the class default.

InvalidInput

HTTP 400 · not retryable. The request is malformed: a parameter, body field, path, query mode, or method. code names the field; correct the request before retrying.

Codes: archive_job_not_ready, archive_part_not_found, archive_source_changed, batch_tool_calls, bulk_input_required, code_expired, code_not_found, code_used, conflicting_query_modes, disposable_email, duplicate_archive_formats, empty_key_update, empty_list, invalid_archive_format, invalid_archive_part, invalid_code, invalid_credit_block, invalid_daily_credit_cap, invalid_email, invalid_format, invalid_grace_period, invalid_job_id, invalid_key_name, invalid_language, invalid_ledger_format, invalid_ledger_limit, invalid_ledger_offset, invalid_offset, invalid_purchase_id, invalid_request_id, invalid_usage_date, invalid_usage_format, invalid_usage_range, invalid_video_id, invalid_webhook_url, job_cancelled, job_not_found, job_size_cap, job_video_not_found, key_in_query_string, key_not_found, key_revoked, list_too_large, method_not_allowed, not_found, purchase_not_found, refund_block_used, refund_reason_too_long, request_not_found, selection_expired, single_video, single_video_required, stripe_account_unknown, stripe_body_invalid_utf8, stripe_body_too_large, stripe_content_length_invalid, stripe_customer_mismatch, stripe_dispute_mismatch, stripe_event_invalid, stripe_product_mismatch, stripe_refund_mismatch, stripe_session_mismatch, terms_required, unknown_query_mode, unsupported_media_type, usage_range_too_large, webhook_dns_empty, webhook_not_configured, webhook_threshold_invalid, webhook_url_invalid, webhook_url_private.

QuotaExceeded

HTTP 429 · not retryable. A cap or throttle was reached. retry_after and the Retry-After header say when it clears: per-minute rates, daily caps at 00:00 UTC, key and job limits, sign-up throttles.

Codes: account_age_ramp, active_job_cap, cache_read_cap, code_locked, cold_fetch_cap, concurrency_cap, daily_credit_cap, data_api_unit_cap, enumeration_cap, free_pool_cap, global_cold_cap, key_limit, new_account_purchase_limit, purchase_in_progress, rate_limit, signup_rate_limited, verify_rate_limited.

TurnstileFailed

HTTP 403 · not retryable. Part of the shared taxonomy for the free site’s bot check. Never returned to an API-key request.

Codes: none beyond the class default.

Forbidden

HTTP 403 · not retryable. Authentication failed (missing, malformed, revoked, or unknown key; missing dashboard header) or the caller may not use this route (transcript bodies are not served to crawlers).

Codes: account_closed, account_not_found, api_key_required, authentication_required, crawler_forbidden, dashboard_header_required, incorrect_code, invalid_api_key, invalid_authorization.

InsufficientCredits

HTTP 402 · not retryable. The available balance cannot cover the work. The body carries required, available, shortfall, and buy_url. Jobs pause with insufficient_credits and resume after a purchase.

Codes: insufficient_credits.

AccountFrozen

HTTP 403 · not retryable. The account is frozen (payment dispute, refund debt) or closed. Delivery stops; existing data is preserved. Contact support@scripthaul.com.

Codes: account_frozen, insufficient_credits, refund_debt.

ConfigurationError

HTTP 503 · not retryable. Operator-side configuration is missing or incomplete (a price, a legal approval, an archive limit). Not caused by your request; retry later or contact support.

Codes: archive_entry_too_large, archive_formats_invalid, archive_memory_limit, archive_output_backlog, archive_part_too_large, billing_profile_missing, job_not_found, legal_review_required, stripe_request_failed, stripe_signature_invalid.

TurnstileFailed remains part of the shared free-site taxonomy but API-key requests do not use Turnstile. ConfigurationError identifies an operator-side configuration problem and is never a reason to expose secrets or retry in a tight loop.

Inspect an error in four languages

These examples deliberately request the unavailable mock fixture, receive Unavailable with HTTP 410, and keep its request_id. Production clients should branch on errorClass, not English text. Maintained runnable variants are in the examples directory.

curl -i --get https://api.scripthaul.com/v1/transcript \
  -H "Authorization: Bearer $SCRIPTHAUL_API_KEY" \
  --data-urlencode "video_id=aaaaaaaaaaa"
import json, os, urllib.error, urllib.request
request = urllib.request.Request("https://api.scripthaul.com/v1/transcript?video_id=aaaaaaaaaaa")
request.add_header("Authorization", f"Bearer {os.environ['SCRIPTHAUL_API_KEY']}")
try: urllib.request.urlopen(request)
except urllib.error.HTTPError as error: print(json.loads(error.read())["errorClass"])
const response = await fetch("https://api.scripthaul.com/v1/transcript?video_id=aaaaaaaaaaa", {
  headers: { Authorization: `Bearer ${process.env.SCRIPTHAUL_API_KEY}` },
});
const error = await response.json();
console.log(response.status, error.errorClass, error.request_id);
req, _ := http.NewRequest("GET", "https://api.scripthaul.com/v1/transcript?video_id=aaaaaaaaaaa", nil)
req.Header.Set("Authorization", "Bearer "+os.Getenv("SCRIPTHAUL_API_KEY"))
response, err := http.DefaultClient.Do(req)
if err != nil { log.Fatal(err) }
defer response.Body.Close()
io.Copy(os.Stdout, response.Body)