# Search

Search the web from one request — a working call, the options that matter, the response, what it costs, and the errors to handle.

`POST /v1/search` runs a web search and returns ranked results: a title, a URL and, when
available, a snippet for each. Send a `query` and you get back up to ten results. Everything
else — how many results, which country and language, news or images, how recent — is optional.

## Run a search

```bash
curl -X POST https://api.bytekit.com/v1/search \
  -H "Authorization: Bearer sk_live_YOUR_KEY_HERE" \
  -H "Content-Type: application/json" \
  -d '{"query": "best web scraping tools", "limit": 3}'
```

The call is synchronous: it returns `200` with the results in the response body.

```json
{
  "id": "sr_742ffcf841d24db7a2261f6a8c087469",
  "query": "best web scraping tools",
  "results": [
    {
      "position": 1,
      "title": "Best web scraping tools I've tried (and what I learned from ...",
      "url": "https://www.reddit.com/r/automation/comments/1ncuv8k/best_web_scraping_tools_ive_tried_and_what_i/",
      "snippet": "Playwright: Great for structured. Hyperbrowser: The one I'm using most now. . Playwright is powerful and well-structured, Selenium / Playwright ...",
      "date": "1 year ago"
    },
    {
      "position": 2,
      "title": "5 best AI web scraper tools I'm using in 2026 (free + paid)",
      "url": "https://www.gumloop.com/blog/best-ai-web-scrapers",
      "snippet": "5 best AI web scraper tools to use in 2026 · 1. Gumloop · 2. Octoparse · 3. Browse AI · 4. Thunderbit · 5. Firecrawl. Firecrawl web scraper. Best ...",
      "date": null
    },
    {
      "position": 3,
      "title": "Best web scraping tools (ideally premium instead of open source)?",
      "url": "https://www.reddit.com/r/Entrepreneur/comments/1pc9u5k/best_web_scraping_tools_ideally_premium_instead/",
      "date": null
    }
  ],
  "related_searches": [
    "Best web scraping tools reddit",
    "Best web scraping tools free",
    "AI web scraping tool free",
    "Best web scraper Python"
  ],
  "credits_used": 1
}
```

> This request was run for real and returned `200`. The response is shortened for the page: the
> real one listed eight `related_searches`, and four are shown. Results are live search results,
> so yours will differ.

| Field              | Description                                                                                                            |
| ------------------ | ---------------------------------------------------------------------------------------------------------------------- |
| `id`               | Search id, prefixed `sr_`                                                                                              |
| `query`            | The query that was searched                                                                                            |
| `results`          | The ranked results, best first. `position` starts at `1` and runs without gaps; the list holds at most `limit` results |
| `related_searches` | Related queries the search suggests. An empty array when there are none                                                |
| `credits_used`     | Credits this search cost (see [Cost](#cost))                                                                           |

Each item in `results` has:

| Field      | Description                                                                                                                                                                                                                                 |
| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `position` | Rank of the result, from `1`                                                                                                                                                                                                                |
| `title`    | The result's title                                                                                                                                                                                                                          |
| `url`      | The result's URL                                                                                                                                                                                                                            |
| `snippet`  | A short text excerpt. Not every result has one — in the example, result 3 has none — and it is never present for `images` results                                                                                                           |
| `date`     | When the result was published, as the search reports it. A string such as `1 year ago` (the wording follows `language`) or `null` when no date is reported. Not a fixed date format, so do not parse it as ISO. Absent for `images` results |

A search can return fewer results than `limit`, and an empty `results` array is a valid answer.

## Options

Only `query` is required. Send any of the others alongside it.

| Option       | Default | Description                                                                                                   |
| ------------ | ------- | ------------------------------------------------------------------------------------------------------------- |
| `query`      | —       | The search query, 1 to 2048 characters                                                                        |
| `limit`      | `10`    | Maximum number of results, an integer from `1` to `100`. It also sets what the call costs (see [Cost](#cost)) |
| `type`       | `web`   | Which index to search: `web`, `news`, or `images`                                                             |
| `country`    | `us`    | Two-letter country code to localize results, for example `us` or `de`                                         |
| `language`   | `en`    | Two-letter ISO 639-1 language code, for example `en` or `fr`. Region forms such as `en-US` are rejected       |
| `date_range` | `any`   | Only results from a recent window: `any` (no filter), `hour`, `day`, `week`, `month`, or `year`               |

`country` and `language` are not case sensitive. An unknown value for `type` or `date_range`, a
`limit` outside `1`–`100`, a `country` that is not two characters, or a `language` that is not a
real ISO 639-1 code is rejected with `400 validation_error`.

```bash
curl -X POST https://api.bytekit.com/v1/search \
  -H "Authorization: Bearer sk_live_YOUR_KEY_HERE" \
  -H "Content-Type: application/json" \
  -d '{
    "query": "web scraping",
    "type": "news",
    "limit": 2,
    "country": "de",
    "language": "de",
    "date_range": "week"
  }'
```

> This request was run for real and returned `200` with two results, each carrying a `source`
> (the publisher) and an `image`, and `"credits_used": 1`. The `date` values came back in German
> (`vor 3 Tagen`), following `language`.

### Result types

`type` changes which fields a result carries. The rest of the response is the same, and so is the
cost.

| `type`   | What `results` holds                                                                                                                                                                                          |
| -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `web`    | Ranked web results with `title`, `url`, `snippet` and `date`                                                                                                                                                  |
| `news`   | News articles. Adds `source` (the publisher) and `image`, a thumbnail whose only field is `url`                                                                                                               |
| `images` | Image results. Adds `source`, `domain` (the page hosting the image) and a fuller `image` object: `url`, `width`, `height`, `thumbnail_url`, `thumbnail_width`, `thumbnail_height`. Has no `snippet` or `date` |

The extra fields are present when the search reports them, so treat each as optional.

## Cost

Search is billed in credits, not bandwidth. A search costs one credit for every ten results
requested, rounded up, and never more than ten credits:

```
credits = min(ceil(limit / 10), 10)
```

| `limit`            | Credits |
| ------------------ | ------- |
| `10` (the default) | `1`     |
| `11`               | `2`     |
| `35`               | `4`     |
| `100`              | `10`    |

The charge follows the `limit` you ask for, not the number of results you get back: a search
that returns fewer results than `limit` still costs the full amount. Ask for no more than you
need. `credits_used` in the response reports what the call cost.

> The `11` row was run for real: the response held 11 results at `position` 1 through 11 and
> reported `"credits_used": 2`. The other rows follow from the formula, which
> [Credit billing](https://bytekit.com/docs/billing/credits) also states.

A search that is rejected before it runs (`400`, `401`, `402`, `429`) is not charged, and a
search that fails with `500` or `503` is refunded. For how the credit balance works, see
[Credit billing](https://bytekit.com/docs/billing/credits).

## Use it from an SDK

TypeScript:

```ts
import { ByteKit } from '@hunt-labs/bytekit-sdk';

const client = new ByteKit({ apiKey: process.env.BYTEKIT_API_KEY! });

const res = await client.search.query({ query: 'best web scraping tools', limit: 3 });
for (const r of res.results) console.log(r.position, r.title, r.url);
console.log('credits used:', res.credits_used);
```

Python:

```python
import os

from bytekit import AuthenticatedClient

client = AuthenticatedClient(token=os.environ["BYTEKIT_API_KEY"])

res = client.search("best web scraping tools", limit=3)
for r in res.results:
    print(r.position, r.title, r.url)
print("credits used:", res.credits_used)
```

> Both snippets were run for real and printed three results and `credits used: 1`. The
> endpoint accepts the same options as the table above, passed as `type`, `limit`, `country`,
> `language` and `date_range`. A non-`200` response raises an error carrying the status and the
> error `code` (`ByteKitError` in TypeScript, `errors.UnexpectedStatus` in Python). See the
> [TypeScript](https://bytekit.com/docs/sdk/typescript/search) and [Python](https://bytekit.com/docs/sdk/python/search) references.

## Errors

Errors use the unified envelope described in the [Errors guide](https://bytekit.com/docs/guides/errors). These are the
ones you are most likely to meet on this endpoint:

| Status | Code                                                                                  | When                                                                                                          | Retry?                                                 |
| ------ | ------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------ |
| `400`  | `validation_error`                                                                    | `query` is missing or empty, an option is out of range or not an allowed value, or the body is not valid JSON | No — fix the request                                   |
| `401`  | `unauthorized`, `invalid_api_key`                                                     | The API key is missing, malformed, or not valid                                                               | No — check your key                                    |
| `402`  | `quota_exceeded`                                                                      | Your credit balance cannot fund the search. The message states the credit total that is exhausted             | No — add credits or upgrade; a retry fails until then  |
| `402`  | `spending_cap_reached`, `billing_entitlement_impaired`, `billing_entitlement_expired` | The spending cap or the subscription state blocks the request — see the [Errors guide](https://bytekit.com/docs/guides/errors)        | No — resolve the billing state first                   |
| `429`  | `rate_limited`                                                                        | Too many requests per second                                                                                  | Yes — wait for the `Retry-After` header, then back off |
| `500`  | `internal_error`                                                                      | The search could not be recorded, so it did not run                                                           | Yes — back off and retry                               |
| `503`  | `search_provider_error`                                                               | The search service failed to answer                                                                           | Yes — back off and retry                               |

`400 validation_error` is the status this endpoint uses for validation failures; the
[Errors guide](https://bytekit.com/docs/guides/errors) lists it as such for `/v1/search`. The error body carries the first
failing check in `error.message` and no `error.details`:

```json
{
  "status": "failed",
  "error": {
    "code": "validation_error",
    "message": "Number must be less than or equal to 100",
    "http_status": 400,
    "failed_at": "2026-10-09T20:46:17.996Z"
  }
}
```

> This response was returned by a real request with `"limit": 101`.

A `429` carries a `Retry-After` header, in seconds. A real burst of requests past a
5-requests-per-second limit returned:

```json
{
  "status": "failed",
  "error": {
    "code": "rate_limited",
    "message": "Rate limit exceeded. Allowed 5 requests per second.",
    "http_status": 429,
    "failed_at": "2026-10-09T20:46:28.344Z"
  }
}
```

with the headers `Retry-After: 1` and `X-RateLimit-Limit: 5`. Your own limit depends on your
plan; see [Rate Limits](https://bytekit.com/docs/guides/rate-limits).

A `503 search_provider_error` does not use the unified envelope. Its body is exactly `{"error": "search_provider_error"}`
— a bare string, not an object. The [Errors guide](https://bytekit.com/docs/guides/errors) does not list this code, so
handle it here: check for a top-level `error` that is a string. The TypeScript SDK raises a
`ByteKitError` whose `code` is `search_provider_error`; the Python SDK raises
`errors.UnexpectedStatus` with `status_code` `503` and the raw body in `content`, and its `code` is
`None` for this body.

## Next steps

- [Scraping](https://bytekit.com/docs/guides/scraping) — fetch the content of a result's URL
- [Errors](https://bytekit.com/docs/guides/errors) — the error envelope and retry guidance
- [Rate Limits](https://bytekit.com/docs/guides/rate-limits) — requests per second and concurrency slots
- [Credit billing](https://bytekit.com/docs/billing/credits) — how credits are charged
- [API Reference: Search](https://bytekit.com/docs/api/search/createsearch) — full request and response schema