Errors
Errors are stable JSON, not prose-only HTTP bodies.
Read the response's retryable value and Retry-After together. Search attempt exhaustion (search_attempt_cap) is a nonretryable 429 QuotaExceeded until UTC midnight. An identical search still running (search_in_progress) uses the same class/status but is retryable after one second; repeat it with the same key and parameters to recover a delivered page's free replay. The reference table lists each class's default, while individual responses can supply different retry guidance.
For latest uploads or a new monitor's baseline, 404 NotFound means YouTube returned 404 for a well-formed channel or playlist RSS feed ID. It is not automatically retryable; check the source ID before trying again. The JavaScript and Python SDKs expose a named NotFound exception. A missing channel from the separate channel-resolution endpoint still uses its documented 404 InvalidInput response.
{
"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
The OAuth provider and token/registration rate limits use protocol fields error and error_description. A token or registration throttle returns 429 with error: temporarily_unavailable and Retry-After: 60; a missing production limiter returns 503 with the same protocol error. Global registration capacity can also return temporarily_unavailable. A registration whose client_name uses the ScriptHaul or BulkTranscripts name is refused with invalid_client_metadata and a description that says so; every other OAuth error carries a generic description on purpose. This is not an errorClass from the REST table. General request validation can still use the API error envelope. Follow the HTTP status and Retry-After, and reconnect for an expired or revoked grant as explained in authentication.
| errorClass | HTTP | Retryable |
|---|---|---|
| IpBlocked | 502 | yes |
| PoTokenRequired | 502 | no |
| RateLimited | 502 | yes |
| Unavailable | 410 | no |
| NoCaptions | 404 | no |
| UpstreamError | 502 | yes |
| RelayUnavailable | 502 | yes |
| BudgetExhausted | 503 | no |
| InvalidInput | 400 | no |
| NotFound | 404 | no |
| QuotaExceeded | 429 | no |
| TurnstileFailed | 403 | no |
| Forbidden | 403 | no |
| InsufficientCredits | 402 | no |
| AccountFrozen | 403 | no |
| ConfigurationError | 503 | no |
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 with backoff.
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 or search relay did not answer. Retry with backoff.
Codes: archive_failed.
BudgetExhausted
HTTP 503 · not retryable. New cold fetches and search pages are paused while the shared proxy budget refills; cached transcript 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, billing_profile_missing, bulk_input_required, channel_resolution_required, code_expired, code_not_found, code_used, conflicting_query_modes, connected_app_not_found, 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_cursor, invalid_ledger_format, invalid_ledger_limit, invalid_ledger_offset, invalid_ledger_range, invalid_offset, invalid_purchase_id, invalid_request_id, invalid_translate, 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, monitor_not_found, 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.
NotFound
HTTP 404 · not retryable. YouTube returned 404 for a well-formed channel or playlist RSS feed. Check the source ID before retrying; the request costs 0 credits and is not automatically retryable. Existing monitors record the failure and retain their scheduled feed-recovery policy.
Codes: feed_not_found.
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. The class default is nonretryable; search_in_progress explicitly returns retryable: true with a one-second wait, while search_attempt_cap stays nonretryable until UTC midnight.
Codes: account_age_ramp, active_job_cap, auth_rate_limited, cache_read_cap, channel_resolutions_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, latest_uploads_cap, library_coverage_cap, library_search_cap, monitor_cap, purchase_in_progress, rate_limit, search_attempt_cap, search_concurrency_cap, search_in_progress, search_page_cap, signup_network_cap, 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, dashboard_session_required, incorrect_code, invalid_api_key, invalid_authorization, invalid_oauth_token, stripe_signature_invalid.
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, refund_debt.
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.
ConfigurationError
HTTP 503 · not retryable. Operator-side configuration is missing or incomplete (a price, a legal approval, an archive limit), or a search page could not be settled. Not caused by your request; follow retryable and contact support with the request ID if it persists.
Codes: archive_entry_too_large, archive_formats_invalid, archive_memory_limit, archive_output_backlog, archive_part_too_large, legal_review_required, search_settlement_failed.
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)