# 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.

```json
{
  "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`               | The [balance](https://bytekit.com/docs/billing#terms) cannot fund this request. Capture endpoints reject when it cannot fund the request's byte reservation, and `error.details.required_bytes` and `error.details.available_bytes` give the bytes the request reserves and the bytes available; `/v1/search` meters credits and rejects when its credit balance is used up                                                                                                                              |
| `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                                                                                                                                                                                                                                                                                                                                                                               |
| `422`       | `blocked`, `upstream_error`    | `/v1/fetch` only: the fetch ran and the target could not be read (bot protection refused every attempt, or another exhausted-retries failure such as an unresolvable host). This is a result about the URL, not an outage of this API, and it is charged nothing. `upstream_error` can also come from a relay fault on our side, so the code alone does not prove the target is at fault. The `X-Fetch-ID` response header, when present, names the failed request's row in Logs |
| `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`               | A ByteKit-side failure (for example, failing to enqueue a job or to persist a result, or an unexpected server error), not a problem with the target site. A target-site failure is a result instead: a `200` response with a `failed` scrape envelope on `/v1/scrape` (its `error.code` can itself be `internal_error`), or the `422` `blocked` / `upstream_error` row above on `/v1/fetch`                                                                                      |

## 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:**

```json
{
  "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:

```json
{
  "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:**

```json
{
  "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     | Retrying the same request cannot succeed while the balance is short. Compare `error.details.required_bytes` with `error.details.available_bytes`: add balance (upgrade your plan or top up), or send a smaller request. On `/v1/search`, upgrade your plan or wait for the credit reset |
| `spending_cap_reached`             | No     | The overage cap is account-level and has no self-service control — [contact support](https://bytekit.com/docs/billing/spending) 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

```bash
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"
fi
```

## Next steps

- [Rate Limits](https://bytekit.com/docs/guides/rate-limits) — usage counting, concurrency slots, rate limit headers
- [Scraping](https://bytekit.com/docs/guides/scraping) — formats, options, latency, and async polling
- [API Reference](https://bytekit.com/docs/api) — per-endpoint error codes