Every API here is available over the APIs.io API and to AI agents over MCP.
openapi: 3.2.0
info:
title: Searlo Web Data API
description: Live search results as clean JSON. The search endpoints return Google results — web, images, news, video, shopping, maps, scholar, patents and the full results page with ads, People Also Ask and AI Overviews — plus AI answers (ChatGPT, Copilot, Gemini), rank checks, web data (scrape, crawl, map, extract) and account endpoints. Get a free API key with 3,000 credits (no card required) at https://dashboard.searlo.tech. Not affiliated with or endorsed by Google LLC or any other search engine. All trademarks belong to their owners.
version: 1.2.0
contact:
name: Searlo
url: https://searlo.tech
email: hello@searlo.tech
license:
name: Proprietary API — see https://searlo.tech/terms
servers:
- url: https://api.searlo.tech/api/v1
description: Production
security:
- ApiKeyAuth: []
- BearerAuth: []
tags:
- name: Web Data
description: Retrieval. Search finds the pages; these read them — scrape one, map a site, crawl it, or extract structured records. A fetched page costs 1 credit; from 2026-11-01, a page that had to be rendered (render true, actions, or the automatic fallback) costs 5, and its `via` says `browser`. /map is unchanged. The automatic fallback is for accounts that have bought credits.
paths:
/scrape:
post:
tags:
- Web Data
summary: Scrape one page
description: 'Fetch a URL and return every requested view of it in one call: markdown, plain text, raw html, links, images, headings, tables and the page''s own JSON-LD, plus title, description, author, published date, canonical URL and Open Graph. Pages that render client-side are rendered automatically (on accounts that have bought credits). 1 credit; 2 when maxAge or cache false forces a live fetch; from 2026-11-01, 4 more when the page had to be rendered (`via: browser`). The url may be an internationalised domain name (https://www.löschwagen.com), fetched, cached and billed under its encoded form; a malformed URL or a control character in it is a 400, never charged.'
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- url
properties:
url:
type: string
maxLength: 2048
formats:
type: array
items:
type: string
enum:
- markdown
- text
- html
- links
- images
- headings
- tables
- json_ld
render:
type: boolean
description: Force a browser render (true) or never render (false). From 2026-11-01 a page fetched in a browser costs 4 credits more.
onlyMainContent:
type: boolean
default: true
chunk:
type: boolean
description: Split content into overlapping chunks
responses:
'200':
$ref: '#/components/responses/SearchResults'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'402':
$ref: '#/components/responses/InsufficientCredits'
'422':
$ref: '#/components/responses/TargetError'
'429':
$ref: '#/components/responses/RateLimited'
operationId: postScrape
x-operation-id-source: derived
/map:
post:
tags:
- Web Data
summary: List a site's URLs
description: What exists on a site, before paying to crawl it. Reads robots.txt for declared sitemaps, walks them (including sitemap indexes), and falls back to the homepage's internal links when a site publishes no sitemap. The url may be an internationalised domain name.
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- url
properties:
url:
type: string
maxLength: 2048
limit:
type: integer
minimum: 1
maximum: 5000
default: 1000
search:
type: string
description: Only URLs containing this text
includeSubdomains:
type: boolean
sitemapOnly:
type: boolean
description: Sitemaps only, no link harvesting.
responses:
'200':
$ref: '#/components/responses/SearchResults'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'402':
$ref: '#/components/responses/InsufficientCredits'
'422':
$ref: '#/components/responses/TargetError'
'429':
$ref: '#/components/responses/RateLimited'
operationId: postMap
x-operation-id-source: derived
/scrape/batch:
post:
tags:
- Web Data
summary: Batch scrape
description: 'Many URLs, one job. Returns a job id immediately; poll it or supply a webhook. URLs are normalised and de-duplicated before queueing, so the same page written two ways is one fetch and one credit — compare `submitted` with `urlCount` in the response. Nothing is discovered: traversal options are ignored, because you already named every URL. Every page is fetched live at 1 credit (5 from 2026-11-01 for a page that needed a browser): `maxAge`, `cache`, `includeLinks` and `excludeSelectors` are accepted but have no effect on a batch, and only markdown, text, html, links and json_ld are stored. Status, results, cancellation and billing all use the crawl endpoints.'
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- urls
properties:
urls:
type: array
minItems: 1
maxItems: 1000
items:
type: string
maxLength: 2048
formats:
type: array
items:
type: string
enum:
- markdown
- text
- html
- links
- json_ld
render:
type: boolean
onlyMainContent:
type: boolean
default: true
concurrency:
type: integer
minimum: 1
maximum: 8
storeContent:
type: boolean
default: true
maxAge:
type: integer
minimum: 0
maximum: 2592000
description: 'Accepted for parity with /scrape but not used by a batch: every page is fetched live.
'
webhook:
type: object
description: Called once when the job ends; not retried.
properties:
url:
type: string
secret:
type: string
responses:
'202':
description: Batch queued; the body carries the job id, urlCount and its status/results URLs.
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'402':
$ref: '#/components/responses/InsufficientCredits'
'429':
$ref: '#/components/responses/RateLimited'
operationId: postScrapeBatch
x-operation-id-source: derived
/crawl:
post:
tags:
- Web Data
summary: Start a crawl
description: 'Queue a crawl and get a job id back immediately. Credits are charged per page as it runs, so a job with `limit: 500` on a twelve-page site costs twelve; from 2026-11-01, a page that needed a browser costs 5 instead of 1. Poll the job or supply a webhook. robots.txt is obeyed by default.'
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- url
properties:
url:
type: string
maxLength: 2048
maxDepth:
type: integer
minimum: 0
maximum: 10
default: 2
limit:
type: integer
minimum: 1
maximum: 5000
default: 100
includePaths:
type: array
items:
type: string
description: Globs, e.g. /blog/*
excludePaths:
type: array
items:
type: string
formats:
type: array
items:
type: string
enum:
- markdown
- text
- html
- links
- json_ld
allowSubdomains:
type: boolean
respectRobots:
type: boolean
default: true
concurrency:
type: integer
minimum: 1
maximum: 8
default: 3
webhook:
type: object
properties:
url:
type: string
secret:
type: string
responses:
'202':
description: Crawl queued; the body carries the job id and its status/results URLs
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'402':
$ref: '#/components/responses/InsufficientCredits'
'429':
$ref: '#/components/responses/RateLimited'
operationId: postCrawl
x-operation-id-source: derived
get:
tags:
- Web Data
summary: List crawls
description: Your crawls, newest first. Free.
parameters:
- name: limit
in: query
schema:
type: integer
minimum: 1
maximum: 100
default: 20
- name: status
in: query
schema:
type: string
enum:
- queued
- running
- completed
- failed
- cancelled
responses:
'200':
$ref: '#/components/responses/SearchResults'
'401':
$ref: '#/components/responses/Unauthorized'
operationId: getCrawl
x-operation-id-source: derived
/crawl/{jobId}:
get:
tags:
- Web Data
summary: Crawl status
description: Progress, credits charged so far, and why the crawl stopped. Free.
parameters:
- name: jobId
in: path
required: true
schema:
type: string
responses:
'200':
$ref: '#/components/responses/SearchResults'
'401':
$ref: '#/components/responses/Unauthorized'
'404':
description: Crawl not found
operationId: getCrawlByJobId
x-operation-id-source: derived
delete:
tags:
- Web Data
summary: Cancel a crawl
description: Stop spending on a crawl. Pages already fetched stay readable. Free.
parameters:
- name: jobId
in: path
required: true
schema:
type: string
responses:
'200':
$ref: '#/components/responses/SearchResults'
'401':
$ref: '#/components/responses/Unauthorized'
'404':
description: Crawl not found
operationId: deleteCrawlByJobId
x-operation-id-source: derived
/crawl/{jobId}/results:
get:
tags:
- Web Data
summary: Crawl results
description: The crawled pages, paginated by cursor. Free — the pages were billed as they were fetched.
parameters:
- name: jobId
in: path
required: true
schema:
type: string
- name: limit
in: query
schema:
type: integer
minimum: 1
maximum: 200
default: 50
- name: cursor
in: query
schema:
type: string
- name: status
in: query
schema:
type: string
enum:
- completed
- failed
- skipped
- queued
- all
default: completed
responses:
'200':
$ref: '#/components/responses/SearchResults'
'401':
$ref: '#/components/responses/Unauthorized'
'404':
description: Crawl not found
operationId: getCrawlByJobIdResults
x-operation-id-source: derived
/extract:
post:
tags:
- Web Data
summary: Extract structured data
description: 'URLs plus a field schema, records out. Values come first from what the page itself declares (schema.org JSON-LD, Open Graph, meta tags), then from unambiguous patterns in the text. Every field reports its source, and a field that cannot be found comes back null and is listed in `missingFields` rather than guessed. The URLs (internationalised domain names included) are fetched several at a time inside one overall time budget (75 seconds by default): pages that arrive in time are served and billed, the rest come back as rows that say why (`errorCode`, `retryable`), and `partial` is true when the budget ran out. Billing is one credit per page actually fetched (from 2026-11-01, five for a page that had to be rendered); a failed row is free.'
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- schema
properties:
urls:
type: array
items:
type: string
maxItems: 20
url:
type: string
schema:
type: object
description: 'Field name to type: string, number, integer, float, boolean or array'
additionalProperties:
type: string
enum:
- string
- number
- integer
- float
- boolean
- array
useLlm:
type: boolean
default: true
description: 'Fall back to a language model for fields the page''s own JSON-LD, Open Graph and meta tags did not declare. Defaults to true when an LLM is configured on the deployment; has no effect otherwise. Set false to force the deterministic pass only.
'
onlyMainContent:
type: boolean
default: true
description: Strip navigation, headers and footers from the page before extracting.
responses:
'200':
description: One row per URL, in the order sent
content:
application/json:
schema:
$ref: '#/components/schemas/ExtractResponse'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'402':
$ref: '#/components/responses/InsufficientCredits'
'422':
$ref: '#/components/responses/TargetError'
'429':
$ref: '#/components/responses/RateLimited'
operationId: postExtract
x-operation-id-source: derived
/search-and-scrape:
post:
tags:
- Web Data
summary: Search and scrape in one call
description: 'Run a web search and fetch the results'' pages. Returns the results with their content attached; a page that cannot be fetched comes back with `scraped: false` beside the ones that worked. One credit for the search plus one per page actually scraped; from 2026-11-01, five for a page that needed a browser.'
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- q
properties:
q:
type: string
maxLength: 500
limit:
type: integer
minimum: 1
maximum: 20
default: 5
scrape:
type: boolean
default: true
formats:
type: array
items:
type: string
enum:
- markdown
- text
- html
- json_ld
gl:
type: string
maxLength: 5
hl:
type: string
maxLength: 5
responses:
'200':
$ref: '#/components/responses/SearchResults'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'402':
$ref: '#/components/responses/InsufficientCredits'
'429':
$ref: '#/components/responses/RateLimited'
operationId: postSearchAndScrape
x-operation-id-source: derived
components:
responses:
RateLimited:
description: 'The account''s rate limit (code 4201), the API key''s own (code 4202) or the cap on searches in flight (code 4203, errorCode CONCURRENCY_LIMIT, Retry-After 1, X-Concurrency-Limit carries the cap). Wait Retry-After seconds. See also AT_CAPACITY / BUSY / VERIFIER_BUSY, a full lane rather than a limit reached. Never charged.
'
headers:
Retry-After:
schema:
type: integer
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
Unauthorized:
description: Missing, invalid or expired API key (code UNAUTHORIZED)
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
SearchResults:
description: Search results
content:
application/json:
schema:
$ref: '#/components/schemas/SearchResponse'
BadRequest:
description: 'Invalid request (code VALIDATION_ERROR; errors lists the fields). Search endpoints also answer 400 PAGE_OUT_OF_RANGE (maxPage) and DATE_FILTER_UNAVAILABLE, with the name in errorCode. Never charged.
'
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
InsufficientCredits:
description: 'Account balance too low to complete the request (code INSUFFICIENT_CREDITS). details gives required, available and shortfall; topUpUrl links to buying more.
'
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
TargetError:
description: 'The site could not be fetched; the name is in errorCode. TARGET_REFUSED: the site refused every attempt or answered with an error of its own (targetStatus is its own status; retryable only for its 429 or 5xx). TARGET_NOT_ALLOWED: a private or reserved network address (retryable false). TARGET_UNREACHABLE: no attempt could fetch it, for example its secure-connection handshake is refused, it redirects in a loop or the page is too large (retryable false; targetError says which). Not charged.
'
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
schemas:
SearchResponse:
type: object
description: The answer is the body itself (no success/data envelope).
properties:
page:
type: integer
nextPage:
type:
- integer
- 'null'
description: The next page, or null when the results end here.
totalResults:
type: integer
credits:
type: number
description: Credits charged for this request; 0 for an answer with no results. Equals the X-Credits-Deducted header.
searchParameters:
type: object
properties:
q:
type: string
type:
type: string
gl:
type: string
hl:
type: string
num:
type: integer
organic:
type: array
items:
type: object
properties:
position:
type: integer
title:
type: string
link:
type: string
snippet:
type: string
domain:
type: string
displayedLink:
type: string
favicon:
type: string
date:
type: string
thumbnail:
type: string
breadcrumb:
type: string
ExtractResponse:
type: object
properties:
searchParameters:
type: object
properties:
type:
type: string
example: extract
urls:
type: integer
fields:
type: array
items:
type: string
mode:
type: string
enum:
- structured
- structured+llm
llmAvailable:
type: boolean
totalResults:
type: integer
description: Rows in data.
partial:
type: boolean
description: 'Present and true when the time budget ran out before every URL was fetched: the rows say which (TIMEOUT, NOT_FETCHED); the rest are served and billed.'
data:
type: array
items:
type: object
required:
- url
- success
- data
properties:
url:
type: string
success:
type: boolean
description: 'true: the page was fetched (1 credit). false: free.'
data:
type:
- object
- 'null'
additionalProperties: true
description: The record; null on a failed row.
_sources:
type: object
additionalProperties:
type: string
enum:
- json-ld
- meta
- text
- llm
missingFields:
type: array
items:
type: string
via:
type:
- string
- 'null'
llmUsed:
type: boolean
llmSkipped:
type: string
enum:
- time budget
description: The page came in too late to ask the model for its missing fields.
errorCode:
type: string
enum:
- TARGET_REFUSED
- TARGET_NOT_ALLOWED
- TARGET_UNREACHABLE
- INVALID_REQUEST
- SCRAPE_FAILED
- TIMEOUT
- NOT_FETCHED
description: On a failed row.
error:
type: string
description: 'On a failed row: what happened.'
retryable:
type: boolean
description: 'On a failed row: whether a retry may succeed.'
targetStatus:
type: integer
description: 'On a TARGET_REFUSED row: the site''s own HTTP status.'
source:
type: string
credits:
type: number
description: Pages fetched.
ErrorResponse:
type: object
properties:
success:
type: boolean
example: false
code:
description: 'Stable machine-readable name of the failure. Strings: VALIDATION_ERROR, INVALID_JSON, INVALID_IDEMPOTENCY_KEY, CITY_LOCATION_UNAVAILABLE, UNKNOWN_LOCATION, INVALID_DOMAIN, UNAUTHORIZED, INSUFFICIENT_CREDITS, FORBIDDEN, FEATURE_NOT_AVAILABLE, API_KEY_SCOPE_FORBIDDEN, NOT_FOUND, IDEMPOTENCY_REQUEST_IN_PROGRESS, PAYLOAD_TOO_LARGE, QUERY_NOT_SERVED, COUNTRY_UNAVAILABLE, IDEMPOTENCY_KEY_REUSED, TARGET_NOT_ALLOWED, AT_CAPACITY, BUSY, VERIFIER_BUSY, INTERNAL_ERROR, UPSTREAM_UNAVAILABLE, UPSTREAM_RATE_LIMITED, WARMING, UPSTREAM_TIMEOUT, CREDIT_SYSTEM_UNAVAILABLE, REVIEWS_WITHHELD, SERVICE_UNAVAILABLE, UPSTREAM_FAILED. The rate-limit 429s use numbers: 4201 (account), 4202 (API key), 4203 (searches in flight). Some 4xx carry their name in errorCode instead (see errorCode). No error is charged.
'
oneOf:
- type: string
- type: integer
example: CITY_LOCATION_UNAVAILABLE
message:
type: string
description: For people; may be reworded. Branch on code.
retryable:
type: boolean
description: Whether the same request can succeed later.
retryAfterSec:
type: integer
description: Seconds to wait before a retry (also the Retry-After header).
errors:
type: array
description: Validation errors, one per field.
items:
type: object
properties:
field:
type: string
message:
type: string
suggestions:
type: array
items:
type: string
description: 'UNKNOWN_LOCATION: close matches.'
location:
type: string
description: 'CITY_LOCATION_UNAVAILABLE: the place the request named.'
errorCode:
type: string
description: 'The name on errors that carry it here instead of in code, so read code and fall back to errorCode: PAGE_OUT_OF_RANGE (with maxPage), DATE_FILTER_UNAVAILABLE, INVALID_REQUEST, PLACE_NOT_FOUND, TARGET_REFUSED (with targetStatus), TARGET_UNREACHABLE (with targetError), INSUFFICIENT_CREDITS on POST /extract. Also set with code on 4203 (CONCURRENCY_LIMIT), FEATURE_NOT_AVAILABLE, TARGET_NOT_ALLOWED, REVIEWS_WITHHELD and every 503.
'
retryAfter:
type: integer
description: Seconds to wait, on 429 4201 to 4203 and on a retryable 503.
maxPage:
type: integer
description: 'PAGE_OUT_OF_RANGE: the last page that can be served at this limit.'
targetStatus:
type: integer
description: 'TARGET_REFUSED: the site''s own HTTP status.'
targetError:
type: string
description: 'TARGET_UNREACHABLE: why the site could not be fetched.'
details:
type: object
description: 'INSUFFICIENT_CREDITS: required, available and shortfall.'
additionalProperties:
type: number
topUpUrl:
type: string
description: 'INSUFFICIENT_CREDITS: a link to buy more credits.'
securitySchemes:
ApiKeyAuth:
type: apiKey
in: header
name: x-api-key
description: 'API key from your Searlo dashboard, e.g. sk_.... Get a free key with 3,000 credits at https://dashboard.searlo.tech (no card required).
'
BearerAuth:
type: http
scheme: bearer
description: 'The same API key, sent as Authorization: Bearer sk_.... A bearer token that does not start with sk_ is read as a dashboard login token.
'
CseKeyParam:
type: apiKey
in: query
name: key
description: 'The Searlo API key as the key query parameter, where Google''s Custom Search JSON API takes its key. Used by /customsearch/v1 only.
'
externalDocs:
description: Full documentation
url: https://searlo.tech/docs