Errors
ByteKit error response shape, common error codes, and retry guidance.
All errors from the ByteKit API follow a consistent envelope shape so you can handle them programmatically.
Error response shape
Every 4xx/5xx response across the API uses one unified envelope. status is
always the literal string "failed", so you can branch on the response body alone.
{
"status": "failed",
"error": {
"code": "rate_limited",
"message": "Rate limit exceeded. Allowed 5 requests per second.",
"http_status": 429,
"failed_at": "2026-08-27T12:17:46.234Z"
}
}The number in a rate_limited message is your own plan's per-second limit — the same value the X-RateLimit-Limit response header carries.
| Field | Description |
|---|---|
status | Always "failed" on an error response — the top-level discriminator |
error.code | Machine-readable string identifier |
error.message | Human-readable explanation |
error.http_status | Mirrors the HTTP response status (useful for logged or replayed bodies) |
error.failed_at | ISO-8601 UTC timestamp of when the error was produced |
error.details | Optional object with additional context (e.g. which field failed validation). Absent — not {} — when the error carries no context |
Common error codes
| HTTP status | Code | Meaning |
|---|---|---|
400 | invalid_url | A supplied URL (including webhook_url) is malformed or uses an unsupported scheme |
400 | validation_error | Field errors on /v1/search, POST /v1/billing/checkout and PUT/DELETE /v1/billing/payment-methods/{id}, plus rejections that run before schema validation on any endpoint: a removed field such as cookies or headers on a capture endpoint, or a /v1/fetch or /v1/fetch/bulk request asking for an option only /v1/scrape or /v1/bulk serves |
401 | unauthorized | Missing or malformed Authorization header |
401 | invalid_api_key | API key is present but not valid |
402 | quota_exceeded | Account has exceeded its plan quota |
402 | spending_cap_reached | A credit-metered request would push overage spend past the account's cap. Only reachable on a custom plan with an overage agreement — standard plans have no automatic overage and stop at quota_exceeded |
402 | billing_entitlement_impaired | The paid subscription is in a payment impairment (e.g. past_due); error.message names the reason |
402 | billing_entitlement_expired | The paid subscription's entitlement period has ended, or its dunning grace period has run out |
403 | forbidden | Key is valid but lacks permission for this action |
422 | validation_error | Malformed JSON, or a request field failed validation, everywhere the 400 row above does not name |
429 | rate_limited | Too many requests per second; back off and retry |
429 | concurrency_limit | Too many simultaneous in-flight requests; back off and retry |
500 | internal_error | The upstream fetch or browser render failed |
Webhook URLs Must Use HTTPS
All webhook URLs submitted to the ByteKit API must use HTTPS. Plain HTTP URLs (e.g. http://example.com/webhook) are rejected with a 400 invalid_url error.
Why HTTPS only? Webhook payloads may contain sensitive data (API keys, session tokens, scrape results). HTTPS encryption protects this data in transit.
Request with an invalid webhook URL:
{
"webhook_url": "http://example.com/webhook"
}Response (HTTP 400) — the same unified envelope as every other error; error.details names the offending field and reason:
{
"status": "failed",
"error": {
"code": "invalid_url",
"message": "url is not a valid HTTP(S) URL: scheme_unsupported",
"http_status": 400,
"failed_at": "2026-08-27T12:16:29.033Z",
"details": {
"url": "http://example.com/webhook",
"field": "webhook_url",
"reason": "scheme_unsupported"
}
}
}Working example:
{
"webhook_url": "https://example.com/webhook"
}This applies to all endpoints that accept a webhook_url parameter: /v1/scrape (async), /v1/scrape/bulk, /v1/fetch/bulk, /v1/bulk, /v1/monitors, and /v1/sitemap.
Retry guidance
Not all errors are retryable. Use this table to decide:
| Code | Retry? | Guidance |
|---|---|---|
invalid_url | No | Fix the URL before retrying |
validation_error | No | Fix the request body before retrying |
unauthorized / invalid_api_key | No | Check your API key |
quota_exceeded | No | Upgrade your plan or wait for quota reset |
spending_cap_reached | No | The overage cap is account-level and has no self-service control — contact support to raise it, or upgrade the plan |
billing_entitlement_impaired | No | Settle the outstanding invoice — retrying cannot succeed while the subscription is impaired |
billing_entitlement_expired | No | Restore or renew the subscription; the entitlement period has ended |
forbidden | No | You do not have access to this resource |
rate_limited | Yes | Exponential backoff; honour the Retry-After header |
concurrency_limit | Yes | Honour the Retry-After header (a short fixed back-off — a slot frees on job completion, not a fixed window) |
internal_error | Yes | Exponential backoff; most resolve on retry |
For retriable errors, start with a 1-second delay and double on each subsequent failure, up to
a maximum of 60 seconds. Respect the Retry-After header value when it is present on 429
responses.
Example: handling errors in curl
response=$(curl -s -w "\n%{http_code}" -X POST https://api.bytekit.com/v1/scrape \
-H "Authorization: Bearer sk_live_YOUR_KEY_HERE" \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com"}')
http_code=$(echo "$response" | tail -1)
# Body is everything except the trailing status-code line — drop only the last
# line so multi-line JSON responses survive intact.
body=$(echo "$response" | sed '$d')
if [ "$http_code" -ge 400 ]; then
echo "Error $http_code: $body"
fiNext steps
- Rate Limits — quota model, concurrency slots, rate limit headers
- Scraping — when
500 internal_erroroccurs and how fallback works - API Reference — per-endpoint error codes