Search

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

For AI agents: https://bytekit.com/llms.txt — every docs page is available as markdown by appending .md

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.

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.

{
  "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.

FieldDescription
idSearch id, prefixed sr_
queryThe query that was searched
resultsThe ranked results, best first. position starts at 1 and runs without gaps; the list holds at most limit results
related_searchesRelated queries the search suggests. An empty array when there are none
credits_usedCredits this search cost (see Cost)

Each item in results has:

FieldDescription
positionRank of the result, from 1
titleThe result's title
urlThe result's URL
snippetA short text excerpt. Not every result has one — in the example, result 3 has none — and it is never present for images results
dateWhen 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.

OptionDefaultDescription
query—The search query, 1 to 2048 characters
limit10Maximum number of results, an integer from 1 to 100. It also sets what the call costs (see Cost)
typewebWhich index to search: web, news, or images
countryusTwo-letter country code to localize results, for example us or de
languageenTwo-letter ISO 639-1 language code, for example en or fr. Region forms such as en-US are rejected
date_rangeanyOnly 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.

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.

typeWhat results holds
webRanked web results with title, url, snippet and date
newsNews articles. Adds source (the publisher) and image, a thumbnail whose only field is url
imagesImage 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)
limitCredits
10 (the default)1
112
354
10010

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

Use it from an SDK

TypeScript:

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:

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 and Python references.

Errors

Errors use the unified envelope described in the Errors guide. These are the ones you are most likely to meet on this endpoint:

StatusCodeWhenRetry?
400validation_errorquery is missing or empty, an option is out of range or not an allowed value, or the body is not valid JSONNo — fix the request
401unauthorized, invalid_api_keyThe API key is missing, malformed, or not validNo — check your key
402quota_exceededYour credit balance cannot fund the search. The message states the credit total that is exhaustedNo — add credits or upgrade; a retry fails until then
402spending_cap_reached, billing_entitlement_impaired, billing_entitlement_expiredThe spending cap or the subscription state blocks the request — see the Errors guideNo — resolve the billing state first
429rate_limitedToo many requests per secondYes — wait for the Retry-After header, then back off
500internal_errorThe search could not be recorded, so it did not runYes — back off and retry
503search_provider_errorThe search service failed to answerYes — back off and retry

400 validation_error is the status this endpoint uses for validation failures; the Errors guide lists it as such for /v1/search. The error body carries the first failing check in error.message and no error.details:

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

{
  "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.

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