API Documentation
Introduction
Searlo is a developer-friendly search API. Web, image, news, video, shopping, local, scholarly and full-page Google search, AI answer engines, rank checks, web data and email verification — all with a single API key.
Base URL
https://api.searlo.tech/api/v1Features
- •Web Search — Ranked web results with snippets
- •Image / Video / News Search — Media and real-time news results
- •Site & Advanced Search — Site-restricted and filtered queries (date, file type, exact/exclude terms)
- •Shopping Search — Product results
- •Maps, Places & Reviews — Local businesses, their Google reviews, attributes and menus
- •Scholar & Patents — Papers with citation counts; patents with filing and grant dates
- •Batch Search — Up to 10 queries in one request
- •Autocomplete — Google Autocomplete suggestions
- •Full SERP — Ads, People Also Ask, related searches, knowledge graph and the AI Overview
- •Rank Checks — Where a domain ranks for a keyword, with stored history
- •AI Answers — ChatGPT, Copilot and Gemini answers with their citations (Perplexity is paused)
- •Web Data — Scrape, map, crawl and extract structured data from pages
- •Async Tasks — Queue long jobs, collect results by polling or webhook
- •Email Verifier — Validate email deliverability
- •Search Analytics — Your own usage analytics
- •MCP Server — Connect AI agents (Claude, Cursor, VS Code, Windsurf)
- •Simple Auth — One API key, no OAuth complexity
Quick Start
Get search results in under 2 minutes.
1. Get Your API Key
Sign up at dashboard.searlo.tech → Dashboard → API Keys → Create Key
You'll get a key like sk_abc123...
2. Make Your First Request
Include your API key in the x-api-key header:
Authentication
All API requests require an API key, sent in the x-api-key header or as Authorization: Bearer sk_.... The same key works for the REST API and the MCP server.
Getting Your API Key
1. Sign in at dashboard.searlo.tech
2. Go to Dashboard → API Keys
3. Click Create New Key
4. Copy and save the key immediately (shown only once)
Using Your API Key
Keys start with sk_. Send it as the x-api-key request header on every call, or as Authorization: Bearer sk_....
A key is shown once, when you create it: we keep only a hash of it, and lists show a masked preview (sk_1a2b3c4d••••9f0e), never the key itself.
An API key is for the data API (search, SERP, AI answers, web data, MCP) and for read-only views of its own account: the balance (`GET /credits/balance`), usage, limits, history and GET /user/profile. Managing the account needs a dashboard login: creating or deleting API keys, exporting or deleting your data, payments and billing. An API key used on those routes gets 403 API_KEY_SCOPE_FORBIDDEN, and is not charged. A missing, invalid or expired key gets 401 UNAUTHORIZED.
Web Search
Search the web and get ranked results with titles, URLs, and snippets.
Endpoint
GET /search/web> GET /search is an alias for /search/web (it accepts q, limit, page, gl, hl only: no safe, lr, dateRestrict or cache).
Credits: 1 per request. A search that finds nothing, or fails, is not charged.
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
q | string | Yes | Search query (1–500 chars) |
limit | number | No | Results per page (1–10, default: 10) |
page | number | No | Page number (default: 1). Results are served down to about result 101: see Pages below |
safe | string | No | Safe search: active or off |
lr | string | No | Language restriction, max 50 chars (e.g. lang_en) |
gl | string | No | Country code, max 5 chars (e.g. us, gb) |
hl | string | No | Interface language code, max 5 chars (e.g. en) |
dateRestrict | string | No | Only results from the last n days, weeks, months or years: d7, w2, m1, y1. Works with gl and hl. See Date filter below |
cache | boolean | No | false skips the cached copy and searches now. Same credits (default: true) |
Response
{
"searchParameters": {
"q": "machine learning",
"type": "search",
"gl": "us",
"hl": "en",
"num": 10,
"page": 1
},
"page": 1,
"nextPage": 2,
"previousPage": null,
"totalResults": 1250000,
"searchTime": 0.42,
"organic": [
{
"position": 1,
"title": "Introduction to Machine Learning",
"link": "https://example.com/ml-intro",
"snippet": "Machine learning is a subset of AI...",
"description": "An introduction to machine learning concepts and methods.",
"displayedLink": "example.com",
"domain": "example.com",
"favicon": "https://www.google.com/s2/favicons?domain=example.com&sz=32"
}
],
"credits": 1
}Responses also set X-Cache (HIT/MISS) and credit headers (X-Credits-Deducted, X-Credits-Remaining).
Pages
Web search serves results down to about result 101. A page whose first result would come after that returns 400 PAGE_OUT_OF_RANGE with the last page it can serve, and is not charged. At limit 10 the last page is 11, at limit 5 it is 21.
{
"success": false,
"errorCode": "PAGE_OUT_OF_RANGE",
"message": "Search pages this query up to page 11 at this limit. Ask for page 11 or earlier.",
"retryable": false,
"maxPage": 11
}Date filter
dateRestrict keeps the results from a recent window: d7 is the last 7 days, w2 the last 2 weeks, m1 the last month and y1 the last year. It works with gl and hl. News Search takes the same filter, and Advanced Search takes it as dateRestrict or dateRange.
A date filter cannot be combined with search operators in q: site:, inurl:, intitle:, filetype:, before: or after:, e-mail addresses and long OR chains. That request returns 400 DATE_FILTER_UNAVAILABLE with retryable: false, and is not charged: remove the filter or the operators. A value that is not one of d, w, m or y followed by a number returns 400 VALIDATION_ERROR.
{
"success": false,
"errorCode": "DATE_FILTER_UNAVAILABLE",
"message": "This query cannot take a date filter: the lane that applies dates does not serve operator queries (site:, filetype:, OR chains, e-mail addresses). Remove the date filter or the operators.",
"retryable": false
}Custom Search JSON API (drop-in)
Google's Custom Search JSON API is closed to new customers, and existing customers lose it on January 1, 2027. Searlo answers the same request in the same JSON shape, so you move by changing the host and the key:
- https://www.googleapis.com/customsearch/v1?key=GOOGLE_API_KEY&cx=YOUR_CX&q=lectures
+ https://api.searlo.tech/customsearch/v1?key=SEARLO_API_KEY&cx=YOUR_CX&q=lecturesStep-by-step guides, with code we ran before publishing: Python, Node.js, PHP, LangChain, Open WebUI, n8n and WordPress. Google's dates and the full parameter table are on the migration page.
Endpoint
GET https://api.searlo.tech/customsearch/v1
GET https://api.searlo.tech/customsearch/v1/siterestrict
GET https://api.searlo.tech/api/v1/customsearch/v1All three paths run the same search. /customsearch/v1 is Google's own path, so a client that lets you set the API host needs no other change.
Authentication: pass your Searlo API key (sk_…) in the key parameter, where Google takes its key. The x-api-key header and Authorization: Bearer sk_… also work, and keep the key out of URLs. Searlo strips key from its own logs.
Credits: a call is billed exactly as Web Search bills one: 1 credit per call, whatever num (1 to 10) and start are. A call with searchType=image is billed as Image Search: 1 credit. A call that returns an error is not charged.
Parameters
| Parameter | Status | Notes |
|---|---|---|
q | Supported | Required. Up to 500 characters |
key | Supported | Your Searlo API key |
cx | Accepted, no effect | An engine's site list lives in your Google console, which Searlo cannot read, so Searlo searches the whole web. To restrict results, use siteSearch or site: in q |
num | Supported | 1 to 10 (default 10) |
start | Supported | 1-based. As on Google, the last result can be at most the 100th: start=91&num=10 is the last full page |
gl | Supported | Country, two letters |
cr | Supported | One country, as countryXX. Read as gl |
hl | Supported | Interface language |
lr | Supported | One language, as lang_xx. Read as the request language |
safe | Supported | active or off |
dateRestrict | Supported | d[n], w[n], m[n] or y[n]. Combining it with operator parameters (siteSearch, fileType, orTerms…) or with image search returns 400 |
siteSearch, siteSearchFilter | Supported | site: (i, the default) or -site: (e) |
exactTerms, excludeTerms, orTerms, fileType, hq, lowRange, highRange | Supported | Applied as the search operators Google applies |
searchType | Supported | image for image search |
imgSize, imgType, imgColorType, imgDominantColor | Supported | Image filters, with Google's values |
fields, prettyPrint, alt=json | Supported | Partial responses and pretty-printed JSON |
googlehost, quotaUser, userIp | Accepted, no effect | Listed in the X-Searlo-Ignored-Params header, with cx |
filter, c2coff, sort | Default only | filter=1, c2coff=0 and an empty sort are accepted. Any other value returns 400 |
linkSite, relatedSite, rights, callback | Not supported | Returns 400 with the reason |
location, uule and other city or point locations | Not supported | Country level only (gl, cr). Returns 400 |
| Any other parameter | Not supported | Returns 400, as Google does for an unknown parameter |
Response
The response has Google's customsearch#search shape:
{
"kind": "customsearch#search",
"url": {
"type": "application/json",
"template": "https://api.searlo.tech/customsearch/v1?q={searchTerms}&num={count?}&start={startIndex?}&…&alt=json"
},
"queries": {
"request": [
{
"title": "Searlo Custom Search - lectures",
"totalResults": "2410000",
"searchTerms": "lectures",
"count": 10,
"startIndex": 1,
"inputEncoding": "utf8",
"outputEncoding": "utf8",
"safe": "off",
"cx": "YOUR_CX"
}
],
"nextPage": [{ "startIndex": 11, "count": 10, "…": "the same fields" }]
},
"context": { "title": "Searlo" },
"searchInformation": {
"searchTime": 0.21,
"formattedSearchTime": "0.21",
"totalResults": "2410000",
"formattedTotalResults": "2,410,000"
},
"items": [
{
"kind": "customsearch#result",
"title": "Lectures | MIT OpenCourseWare",
"htmlTitle": "<b>Lectures</b> | MIT OpenCourseWare",
"link": "https://ocw.mit.edu/…",
"displayLink": "ocw.mit.edu",
"snippet": "Video lectures from …",
"htmlSnippet": "Video <b>lectures</b> from …",
"formattedUrl": "https://ocw.mit.edu/…",
"htmlFormattedUrl": "https://ocw.mit.edu/…"
}
]
}- •When nothing matches,
itemsis left out, as Google does.queries.nextPageappears only when more results exist. - •Image results add
mime,fileFormatandimage. Theimageobject holdscontextLink, pluswidth,height,thumbnailLink,thumbnailWidthandthumbnailHeightwhen the image source reports them.byteSizeis not returned. - •
pagemapappears only when a result has a thumbnail (pagemap.cse_thumbnail). There is nocacheId,labels,promotionsorspelling. - •Every response carries
X-Request-ID. Quote it when you contact support.
Errors
Errors use Google's envelope, so code that reads error.code, error.errors[0].reason or error.status keeps working:
{
"error": {
"code": 400,
"message": "Request contains an invalid argument.",
"errors": [
{ "message": "Request contains an invalid argument.", "domain": "global", "reason": "badRequest", "location": "num", "locationType": "parameter" }
],
"status": "INVALID_ARGUMENT",
"details": [{ "@type": "type.googleapis.com/google.rpc.RequestInfo", "requestId": "…" }]
}
}| Status | reason | When |
|---|---|---|
400 | badRequest, invalid, required | A parameter is missing or invalid |
400 | keyInvalid | The key is not a valid Searlo API key (this includes a Google AIza… key) |
400 | unsupportedParameter, cityLocationUnavailable | A parameter Searlo does not take (see above) |
403 | forbidden | No key, or the account is suspended or has not confirmed its email |
403 | dailyLimitExceeded | The account is out of credits. The message links to the top-up |
429 | rateLimitExceeded | The rate limit, or too many searches in flight. Wait for Retry-After |
503 | backendError | Search is temporarily unavailable. Wait for Retry-After |
500 | backendError | Internal error |
None of these is charged.
What differs from Google
- •
cxhas no effect: results come from the whole web, not from an engine's site list./siterestrictworks the same way. - •Results come from Searlo's web search, so ranking, snippets and the
totalResultsestimate can differ from your Google engine. - •Location is country level only, through
glorcr. Booleancrandlrexpressions are not supported. - •Running out of credits is a
403 dailyLimitExceededthat clears when you top up, not at midnight. - •Call the API from a server. Browsers on other sites are blocked by CORS, and JSONP (
callback) is not supported.
Image Search
Search for images with thumbnails and source URLs.
Endpoint
GET /search/imagesCredits: 1 per request
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
q | string | Yes | Search query (1–500 chars) |
limit | number | No | Results per page (1–10, default: 10) |
page | number | No | Page number (default: 1). Google serves about 200 images per query: at limit 10 the last page is 21 |
size | string | No | Minimum size: icon, small, medium, large, xlarge, xxlarge or huge |
imgSize | string | No | Another name for size |
imgType | string | No | Kind of image: photo, face, clipart, lineart, stock or animated |
imgColorType | string | No | Colour treatment: color, gray, mono or trans (transparent background) |
imgDominantColor | string | No | Dominant colour: black, blue, brown, gray, green, orange, pink, purple, red, teal, white or yellow |
gl | string | No | Country code, max 5 chars (e.g. us) |
hl | string | No | Interface language code, max 5 chars (e.g. en) |
cache | boolean | No | false skips the cached copy and searches now. Same credits (default: true) |
The four filters take effect and combine: size (or imgSize), imgType, imgColorType and imgDominantColor each narrow the results, and the ones applied are echoed in searchParameters. A value outside the lists above returns 400 VALIDATION_ERROR. A page past the last one Google serves ((page - 1) * limit above 200) returns 400 VALIDATION_ERROR that names the last page; it is answered at once and is not charged.
Response
{
"searchParameters": { "q": "sunset", "type": "images", "gl": "us", "num": 10, "page": 1, "imgSize": "large", "imgType": "photo" },
"page": 1,
"nextPage": 2,
"previousPage": null,
"totalResults": 50000,
"searchTime": 0.35,
"images": [
{
"position": 1,
"title": "Beautiful Sunset",
"imageUrl": "https://example.com/sunset.jpg",
"width": 1920,
"height": 1080,
"thumbnailUrl": "https://example.com/thumb.jpg",
"thumbnailWidth": 300,
"thumbnailHeight": 168,
"link": "https://example.com/gallery",
"sourceUrl": "https://example.com/gallery",
"source": "example.com",
"domain": "example.com",
"format": "image/jpeg",
"imageId": "ANd9GcQexample"
}
],
"credits": 1
}- •
imageUrlis the image itself, with its size in pixels inwidthandheight.thumbnailUrlis Google's thumbnail, with its ownthumbnailWidthandthumbnailHeight. - •
linkandsourceUrlare the page the image is on (sourceUrlfalls back to the image URL when Google names no page).sourceanddomainname that page's site. - •
formatis the image's MIME type, andimageIdGoogle's id for it. A field Google does not give comes backnull.
News Search
Search news articles from thousands of sources worldwide.
Endpoint
GET /search/newsCredits: 1 per request
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
q | string | Yes | Search query (1–500 chars) |
limit | number | No | Results per page (1–10, default: 10) |
page | number | No | Page number (default: 1) |
gl | string | No | Country code, max 5 chars (e.g. us) |
hl | string | No | Interface language code, max 5 chars (e.g. en) |
dateRestrict | string | No | Only articles from the last n days, weeks, months or years: d7, w2, m1, y1. Articles dated before the window are dropped, so a dated page can hold fewer than limit |
cache | boolean | No | false skips the cached copy and searches now. Same credits (default: true) |
Response
{
"searchParameters": { "q": "technology", "type": "news", "gl": "us", "num": 10, "page": 1 },
"page": 1,
"nextPage": 2,
"previousPage": null,
"totalResults": 12500,
"searchTime": 0.28,
"news": [
{
"position": 1,
"title": "Tech News Article",
"link": "https://news.example.com/article",
"snippet": "Article summary...",
"description": "A longer summary from the article's own metadata.",
"source": "Example News",
"date": "2026-09-28T10:30:00Z",
"thumbnail": "https://news.example.com/article.jpg"
}
],
"credits": 1
}Video Search
Search for videos across the web.
Endpoint
GET /search/videosCredits: 1 per request
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
q | string | Yes | Search query (1–500 chars) |
limit | number | No | Results per page (1–10, default: 10) |
page | number | No | Page number (default: 1) |
safe | string | No | Safe search: active or off |
gl | string | No | Country code, max 5 chars (e.g. us) |
hl | string | No | Interface language code, max 5 chars (e.g. en) |
Response
The same envelope as Web Search, with the results in videos[]. Fields a video does not have come back as null.
{
"searchParameters": { "q": "react tutorial", "type": "videos", "gl": "us", "num": 10, "page": 1 },
"page": 1,
"nextPage": null,
"previousPage": null,
"totalResults": 20,
"searchTime": 1.1,
"videos": [
{
"position": 1,
"title": "React Tutorial for Beginners",
"link": "https://video.example.com/watch?v=abc",
"thumbnailUrl": "https://video.example.com/abc.jpg",
"duration": "1:02:15",
"durationSeconds": 3735,
"source": "Example Channel",
"sourceUrl": "https://video.example.com/@example",
"publishedDate": "1 year ago",
"views": "2.1M views",
"viewCount": 2100000,
"snippet": "Learn React in 1 hour...",
"videoId": "abc"
}
],
"credits": 1
}Site Search
Restrict a web search to a single site or domain.
Endpoint
GET /search/siteCredits: 1 per request
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
q | string | Yes | Search query (1–500 chars) |
site | string | Yes | Domain or URL (e.g. example.com or https://example.com) |
limit | number | No | Results per page (1–10, default: 10) |
page | number | No | Page number (default: 1) |
Response
The same response as Web Search (results in organic[]), scoped to the given site.
Advanced Search
Run complex queries with filters: date range, file type, exact phrases, excluded terms and site restriction. Available as GET (query params) or POST (JSON body).
Endpoint
GET /search/advanced
POST /search/advancedCredits: 1 per request
Parameters
Provide the query as either query or q (one is required).
| Parameter | Type | Required | Description |
|---|---|---|---|
query / q | string | Yes | Search query (1–500 chars) |
limit | number | No | Results (1–10, default: 10) |
dateRange | string | No | One of day, week, month, year |
timeRange | string | No | Alias of dateRange (day/week/month/year) |
dateRestrict | string | No | A window as d7, w2, m1, y1 (see Date filter under Web Search). Takes precedence over dateRange |
fileType | string | No | File type filter, max 20 chars (e.g. pdf) |
exactTerms | string | No | Exact phrase to include, max 200 chars |
excludeTerms | string | No | Terms to exclude, max 200 chars |
site | string | No | Restrict to a domain/URL (e.g. example.com) |
siteSearch | string | No | Another name for site |
gl | string | No | Country code, max 5 chars (e.g. us) |
hl | string | No | Interface language code, max 5 chars (e.g. en) |
A date filter (dateRange, timeRange or dateRestrict) cannot be combined with site, fileType or search operators in q: that request returns 400 DATE_FILTER_UNAVAILABLE and is not charged. exactTerms and excludeTerms work with a date.
Response
The same response as Web Search (results in organic[]).
Shopping Search
Search product/shopping results.
Endpoint
GET /search/shoppingCredits: 1 per request
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
q | string | Yes | Search query (1–500 chars) |
limit | number | No | Results (1–55, default: 55) |
gl | string | No | Country code, max 5 chars (e.g. us) |
hl | string | No | Interface language code, max 5 chars (e.g. en) |
Response
{
"success": true,
"searchParameters": { "q": "wireless earbuds", "type": "shopping", "gl": "us", "hl": "en" },
"shopping": [
{ "title": "Wireless Earbuds Pro", "price": "$79.99", "source": "example-store.com", "link": "https://..." }
],
"source": "searlo.tech(google)"
}If no products are found you get a 200 with "shopping": []. If results are temporarily unavailable you get a 503 with "retryable": true.
Maps, Places & Reviews
Local business data from Google Maps: search the map, list the businesses that match a query in a named place, read one place's reviews, and get one place's full details: its attributes (payments, service options, children…) and menu.
Endpoints
GET /search/maps
GET /search/places
GET /search/reviews
GET /search/place-detailsCredits: 1 per request. These four endpoints also count against a tighter local rate-limit group, on top of the search group: a request must pass both (see Rate Limiting).
Maps Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
q | string | Yes | Search query (1–500 chars). A place name in the query geocodes on its own, e.g. coffee shops in austin |
limit | number | No | Results (1–20, default: 20) |
page | number | No | Page number (1–100, default: 1) |
gl | string | No | Country code, max 5 chars (e.g. us). With no ll, lat or lng it also sets where the search looks (see below) |
hl | string | No | Language code, max 5 chars (e.g. en) |
ll | string | No | Viewport as a Google ll token, max 64 chars, e.g. @30.2672,-97.7431,13z |
lat | number | No | Latitude to steer results toward (-90 to 90) |
lng | number | No | Longitude to steer results toward (-180 to 180) |
zoom | number | No | Map zoom (1–21) |
cache | boolean | No | false skips the cached copy and looks the places up now. Same credits (default: true) |
Maps Response
{
"searchParameters": { "q": "coffee shops in austin", "type": "maps", "gl": "us", "hl": "en", "num": 20, "page": 1, "ll": "@39.5,-98.35,5z" },
"lite": false,
"page": 1,
"totalResults": 20,
"places": [
{
"position": 1,
"title": "Example Coffee Roasters",
"placeId": "ChIJ7cv00Dm1RIYRx5mHcIYlGmE",
"cid": "0x8644b5a4e3d8f52b:0x5e1c2c3a4b5d6e7f",
"featureId": "/g/11c5k8tq1n",
"category": "Coffee shop",
"categories": ["Coffee shop", "Cafe"],
"rating": 4.7,
"reviews": 1834,
"price": "$$",
"address": "123 Congress Ave, Austin, TX 78701",
"latitude": 30.2672,
"longitude": -97.7431,
"website": "https://coffee.example.com/",
"domain": "coffee.example.com",
"phone": "(512) 555-0100",
"timezone": "America/Chicago",
"description": "Small-batch roaster with a patio.",
"openState": "Open · Closes 6 PM",
"openNow": true,
"businessStatus": "OPERATIONAL",
"hours": null,
"openingDate": null,
"openingDateText": null,
"attributes": {
"service_options": { "serves_dine_in": true, "has_takeout": true, "has_delivery": false },
"payments": { "requires_cash_only": false, "pay_credit_card": true, "pay_mobile_nfc": true }
},
"thumbnailUrl": "https://lh5.googleusercontent.com/...",
"reviewsLink": "https://www.google.com/maps/...",
"bookingLink": null,
"bookingLinks": null
}
],
"source": "searlo.tech(engine)",
"credits": 1
}- •
lite: truemeans Google served its reduced record. Since October 2026 that is what Google Maps shows every signed-out visitor, so expectlite: trueon maps and places: no price level, and accessibility as the only attribute group (the sample above shows the full record). A lite answer is never cached. - •
businessStatussays whether the place still trades:OPERATIONAL,CLOSED_TEMPORARILYorCLOSED_PERMANENTLY, as the Places API names them, ornullwhen Google's record does not say. It is never guessed from missing data. - •
openNowistrueorfalsefor whether the place is open right now, ornullwhen the record does not say.openStateis Google's own state line, in the language ofhl("Abierto · Cierre: 21:30"withhl=es). A place that has closed (businessStatusCLOSED_TEMPORARILYorCLOSED_PERMANENTLY) has both set tonull. - •With no
ll,latorlng,glalso sets where the search looks: the main city of that country, for 51 countries (gl=eswithfarmaciaanswers from Spain, not the US). A place named inq(hoteles en tokio) decides for itself.gl=us, nogl, and a country without a default area keep the US-wide default.searchParameters.llechoes the area used. - •In a maps result,
cidis the0x…:0x…place id,featureIdis a Knowledge Graph id (/g/…) andplaceIdis the Google Place ID (ChIJ…). - •
bookingLinkis the single link to send a user to when the place takes bookings or orders;bookingLinksholds the full set. - •
attributesis Google's "About" list for the place, grouped as Google groups it (service_options,payments,children,parking,offerings…) and keyed by Google's own ids, the same in every language. A yes/no attribute istrueorfalse(attributes.payments.requires_cash_onlyistrueon a cash-only place,falsewhere Google says it is not), a value attribute is a string (price_level), a list one an array (pay_credit_card_types_accepted). It isnullwhen Google lists none, and aliteanswer carries accessibility only. - •A place listed before it opens has
openingDateText, Google's text in yourhl("Opens 1 Nov"), andopeningDate, the date read from it ("2026-11-01"). Both arenullon a place that is open, and so isopenStateon one that is not open yet./search/placescarries the same two fields. - •A maps query that finds nothing answers
200withplaces: [], and is not charged.
Places Parameters
GET /search/places lists the businesses that match a query in a place you name, instead of a map viewport.
| Parameter | Type | Required | Description |
|---|---|---|---|
q | string | Yes | What to look for (1–500 chars), e.g. dentist |
location | string | No | Where to search, max 200 chars, e.g. Austin, Texas |
limit | number | No | Results (1–20, default: 20) |
page | number | No | Page number (1–100, default: 1) |
gl | string | No | Country code, max 5 chars |
hl | string | No | Language code, max 5 chars |
ll | string | No | Viewport token, as on maps |
lat | number | No | Latitude (-90 to 90) |
lng | number | No | Longitude (-180 to 180) |
cache | boolean | No | false skips the cached copy and looks the places up now. Same credits (default: true) |
Places Response
{
"searchParameters": { "q": "dentist", "type": "places", "location": "Austin, Texas", "gl": "us", "hl": "en", "num": 20, "page": 1 },
"lite": false,
"page": 1,
"totalResults": 20,
"places": [
{
"position": 1,
"title": "Example Family Dental",
"address": "456 Lamar Blvd, Austin, TX 78703",
"latitude": 30.2849,
"longitude": -97.7545,
"rating": 4.9,
"ratingCount": 412,
"category": "Dentist",
"categories": ["Dentist", "Cosmetic dentist"],
"priceLevel": null,
"phoneNumber": "(512) 555-0199",
"website": "https://dental.example.com/",
"domain": "dental.example.com",
"openState": "Closed · Opens 8 AM",
"openNow": false,
"businessStatus": "OPERATIONAL",
"hours": null,
"openingDate": null,
"openingDateText": null,
"attributes": { "payments": { "pay_credit_card": true, "requires_cash_only": false } },
"thumbnailUrl": "https://lh5.googleusercontent.com/...",
"cid": "0x8644b5a4e3d8f52b:0x2f1e3d4c5b6a7980",
"featureId": "0x8644b5a4e3d8f52b:0x2f1e3d4c5b6a7980",
"knowledgeGraphId": "/g/1tf6yx2q",
"placeId": "ChIJN1t_tDeuEmsRUsoyG83frY4"
}
],
"source": "searlo.tech(google_places)",
"credits": 1
}A places query that finds nothing returns 200 with places: [], and is not charged. openState, openNow, businessStatus and lite mean the same as on maps.
Reviews Parameters
GET /search/reviews returns the reviews for one place. Identify it one of four ways. placeId, featureId and cid are faster than q, which costs a place lookup first.
| Parameter | Type | Required | Description |
|---|---|---|---|
placeId | string | One of four | A Google Place ID (ChIJ…), as on a maps result or from the Places API |
featureId | string | One of four | The 0x…:0x… place id: featureId on a places result, or cid on a maps result |
cid | string | One of four | The decimal place id from a Google Maps share link (digits only, max 32). Not the 0x… value |
q | string | One of four | A place name to look up (1–500 chars); the top match is used |
limit | number | No | Reviews to return (1–50, default: 10). A call returns the first page Google serves, about eight reviews: limit can only lower that |
gl | string | No | Country code, max 5 chars |
hl | string | No | Language code, max 5 chars. Sets the language of relativeDate |
Reviews Response
{
"searchParameters": { "type": "reviews", "featureId": "0x8644b5a4e3d8f52b:0x2f1e3d4c5b6a7980", "hl": "en", "gl": "us", "num": 10 },
"place": {
"title": "Example Family Dental",
"address": "456 Lamar Blvd, Austin, TX 78703",
"featureId": "0x8644b5a4e3d8f52b:0x2f1e3d4c5b6a7980",
"rating": 4.9,
"reviewCount": 412
},
"totalResults": 8,
"hasMore": true,
"hasNextPage": true,
"nextPageToken": "CAESBkVnSUlDQQ",
"reviews": [
{
"position": 1,
"reviewId": "ChZDSUhNMG9nS0VJQ0FnSUNf",
"rating": 5,
"text": "Friendly staff and no wait.",
"date": "2026-09-14T16:20:00.000Z",
"timestamp": 1789402800,
"relativeDate": "2 weeks ago",
"language": "en",
"author": "Jordan P.",
"authorProfile": "https://www.google.com/maps/contrib/...",
"authorPhoto": "https://lh3.googleusercontent.com/...",
"authorId": "104512345678901234567",
"authorReviewCount": 37,
"isLocalGuide": true,
"response": { "text": "Thank you, Jordan!", "relativeTime": "a week ago", "date": "2026-09-21T09:00:00.000Z", "language": "en" }
}
],
"source": "searlo.tech(google_reviews)",
"credits": 1
}- •Each call returns the first page of reviews Google serves with the place, about eight, in any language:
hlsets the language ofrelativeDate, andlanguageis the one each review was written in. - •
hasMore(alsohasNextPage) istruewhen the place has more reviews than that page. Later pages cannot be fetched yet:nextPageToken, when present, is Google's token for them, and no parameter takes it.place.reviewCountis the lifetime count. - •
dateis the ISO time,timestampthe same in Unix seconds andrelativeDateGoogle's own text. - •
responseis present only when the owner replied:text,relativeTime,dateandlanguage. - •A place with no reviews answers
200withreviews: [], and is not charged. A place Google reports reviews for but sent without them answers503REVIEWS_WITHHELD(retryable: true), also not charged: retry the request.
Place Details Parameters
GET /search/place-details returns everything Google's place panel holds for one place: every field a maps result has, plus the full attribute list and the menu. Identify the place as on reviews; a placeId costs no lookup.
| Parameter | Type | Required | Description |
|---|---|---|---|
placeId | string | One of four | A Google Place ID (ChIJ…) |
featureId | string | One of four | The 0x…:0x… place id |
cid | string | One of four | The decimal place id from a Google Maps share link |
q | string | One of four | A place name to look up (1–500 chars); the top match is used |
gl | string | No | Country code, max 5 chars |
hl | string | No | Language code, max 5 chars; sets the language of labels, descriptions and the menu |
menu | boolean | No | false skips the menu lookup for a place that serves food (a faster answer when you do not need the menu) |
Place Details Response
{
"searchParameters": { "type": "place-details", "placeId": "ChIJV8rJWPR1nkcRfAVGOrZIeVs", "featureId": "0x479e75f458c9ca57:0x5b7948b63a46057c", "hl": "en", "gl": "de" },
"lite": true,
"place": {
"title": "Augustiner Klosterwirt",
"placeId": "ChIJV8rJWPR1nkcRfAVGOrZIeVs",
"cid": "0x479e75f458c9ca57:0x5b7948b63a46057c",
"featureId": "/g/1ptx9q8dt",
"category": "Bavarian restaurant",
"rating": 4.5,
"reviews": 14740,
"address": "Augustinerstraße 1, 80331 München-Altstadt-Lehel, Germany",
"website": "http://augustiner-klosterwirt.de/",
"phone": "+498955054466",
"openState": "Open · Closes 12 AM",
"openNow": true,
"businessStatus": "OPERATIONAL",
"hours": [{ "day": "Friday", "hours": ["9:30 AM–12 AM"] }],
"attributes": {
"accessibility": { "has_wheelchair_accessible_entrance": true, "has_wheelchair_accessible_restroom": true }
},
"menu": {
"url": "https://augustiner-klosterwirt.de/index.php/speisen-getraenke/speisekarte",
"sections": [
{
"name": "Soups",
"items": [{ "name": "Bavarian Liver Dumpling Soup", "description": "With fresh parsley", "price": "€7.50" }]
}
],
"photos": []
},
"menuStatus": "found"
},
"source": "searlo.tech(google_place)",
"credits": 1
}- •
placecarries every field of a maps result (openState,openNow,businessStatus,openingDate,bookingLinks…) with the same meaning. - •
menuis the menu Google holds for the place:url(the restaurant's own menu page) andsectionsof items with descriptions and prices as Google shows them, in the languagehlnames. Since October 2026 Google Maps shows signed-out visitors no menu, so for a place that serves food the menu is read from the place's Google Search panel instead.photosis empty for now. Any part Google does not have is empty, andmenuisnullwhen it has none. - •
menuStatussays how the menu lookup went:found;none(Google holds no menu for the place);pending(still being read: ask again in a few minutes and it is there, which is how a large batch gets its menus, on a second pass);unavailable(the lookup failed: retry later). It is absent for a place that serves no food. A menu is kept for 7 days, so asking again costs no wait. - •
lite: truemeans Google answered its reduced place record: since October 2026 that is what Google Maps shows every signed-out visitor, with accessibility as the only attribute group and no price level. A lite answer is never cached. - •A
placeIdthat names no business (an address or plus-code id) or aqthat matches nothing answers400, an id Google does not know404, both witherrorCode: "PLACE_NOT_FOUND"and not charged.
Scholar & Patents
Google Scholar and Google Patents results. Both use the same organic array as web results, with the scholarly or patent fields alongside.
Endpoints
GET /search/scholar
GET /search/patentsCredits: 1 per request
Scholar Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
q | string | Yes, unless cluster or cites is set | Search query (1–500 chars) |
limit | number | No | Results (1–20, default: 10) |
page | number | No | Page number (1–100, default: 1) |
hl | string | No | Language code, max 5 chars |
yearFrom | number | No | Earliest publication year (1500–2200) |
yearTo | number | No | Latest publication year (1500–2200) |
sortByDate | boolean | No | Sort by date instead of relevance |
includePatents | boolean | No | Include patents in the results (has no effect when includeCitations is false) |
includeCitations | boolean | No | Include Scholar's citation-only records (included by default); false leaves them out |
cluster | string | No | A clusterId from a result (digits, max 32): every version of that work |
cites | string | No | A citesId from a result (digits, max 32): the works that cite it |
Scholar Response
{
"searchParameters": { "q": "transformer attention", "type": "scholar", "hl": "en", "num": 10, "page": 1, "yearFrom": 2020 },
"page": 1,
"totalResults": 128000,
"hasNextPage": true,
"organic": [
{
"position": 1,
"title": "An example study of attention mechanisms",
"link": "https://arxiv.org/abs/2001.00001",
"snippet": "We study how attention...",
"domain": "arxiv.org",
"authors": ["A Author", "B Author"],
"year": 2021,
"publication": "arXiv preprint",
"type": "PDF",
"citedBy": 1520,
"citedByLink": "https://scholar.google.com/scholar?cites=2960712678066186980",
"pdfLink": "https://arxiv.org/pdf/2001.00001",
"resultId": "u8Yx3kZ1abcJ",
"citesId": "2960712678066186980",
"clusterId": "2960712678066186980"
}
],
"source": "searlo.tech(google_scholar)",
"credits": 1
}- •
citesIdandclusterIdcome back on every result. Pass one back ascitesorclusterto walk the citation graph. - •
totalResultsis Scholar's own estimate for the whole query, not the page size. - •
typeis Scholar's tag for the result (BOOK,PDF,HTML,CITATION) ornull. - •A query with no results returns
200with an emptyorganicarray.
Patents Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
q | string | Yes | Search query (1–500 chars) |
limit | number | No | Results (1–100, default: 10). Google Patents serves pages of 10, 25, 50 or 100; other values round down to one of those, and anything under 10 fetches 10 |
page | number | No | Page number (1–100, default: 1) |
sort | string | No | new or old; relevance when omitted |
country | string | No | Issuing authority, max 40 chars, e.g. US or US,EP |
language | string | No | Publication language, max 20 chars (sent upper-cased) |
status | string | No | GRANT or APPLICATION |
type | string | No | PATENT or DESIGN |
inventor | string | No | Inventor name, max 120 chars |
assignee | string | No | Assignee name, max 120 chars |
before | string | No | Upper date bound: priority:YYYYMMDD, filing:YYYYMMDD or publication:YYYYMMDD |
after | string | No | Lower date bound, same format as before |
Patents Response
{
"searchParameters": { "q": "solid state battery", "type": "patents", "num": 10, "page": 1 },
"page": 1,
"totalResults": 23000,
"totalPages": 100,
"hasNextPage": true,
"organic": [
{
"position": 1,
"title": "Solid-state battery with an example electrolyte",
"link": "https://patents.google.com/patent/US11000000B2/en",
"snippet": "A solid-state battery comprising...",
"patentId": "patent/US11000000B2/en",
"publicationNumber": "US11000000B2",
"inventor": "Jane Inventor",
"assignee": "Example Corp",
"priorityDate": "2019-05-14",
"filingDate": "2020-05-12",
"grantDate": "2021-05-11",
"publicationDate": "2021-05-11",
"language": "en",
"pdfLink": "https://patentimages.storage.googleapis.com/.../US11000000.pdf",
"thumbnailUrl": "https://patentimages.storage.googleapis.com/...",
"figures": [
{ "thumbnail": "https://patentimages.storage.googleapis.com/...", "full": "https://patentimages.storage.googleapis.com/..." }
]
}
],
"source": "searlo.tech(google_patents)",
"credits": 1
}- •
totalPagesandhasNextPagecome from Google Patents' own counts. - •
figureslists the drawings, each as athumbnailand afullimage URL. - •A query with no results returns
200with an emptyorganicarray.
Simple Search
Legacy web search that returns a simplified, flat result format. Use Web Search for new integrations.
Endpoint
GET /search/simpleCredits: 1 per request
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
q | string | Yes | Search query (1–500 chars) |
limit | number | No | Results (1–10, default: 10) |
Response
{
"success": true,
"data": { "results": [ { "title": "...", "link": "...", "snippet": "..." } ] },
"message": "Simple search completed successfully"
}Autocomplete (Suggestions)
Google Autocomplete: the suggestions Google offers while a query is being typed.
Endpoint
GET /search/suggestionsCredits: 0.5 per request
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
q | string | Yes | Partial query (1–200 chars) |
limit | number | No | Number of suggestions (1–10, default: 10) |
hl | string | No | Language code (2–5 chars, default: en) |
Response
{
"success": true,
"data": {
"query": "machine lear",
"suggestions": ["machine learning", "machine learning python", "machine learning course"],
"source": "searlo.tech(engine)",
"timestamp": "2026-09-29T10:00:00.000Z"
},
"message": "Suggestions retrieved successfully"
}The suggestions are in data.suggestions, as plain strings. An upstream failure returns 502 or 503 with success: false, and the credits are refunded.
Trending (not available)
GET /search/trending is not available. The route exists but has no data source behind it: it answers 200 with an error object in data instead of trending queries. Do not build on it.
Batch Search
Run multiple web searches in a single request.
Endpoint
POST /search/batchCredits: 1 credit per distinct query that found something, up to 10 queries. A query that failed or found nothing costs nothing, and a query repeated in the batch (ignoring case) is charged once; each item's result.credits and data.credits say what was charged.
Body
| Field | Type | Required | Description |
|---|---|---|---|
queries | string[] | Yes | 1–10 query strings (each 1–500 chars) |
Response
{
"success": true,
"data": {
"success": true,
"results": [
{ "query": "react", "result": { "searchParameters": { "q": "react" }, "organic": [ /* ... */ ], "credits": 1 }, "source": "searlo.tech(engine)", "timestamp": "2026-10-05T09:00:00.000Z" },
{ "query": "vue", "result": { "searchParameters": { "q": "vue" }, "organic": [ /* ... */ ], "credits": 1 }, "source": "searlo.tech(engine)", "timestamp": "2026-10-05T09:00:00.000Z" }
],
"totalQueries": 2,
"credits": 2
},
"message": "Batch search completed successfully"
}Full SERP
Google's whole results page as JSON: organic results, videos, ads, People Also Ask, related searches, the knowledge graph and the AI Overview. Each page is fetched live from Google and parsed, in a few seconds, AI Overview included: the overview is read in full from the page as Google served it. An overview Google loads after the page is read from the rendered page instead, which adds several seconds. Web Search returns organic results only, for 1 credit, in about a second.
Endpoints
GET /serp
POST /serp/batch
GET /serp/features
GET /search/news/serp> GET /search/serp is an alias of GET /serp: same parameters, same price.
Credits: 3 per page delivered (pages asks for up to 10; a page Google does not have, or one that repeats the page before it, is not billed), +1 with rawHtml=true. A SERP with no results is not charged. An AI Overview read in full from the page as served is included at no extra charge. From 2026-11-01, a request whose AI Overview had to be completed by rendering the page costs 5 more (8 for a one-page request), and the response says aioverview.rendered: true; with aioverview=false it never does. Until then it stays 3. A cached result is billed like a live one, rendered or not. credits in the body and the X-Credits-Deducted header say what was charged.
Search operators (site:, intitle:, inurl:, filetype:, before:/after:), e-mail lookups and long OR chains are not served: they answer 422 QUERY_NOT_SERVED with retryable: false, not charged.
> Want Google's page itself rather than JSON, or the page an iPhone gets? See SERP HTML.
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
q | string | Yes | Search query (1–400 chars) |
gl | string | No | Country, 2 letters (default: us) |
hl | string | No | Language, 2–5 chars (default: en) |
location | string | No | A country, by name or two-letter code (max 120 chars), e.g. Germany or DE. Applied as gl, and takes precedence over it. A city or state returns 400 CITY_LOCATION_UNAVAILABLE (see Location); an unknown name returns 400 UNKNOWN_LOCATION with suggestions. Neither is charged. Cannot be combined with uule |
uule | string | No | A Google uule token (max 300 chars). A country's token is applied as that country's gl; any other token (a city, a state, coordinates) returns 400 CITY_LOCATION_UNAVAILABLE |
device | string | No | desktop (default). mobile and tablet are accepted but return the desktop layout; for the page an iPhone gets, use SERP HTML with device=ios |
page | number | No | First results page to fetch (1–100, default: 1) |
pages | number | No | Consecutive pages to fetch (1–10, default: 1). Each page delivered is billed; pagesFetched says how many came back |
num | number | No | Results per page (1–10, default: 10). Google serves at most 10 per page: ask for more with pages |
tbs | string | No | Google time or sort filter, max 40 chars. Only qdr:h, qdr:d, qdr:w, qdr:m and qdr:y (optionally with a number, e.g. qdr:d3), li:1 and sbd:1 are applied; other values are ignored |
safe | string or boolean | No | active (or true) turns SafeSearch on; off (or false, the default) leaves it off. Cached apart from SafeSearch-off results |
aioverview | boolean | No | Include the AI Overview (default: true). false drops the aioverview key and skips reading it in full, which makes the call faster |
googleGoto | boolean | No | Resolve Google /goto redirect links in the AI Overview sources (default: true) |
googleGotoOrganic | boolean | No | Also resolve /goto links on organic results (default: false) |
cache | boolean | No | false skips the cache and fetches live (default: true) |
rawHtml | boolean | No | true adds rawHtml: the page's HTML as Google served it, alongside the JSON (+1 credit). A result with the page is cached separately from one without it. For the rendered page, use SERP HTML with js=true |
Response
{
"success": true,
"searchParameters": { "q": "best running shoes", "gl": "us", "hl": "en", "page": 1, "pages": 1, "num": 10, "device": "desktop", "type": "serp" },
"organic": [
{
"position": 1,
"title": "The Best Running Shoes of 2026",
"url": "https://www.example.com/best-running-shoes",
"redirectUrl": null,
"resolved": true,
"displayedUrl": "https://www.example.com › reviews",
"domain": "example.com",
"snippet": "We ran more than 500 miles in 40 pairs...",
"date": "Sep 12, 2026",
"sitelinks": [{ "title": "Road shoes", "url": "https://www.example.com/road" }],
"page": 1
}
],
"videos": [
{ "position": 1, "title": "Best Running Shoes 2026", "url": "https://www.youtube.com/watch?v=abc123", "redirectUrl": null, "resolved": true, "source": "YouTube", "channel": "Example Running" }
],
"ads": [
{ "position": 1, "block": "top", "title": "Running Shoes Sale", "url": "https://shop.example.com/", "displayedUrl": "shop.example.com", "description": "Free shipping on all orders." }
],
"peopleAlsoAsk": [{ "question": "What running shoes do podiatrists recommend?" }],
"relatedSearches": [{ "query": "best running shoes for women" }],
"knowledgeGraph": null,
"aioverview": {
"text": "The best running shoe depends on your gait and distance...",
"markdown": "The best running shoe depends on your gait and distance...",
"sources": [
{ "position": 1, "label": "Example Running", "title": "Best running shoes, tested", "supportingCount": 0, "url": "https://www.example.com/tested", "redirectUrl": null, "resolved": true, "domain": "example.com" }
],
"complete": true
},
"aiModeAvailable": true,
"pagesFetched": 1,
"gotoResolved": 4,
"fetchedAt": "2026-09-29T10:00:00.000Z",
"source": "searlo.tech(google_serp)",
"cached": false
}- •Organic results carry the destination in
url(Web Search useslink). When Google wrapped a result in a/gotoredirect,redirectUrlholds that link andresolvedsays whetherurlis the final destination.gotoResolvedcounts the links resolved for you. - •
peopleAlsoAsklists the questions only (question), not their answers.relatedSearchesitems carryquery.knowledgeGraph(title,type,description,attributes,website) isnullwhen the page has none. - •
aioverviewisnullwhen Google showed no AI Overview; itssourcesare the pages it cited.completeistruewhen the overview was read in full, from the page as served or from the rendered page, andfalsewhen only part of it could be read.pending: truewith an emptytextmarks an overview Google loads after the page;GET /serpthen reads it from the rendered page.rendered: truemarks an overview completed that way, which from 2026-11-01 makes the request cost 5 more (see Credits). Itssourcescome back resolved:urlis the cited page andredirectUrlGoogle's/gotolink.aiModeAvailablesays whether the page offered an AI Mode tab. - •With
pagesabove 1, organic results from the later pages are appended with continuingpositionvalues and their ownpage. Ads, videos, People Also Ask, related searches, the knowledge graph and the AI Overview come from the first page only. - •
positioncounts from 1 within the response: withpage=3the first organic result isposition1 onpage3. - •
pagesFetchedcan be lower thanpageswhen a later page did not render. Billing follows the pages you requested. - •Responses set
X-Cache(HIT/MISS). Identical requests in flight at the same moment share one fetch, and each of them is billed.
Location
Every Google endpoint applies the country and the language: gl sets the country and hl the language. location takes a country too, by name or two-letter code, and becomes its gl.
City- and state-level location is not available yet. In our tests (September 2026) Google no longer localizes a results page by uule, so a city, a state or a non-country uule is refused, and not charged, rather than answered with results for somewhere else:
{
"success": false,
"code": "CITY_LOCATION_UNAVAILABLE",
"retryable": false,
"message": "City- and state-level location is not available yet. Country (gl) and language (hl) are applied: pass the country as gl, or as location (for example \"Germany\").",
"location": "Austin,Texas,United States"
}The same rule applies to POST /serp/batch, GET /serp/features, GET /search/news/serp, SERP HTML, Rank Checks and the serp, news/serp and rankings/check async tasks.
Errors
| Status | code | Meaning |
|---|---|---|
400 | VALIDATION_ERROR | A parameter is missing or out of range (errors lists which) |
400 | CITY_LOCATION_UNAVAILABLE | location or uule names a city or a state (see Location) |
400 | UNKNOWN_LOCATION | location is not a country we recognise; suggestions lists close matches |
429 | AT_CAPACITY | The endpoint is at capacity. Retry after Retry-After; a retry before then is refused |
403 | FEATURE_NOT_AVAILABLE | The endpoint is switched off on this deployment (retryable: false) |
422 | QUERY_NOT_SERVED | A query shape this endpoint does not serve: search operators (site:, inurl:, intitle:, filetype:, before: / after:), e-mail lookups and long OR chains (retryable: false) |
422 | COUNTRY_UNAVAILABLE | This gl cannot be served right now |
503 | UPSTREAM_UNAVAILABLE | No usable results page was available within the call (retryable: true). Retry after the short Retry-After |
Failed calls are refunded. The account rate limit 429 (code 4201 or 4202) is separate from AT_CAPACITY.
Batch Endpoint
POST /serp/batchCredits: 3 per distinct keyword per page delivered. The batch reserves 3 × keywords × pages when it is accepted (plus 5 per keyword with aioverview: true from 2026-11-01) and is charged what it delivered: a keyword that failed or found nothing costs nothing. Duplicate keywords (ignoring case) are billed once. 20 keywords at 1 page cost at most 60 credits without rendered overviews. From 2026-11-01, with aioverview: true, a keyword whose AI Overview had to be completed by rendering the page costs 5 more (aioverview.rendered: true on its row). Each row's credits and credits.used say what was charged.
Up to 20 keywords run synchronously in one request, with one set of settings for all of them; meta.max_sync_batch reports the cap in force. For larger runs, queue serp tasks with Async Tasks.
| Field | Type | Required | Description |
|---|---|---|---|
keywords | string[] | Yes | 1–20 keywords, each 1–400 chars |
gl | string | No | Country, 2 letters (default: us) |
hl | string | No | Language, 2–5 chars (default: en) |
location | string | No | A country, as on GET /serp. A city or state refuses the whole batch with 400 CITY_LOCATION_UNAVAILABLE before any keyword runs (not charged). Cannot be combined with uule |
uule | string | No | As on GET /serp |
device | string | No | desktop (default). mobile and tablet are accepted and return the desktop layout, as on GET /serp |
pages | number | No | 1–10 (default: 1) |
num | number | No | 1–100 (default: 10) |
tbs | string | No | As on GET /serp |
safe | string or boolean | No | As on GET /serp: active or off. Every keyword in the batch is searched with it |
aioverview | boolean | No | Include the AI Overview (default: false in a batch) |
googleGoto | boolean | No | Default: true |
cache | boolean | No | Default: true |
{
"schema": "serp-batch.v1",
"results": [
{
"keyword": "running shoes",
"success": true,
"searchParameters": { "q": "running shoes", "gl": "us", "hl": "en", "page": 1, "pages": 1, "num": 10, "device": "desktop", "type": "serp" },
"organic": [{ "position": 1, "title": "Running Shoes", "url": "https://www.example.com/running" }],
"videos": [],
"ads": [],
"peopleAlsoAsk": [{ "question": "How often should you replace running shoes?" }],
"relatedSearches": [{ "query": "running shoes sale" }],
"knowledgeGraph": null,
"aiModeAvailable": true,
"pagesFetched": 1,
"gotoResolved": 0,
"fetchedAt": "2026-09-29T10:00:00.000Z",
"source": "searlo.tech(google_serp)",
"cached": false
},
{
"keyword": "trail running shoes",
"success": false,
"status": 429,
"code": "AT_CAPACITY",
"retryable": true,
"message": "Google SERP is at capacity. Retry in 60s — immediate retries will be refused."
}
],
"credits": { "used": 6 },
"meta": { "requested": 2, "distinct": 2, "succeeded": 1, "failed": 1, "pages": 1, "gl": "us", "hl": "en", "device": "desktop", "max_sync_batch": 20 }
}A keyword that fails inside a 200 batch is not billed (since 2026-10-05). The batch answers 200 whenever it ran; read meta.failed and each failed row's retryable before you resubmit.
Features Endpoint
GET /serp/featuresCredits: 3 per request (one results page). It only checks whether an AI Overview is present, so it never renders and never costs more.
Which features a query's results page shows, without the page itself. Parameters: q (required, 1–400 chars), gl, hl, location (a country) or uule, device, safe and cache, as on GET /serp.
{
"schema": "serp-features.v1",
"query": "best running shoes",
"features": ["organic", "ads", "ai_overview", "people_also_ask", "related_searches", "video", "sitelinks"],
"counts": { "organic": 10, "ads": 3, "shopping": 0, "videos": 3, "people_also_ask": 4, "related_searches": 8, "local_pack": 0, "top_stories": 0, "image_pack": 0 },
"organic_count": 10,
"credits": { "used": 3 },
"meta": { "gl": "us", "hl": "en", "device": "desktop", "cached": false, "fetchedAt": "2026-09-29T10:00:00.000Z" }
}Feature names come from this list: organic, ads, shopping, ai_overview, ai_mode, featured_snippet, people_also_ask, related_searches, knowledge_graph, video, local_pack, sitelinks, top_stories, image_pack, twitter, faq. The results-page parse does not currently return shopping, local pack, top stories, image pack, Twitter, FAQ or featured-snippet blocks, so those names do not appear today and their counts stay 0.
News Results Page
GET /search/news/serpCredits: 2 per page requested.
Google's News tab, fetched live and parsed: cards with title, source, date, snippet and thumbnail, about a second per page. Parameters: q (required, 1–400 chars), gl, hl, location (a country) or uule, page (1–100), pages (1–10), tbs (e.g. qdr:d for the past day), googleGoto (default: true, resolves the cards' /goto links to the articles) and cache, as on GET /serp.
Cards come in Google's order, with continuing position values and the page each came from. For a developing story Google shows several articles as a group, and each is its own card, so a page can hold more than 10. snippet is empty for grouped articles, which Google shows without one. hasNextPage says whether Google offered a further page.
{
"success": true,
"searchParameters": { "q": "electric vehicles", "gl": "us", "hl": "en", "page": 1, "pages": 1, "type": "news", "mode": "serp" },
"news": [
{ "position": 1, "title": "EV sales rise again", "url": "https://news.example.com/ev-sales", "redirectUrl": null, "resolved": true, "source": "Example News", "snippet": "Sales of electric vehicles...", "date": "3 hours ago", "thumbnail": "https://news.example.com/thumb.jpg", "page": 1 }
],
"hasNextPage": true,
"pagesFetched": 1,
"gotoResolved": 18,
"fetchedAt": "2026-09-29T10:00:00.000Z",
"source": "searlo.tech(google_news_serp)",
"cached": false
}SERP HTML (raw & rendered)
Google's results page as HTML, for callers that parse the page themselves. Two ways, one endpoint:
- •Without JavaScript (default): the full results page exactly as Google's server sent it. Nothing is extracted, rewritten or re-serialised: result links are Google's own
/gotoredirects, as on the page. Typically 2–3 seconds, about 1 MB of HTML. - •With JavaScript (
js=true): the same page rendered, with the AI Overview opened out: finished streaming, with its Show more and Show all sections expanded in the DOM that comes back. Typically 8–15 seconds. Images, fonts and media are not downloaded; the HTML references them.
Both work for desktop and for iOS (device=ios): the page Google serves to Safari on an iPhone.
Endpoint
GET /serp/htmlCredits: 2 per page; 8 per page with js=true. Failed calls are refunded. Never cached: each call fetches the page at request time.
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
q | string | Yes | Search query (1–400 chars) |
gl | string | No | Country, 2 letters (default: us) |
hl | string | No | Language, 2–5 chars (default: en) |
page | number | No | Results page (1–10, default: 1) |
tbs | string | No | Google time or sort filter, max 40 chars (e.g. qdr:w) |
safe | string | No | active turns SafeSearch on; off |
device | string | No | desktop (default) or ios |
js | boolean | No | true returns the rendered page with the AI Overview opened out (default: false) |
expandAio | boolean | No | With js=true: return the AI Overview with its Show more and Show all sections expanded (default: true). false returns the rendered page as first shown |
format | string | No | json (default) wraps the page in a JSON envelope; raw answers with the page itself as text/html |
location | string | No | A country, by name or two-letter code (e.g. Germany, DE), applied as gl. A city or state returns 400 CITY_LOCATION_UNAVAILABLE and is not charged (see Location). Cannot be combined with uule |
uule | string | No | A country's uule is applied as its gl; any other token returns 400 CITY_LOCATION_UNAVAILABLE |
One call is one results page: page=2 is results 11–20. Google serves about ten organic results a page and has not honoured a larger num since 2025, so there is no num here. Two identical calls are two fetches, and two pages.
Raw or rendered?
| Raw (default) | Rendered (js=true) | |
|---|---|---|
| Credits | 2 | 8 |
| Typical time | 2–3 s (p95 about 7 s) | 8–15 s |
| HTML | about 1 MB | 1–2 MB |
| What it is | The bytes Google's server sent, before any JavaScript ran | The DOM after Google's JavaScript ran, with the AI Overview opened out |
| Organic results, ads, People Also Ask questions, related searches, knowledge panel | Yes | Yes |
| AI Overview | Its first part only: Google streams the rest in after the page loads (in our measurements a median of 39% of the full text, and 1–4 of its sources) | In full, every source included (Show more and Show all pressed) |
| Links | Google's /goto redirects, as served | As on the rendered page |
iOS (device=ios) | Yes | Yes |
Raw is the choice for rank and SERP-feature data at volume. Rendered is for the AI Overview's full text and sources, or the page as a person sees it. For the overview as JSON rather than HTML, Full SERP reads it in full for 3 credits (from 2026-11-01, 8 when it has to render the page to complete the overview).
Response (format=json)
{
"success": true,
"searchParameters": { "q": "how does compound interest work", "type": "rendered", "gl": "us", "hl": "en", "page": 1, "device": "desktop" },
"html": "<!doctype html><html itemscope=\"\" itemtype=\"http://schema.org/SearchResultsPage\" lang=\"en\">…",
"url": "https://www.google.com/search?q=how+does+compound+interest+work&hl=en&gl=us",
"empty": false,
"rendered": true,
"aio": { "present": true, "expanded": true, "sourcesOpened": true, "showAll": "pressed", "sourcesBefore": 7, "sources": 7, "sourcesVisible": 11 },
"aiOverview": {
"text": "Compound interest is interest earned on both the principal and the interest already added to it...",
"markdown": "Compound interest is interest earned on both the principal and the interest already added to it...",
"sources": [{ "position": 1, "label": "Investopedia", "url": "https://www.google.com/goto?url=…", "resolved": false }]
},
"bytes": 2089123,
"fetchedAt": "2026-09-29T18:39:40.000Z",
"source": "searlo.tech(google_serp_rendered)",
"credits": { "used": 8 }
}| Field | Description |
|---|---|
html | The page. Without js: the bytes Google's server sent. With js: the DOM after the page's JavaScript ran and the AI Overview was opened out |
url | The results page's URL |
empty | true when Google answered with a results page that has no results (it is still the page, and still returned) |
rendered | Whether JavaScript ran (js=true) |
aio | With js=true, what happened to the AI Overview: present (Google showed one), expanded (Show more took), sourcesOpened (every source is on show: Show all took, or there was nothing left to open), showAll (what the Show all step found: pressed; absent, no Show all to press; complete, every source already on screen; failed, the press took nowhere with sources still hidden), sources (unique source links on the page), sourcesVisible (source links on screen) |
aiOverview | With js=true on desktop: the overview parsed from the captured page (text, markdown, sources). null on iOS: the mobile layout is in the html only |
bytes | Size of html in bytes (UTF-8) |
Response (format=raw)
The body is the page itself, Content-Type: text/html; charset=utf-8. The metadata moves to headers:
| Header | Value |
|---|---|
X-Searlo-Url | The results page's URL |
X-Searlo-Fetched-At | When it was fetched (ISO 8601) |
X-Searlo-Empty | true for a results page with no results |
X-Searlo-Rendered | true when JavaScript ran |
X-Searlo-Aio | With js=true: present=…; expanded=…; sources-opened=…; sources=…; show-all=… |
X-Credits-Used | Credits this call took |
AI Overview notes
- •Google decides whether an AI Overview appears, per query and per request.
aio.presenttells you; the page is billed either way. - •Desktop: the overview renders in two parts once opened; all of its sources are in the returned HTML, and Show all makes them visible. A short sources list is shown in full without one (
showAllabsentorcomplete). - •iOS: the mobile overview has no Show all control (
showAll: "absent",sourcesOpened: true); its sources are cards inside the overview, all in the HTML. Its Show more lifts a height limit rather than adding content, so on iOSaio.expandedmeans the tap went through. The overview's full text is in the HTML once it has finished streaming, which is what the render waits for. - •An AI Overview is non-deterministic: the same query can return a different overview, with different sources, from one request to the next.
iOS
device=ios returns the mobile layout Google serves to Safari on an iPhone, from an emulated iPhone rather than a physical device.
Latency, capacity and timeouts
Measured through the public API (Cloudflare, our edge, the SERP hosts) in September 2026:
| p50 | p95 | Success | |
|---|---|---|---|
| Raw, 6 concurrent | 3.1 s | 7.6 s | 99.3% of 302 calls |
| Rendered, 3 concurrent | 9.2 s | 11.9 s | 100% of 60 calls |
Pages are fetched on dedicated SERP hosts. One host sustains at least 5 raw pages a second (97% or better at 16 concurrent requests, the most we have driven it) and about 0.7 rendered pages a second (rendering is CPU-bound), and capacity grows by adding hosts. For sustained volume, tell us your daily volume and peak rate per mode, and capacity is reserved for your key before you ramp.
Set client timeouts of 30 s for raw pages and 90 s for rendered ones. The server gives up at about 92 s and answers either way: a call that runs out of time is a 503 and is not charged.
Retries
- •
429BUSY: the endpoint is at capacity (withjs=true, rendering capacity). WaitRetry-Afterseconds (1–10) and retry. - •
503withretryable: true(UPSTREAM_UNAVAILABLE): no usable page was available within the call. Retry with backoff, for example after 2, 5 and 10 seconds. - •
400and422: retrying the same request gives the same answer. - •A failed call is never charged, so a retry cannot bill twice: you pay once, for the call that returns the page.
Errors
| Status | code | Meaning | Retry |
|---|---|---|---|
400 | VALIDATION_ERROR | Invalid parameters, or a device other than desktop / ios | No |
400 | CITY_LOCATION_UNAVAILABLE | location or uule names a city or a state | No |
400 | UNKNOWN_LOCATION | location is not a country we recognise (suggestions lists close matches) | No |
402 | INSUFFICIENT_CREDITS | Not enough credits for the call | After a top-up |
422 | QUERY_NOT_SERVED | A query shape this endpoint does not serve: search operators (site:, inurl:, intitle:, filetype:, before: / after:), e-mail lookups and long OR chains | No |
422 | COUNTRY_UNAVAILABLE | This gl cannot be served right now | Later |
429 | BUSY | At capacity (with js=true, rendering capacity) | After Retry-After |
429 | 4201 / 4202 | The account or API-key rate limit | After Retry-After |
403 | FEATURE_NOT_AVAILABLE | Switched off on this deployment (retryable: false) | No |
503 | UPSTREAM_UNAVAILABLE | No usable page was available within the call (retryable: true) | Yes, with backoff |
None of them is charged. A failure answers JSON even with format=raw:
{
"success": false,
"code": "QUERY_NOT_SERVED",
"retryable": false,
"message": "This query shape is not served as raw HTML: search operators (site:, inurl:, intitle:, filetype:, before:/after:), e-mail lookups and long OR chains."
}Going to production
- •Use
format=rawwhen you store or parse the page: it arrives astext/htmlwith no JSON escaping, and the metadata is in theX-Searlo-*headers. - •Send
Accept-Encoding: gzip. A raw page is about 1 MB of HTML and compresses to a fraction of that. - •Honour
Retry-Afteron every429, and retry503s markedretryable: truewith backoff. - •Use the timeouts above: 30 s raw, 90 s rendered.
- •Track spend from
X-Credits-Used(credits.usedin JSON) andX-Credits-Remaining. - •Read `GET /search/capabilities` for the endpoint's live state and its success rate over the last two hours.
- •A key's default caps are 20,000 requests a minute and 400,000 a day (Rate Limiting). Ask us for custom limits and reserved capacity before a large ramp.
Rank Checks
Where a domain ranks for a keyword, read from a live Google results page: the position, the page that ranks and the domains above it. Every check is stored, so the history builds up from your own checks.
Endpoints
POST /rankings/check
POST /rankings/batch
GET /rankings/historyCredits: 3 per 10 results of depth, because each 10 results is one results page read. The default depth of 100 costs 30 credits; depth 10 costs 3. A batch costs that for each distinct keyword. History is free.
Check Parameters
| Field | Type | Required | Description |
|---|---|---|---|
keyword | string | Yes | 1–400 chars |
domain | string | Yes | 3–253 chars. Reduced to its registrable domain (blog.example.com becomes example.com): any subdomain counts as a match, and url says which page ranked |
depth | number | No | How many results to search (10–100, default: 100), read in pages of 10. depth 25 reads 3 pages and costs 9 |
gl | string | No | Country, 2 letters (default: us) |
hl | string | No | Language, 2–5 chars (default: en) |
device | string | No | desktop or mobile (default: desktop). Positions are read from Google's desktop results page for both today; device is recorded with the check as asked |
location | string | No | A country, by name, two-letter code or uule (max 300 chars). Applied as gl and echoed in the result as location. A city or state returns 400 CITY_LOCATION_UNAVAILABLE before any credit is taken, for a batch too (see Location) |
cache | boolean | No | Reuse a matching cached results page (default: true) |
Check Response
{
"schema": "ranking-check.v1",
"result": {
"keyword": "project management software",
"domain": "example.com",
"position": 4,
"url": "https://www.example.com/features",
"title": "Project management features | Example",
"found": true,
"search_depth": 30,
"organic_count": 30,
"pages_fetched": 3,
"gl": "US",
"hl": "en",
"device": "desktop",
"serp_features": ["organic", "ads", "people_also_ask", "related_searches", "sitelinks"],
"competitors_above": ["asana.com", "monday.com", "clickup.com"],
"checked_at": "2026-09-29T10:00:00.000Z",
"cached": false
},
"credits": { "used": 9 }
}- •
found: falsewithposition: nullmeans the domain was not in the topsearch_depthresults.search_depthis what was asked for, so a short results page does not change whatfound: falsemeans. - •
competitors_abovelists up to 20 domains ranking above yours, top first. When the domain was not found, it lists the domains on the page. - •
serp_featuresuses the SERP features vocabulary. A rank check skips the AI Overview, soai_overviewnever appears here. - •A failed results-page read returns
429(AT_CAPACITY) or503(UPSTREAM_UNAVAILABLE), as on/serp, and is refunded. An unparseabledomainis400INVALID_DOMAIN.
Batch Endpoint
POST /rankings/batchOne domain, up to 20 keywords: keywords (1–20, each 1–400 chars) and domain, plus the same depth, gl, hl, device, location and cache fields as a check. Each keyword is a separate check and a separate history entry. meta.max_sync_batch reports the cap in force.
{
"schema": "ranking-batch.v1",
"results": [
{ "success": true, "keyword": "project management software", "domain": "example.com", "position": 4, "url": "https://www.example.com/features", "title": "Project management features | Example", "found": true, "search_depth": 30, "organic_count": 30, "pages_fetched": 3, "gl": "US", "hl": "en", "device": "desktop", "serp_features": ["organic", "ads"], "competitors_above": ["asana.com"], "checked_at": "2026-09-29T10:00:00.000Z", "cached": false },
{ "keyword": "kanban board software", "success": false, "code": "UPSTREAM_UNAVAILABLE", "retryable": true, "message": "Google SERP is temporarily unavailable. Please try again shortly." }
],
"credits": { "used": 18 },
"meta": { "domain": "example.com", "requested": 2, "distinct": 2, "measured": 1, "ranking": 1, "failed": 1, "max_sync_batch": 20 }
}A keyword that fails inside a 200 batch is still billed.
History Endpoint
GET /rankings/historyCredits: Free (0).
| Parameter | Type | Required | Description |
|---|---|---|---|
domain | string | Yes | The domain you checked |
keyword | string | No | One keyword. Set it to get a single series and change |
gl | string | No | 2-letter country |
device | string | No | desktop or mobile |
from | string | No | ISO date: earliest checked_at |
to | string | No | ISO date: latest checked_at |
limit | number | No | 1–1000 (default: 100), newest first |
{
"schema": "ranking-history.v1",
"series": [
{ "keyword": "project management software", "domain": "example.com", "gl": "US", "hl": "en", "device": "desktop", "position": 4, "url": "https://www.example.com/features", "search_depth": 30, "serp_features": ["organic", "ads"], "checked_at": "2026-09-29T10:00:00.000Z" },
{ "keyword": "project management software", "domain": "example.com", "gl": "US", "hl": "en", "device": "desktop", "position": 6, "url": "https://www.example.com/features", "search_depth": 30, "serp_features": ["organic", "ads"], "checked_at": "2026-09-28T10:00:00.000Z" }
],
"change": { "delta": 2, "from": 6, "to": 4 },
"credits": { "used": 0 },
"meta": { "domain": "example.com", "returned": 2, "limit": 100 }
}change appears when keyword is set and the series has at least two checks. A positive delta means the domain moved up. When one of the two checks found nothing, delta is null and event is entered or dropped_out. History holds only the checks you ran (batch rows and async rankings/check tasks included). It is not a back-filled index, and it is kept for a configurable retention period.
AI Answers
Answers from AI assistants and the sources they cited: ChatGPT, Microsoft Copilot and Google Gemini. Perplexity answers are paused: GET /search/ai/perplexity answers 403 FEATURE_NOT_AVAILABLE and is not charged. Each answer is generated live for your prompt, with no account history shaping it: the answer the engine's own users see.
Searlo is independent and not affiliated with OpenAI, Microsoft, Google or Perplexity.
Endpoint
GET /search/ai/{engine}> engine is one of chatgpt, copilot or gemini; GET /search/ai?engine=chatgpt is the same route, and GET /search/ai without an engine asks ChatGPT. perplexity (paused), aimode (Google AI Mode) and grok are switched off in production: they answer 403 FEATURE_NOT_AVAILABLE and are not charged.
Credits: per answer: Gemini 4, ChatGPT 8, Copilot 8. A cached answer is billed at the full rate.
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
q | string | Yes | The prompt (1–500 chars) |
gl | string | No | Country, max 5 chars (e.g. us). Best-effort country for the answer |
hl | string | No | Language, max 5 chars (e.g. en). The language to ask in. On Copilot, gl and hl become the Bing market code (e.g. en-US) |
cache | boolean | No | Default true: a repeat of the same prompt within about an hour returns the cached answer. false forces a fresh run, billed as usual |
mode | string | No | Max 20 chars. Echoed in searchParameters.mode and part of the cache key; it does not change the answer |
model | string | No | Max 40 chars. Accepted but not used: answers come from each engine's default model |
html | boolean | No | true also returns html, an HTML snapshot of the answer page, and pageUrl (default: false). Same credits. A snapshot is of its own run: it is never served from or written to the cache |
Structured output (schema) is not available on this endpoint yet. Do not send it: a JSON value is rejected with 400, and a bracketed schema[field]=type is dropped.
These engines take no location beyond gl: location and uule are ignored.
Response
{
"success": true,
"searchParameters": { "q": "best crm for startups", "type": "ai", "engine": "chatgpt", "mode": "ui", "gl": "us", "hl": "en" },
"answer": "For most early-stage startups, a free tier is enough to begin with...",
"answerMarkdown": "For most early-stage startups, a **free tier** is enough to begin with...",
"citations": [
{ "index": 1, "url": "https://www.example.com/best-crm-startups", "title": "The best CRMs for startups", "domain": "example.com" },
{ "index": 2, "url": "https://blog.example.org/crm-guide", "title": "Choosing a CRM", "domain": "blog.example.org" }
],
"sources": ["example.com", "blog.example.org"],
"citationPills": [],
"related": ["Which CRM is free for small teams?"],
"model": "",
"threadUrl": null,
"complete": true,
"via": "dom",
"fetchedAt": "2026-09-29T10:00:00.000Z",
"source": "searlo.tech(chatgpt)",
"cached": false
}| Field | Description |
|---|---|
answer | The answer as plain text |
answerMarkdown | The answer as markdown when the engine provides it, otherwise the same text |
citations | The sources cited, in order: index, url, title, domain. Copilot citations also carry snippet and date |
sources | The unique cited domains, for share-of-voice counts |
citationPills | Copilot's source pills (position, label, title, url, domain); empty on the other engines |
related | Follow-up questions the engine suggested, when it shows any |
model | The model the engine reported: gemini-flash (anonymous) on Gemini, empty on ChatGPT and Copilot |
threadUrl | A link to the engine's thread when it exposes one; null on ChatGPT and Copilot |
complete | false when the answer was still streaming at the deadline, or (Copilot) when its text stops mid-sentence, so it may be cut short. Incomplete answers are never cached |
cached | true when the answer came from cache (also X-Cache: HIT) |
html | With html=true: an HTML snapshot of the answer page (the rendered DOM, not a screenshot). On Copilot it is the answer frame's document. Stylesheets and scripts stay linked, not inlined. Hundreds of KB to about 2 MB (Gemini) |
pageUrl | With html=true: the page's URL at that moment, for resolving its relative links |
Responses also set X-AI-Engine.
Latency, Caching and Variance
- •A live answer typically takes 10–30 seconds. Set a generous client timeout.
- •Answers are non-deterministic: the same prompt can return different text and different citations from one run to the next. That variance is part of what you are measuring.
- •A repeat of the same prompt (ignoring case and extra spaces) on the same engine, with the same
gl,hlandmode, within about an hour is served from cache and billed at the full rate. Passcache=falseto force a new run. - •Identical requests in flight at the same moment,
cache=falseones included, are merged into one run: every call is billed and all of them get the same answer. Parallel identical calls are not independent samples, so space your samples out. - •ChatGPT and Gemini show sources only when they searched the web, so
citationscan be empty on a valid answer. - •
html=truealways runs the engine (a snapshot is never cached), and a snapshot request is merged only with identical snapshot requests in flight.
Errors
| Status | code | Meaning |
|---|---|---|
400 | VALIDATION_ERROR | A missing or invalid parameter, or an unknown engine |
429 | AT_CAPACITY | The engine is at capacity. Retry after Retry-After; a retry before then is refused |
403 | FEATURE_NOT_AVAILABLE | The engine is switched off on this deployment (retryable: false) |
503 | UPSTREAM_UNAVAILABLE | The engine returned something unusable (retryable: true, short Retry-After). Try again shortly |
Failed calls are refunded.
Web Data (Scrape, Map, Crawl, Extract)
The retrieval half of the API: read a page, list a site's URLs, crawl a site, or pull structured records out of pages. Pages that render client-side are rendered automatically. That automatic rendering is for accounts that have bought credits; on sign-up credits alone, pass render: true to render a page.
Endpoints
POST /scrape
POST /scrape/batch
POST /map
POST /crawl
GET /crawl
GET /crawl/{jobId}
GET /crawl/{jobId}/results
DELETE /crawl/{jobId}
POST /extract
POST /search-and-scrape
GET /search/webpageCredits:
| Endpoint | Credits |
|---|---|
POST /scrape, POST /map | 1; 2 when your maxAge or cache: false forced a live fetch |
POST /scrape/batch, POST /crawl | 1 per page fetched successfully, charged while the job runs |
POST /extract | 1 per page fetched; a URL that failed, timed out or was not reached is free |
POST /search-and-scrape | 1 for the search, plus 1 per page scraped, plus 1 per page fetched live when you set maxAge or cache: false |
GET /search/webpage | 1 (a cached copy is billed the same) |
GET and DELETE on /crawl | Free |
From 2026-11-01: pages that needed a browser cost 5. On every row above except /map, a page that had to be rendered costs 4 credits more than the table says: 5 instead of 1 per page. That covers render: true, actions, and automatic rendering when a plain fetch returns no usable content, such as a JavaScript-only page. The page's via says which it was: browser or http. A cached copy costs the same as the page it copies; a failed fetch is never charged; the live-fetch surcharge is added on top, not multiplied (a forced live fetch that needed a browser is 2 + 4 = 6). render: false keeps every page at the plain price. Until 2026-11-01 a browser page costs the same as any other.
Freshness and the Live-Fetch Surcharge
/scrape, /map and /search-and-scrape keep a cache, controlled by two fields:
- •Omit both, and any cached copy is used. The call costs 1 whether or not it was cached (from 2026-11-01, 5 for a page that needed a browser, cached or not).
- •
maxAge(seconds, 0–2592000): use a cached copy only if it is at most this old. - •
cache: falseis the same asmaxAge: 0: always fetch live.
When your maxAge or cache: false makes Searlo fetch the page live, the call costs 2 and the response carries X-Cache-Bypass: CHARGED. If a cached copy satisfied your maxAge, it costs 1. X-Credits-Deducted always shows the total, and a cached answer carries _cacheAgeSec.
URLs
Every url and urls field takes an http or https URL of up to 2,048 characters. An internationalised domain name works as you write it on scrape, map, crawl, scrape/batch and extract: https://www.löschwagen.com/preise is fetched, cached and billed under its encoded form, https://www.xn--lschwagen-07a.com/preise, and so is a path in any script. (GET /search/webpage takes the encoded xn-- form only.)
A URL that cannot be fetched as written is a 400 VALIDATION_ERROR (url must be an http(s) URL), answered before anything is fetched and never charged: a control character or a space anywhere in it, an escaped one in the host (%00), a scheme other than http or https, or a URL that cannot be parsed. A private or reserved address is 422 TARGET_NOT_ALLOWED.
Scrape
POST /scrape| Field | Type | Required | Description |
|---|---|---|---|
url | string | Yes | http(s) URL, max 2048 chars |
formats | string[] | No | Any of markdown, text, html, links, images, headings, tables, json_ld (default: ["markdown"]) |
onlyMainContent | boolean | No | Strip navigation, headers and footers (default: true) |
render | boolean | No | true always renders the page; false never renders. By default a page is rendered only when the plain fetch returns no usable content (an empty JavaScript shell, or a page the site refused), on accounts that have bought credits. From 2026-11-01 a rendered page costs 5 |
includeLinks | boolean | No | false removes links from the content and omits links unless formats asks for it |
includeImages | boolean | No | false removes images from the content |
excludeSelectors | string[] | No | CSS selectors to remove before extraction (up to 20) |
chunk | boolean | No | Also split the markdown into chunks for embedding |
chunkMaxTokens | number | No | Chunk size (256–16384) |
waitForSelector | string | No | Wait for this selector before reading the page |
timeout | number | No | Milliseconds (1000–60000) |
actions | object[] | No | Up to 20 steps run in the page before it is read; implies render: true. type is one of click, type, press, scroll, wait, waitForSelector, evaluate, with selector, text, key, ms (0–15000) or script as the step needs. Each step's outcome comes back in actions |
cache | boolean | No | Default: true (see Freshness) |
maxAge | number | No | Seconds (see Freshness) |
{
"searchParameters": { "url": "https://example.com/pricing", "type": "scrape", "formats": ["markdown", "links"] },
"url": "https://example.com/pricing",
"requestedUrl": "https://example.com/pricing",
"statusCode": 200,
"via": "http",
"bytes": 48213,
"markdown": "# Pricing\n\nStart free, then pay as you go...",
"metadata": {
"title": "Pricing | Example",
"description": "Plans and prices.",
"author": null,
"publishedDate": null,
"language": "en",
"canonicalUrl": "https://example.com/pricing",
"robots": null,
"wordCount": 812,
"openGraph": { "title": "Pricing", "image": "https://example.com/og.png" },
"twitter": null
},
"links": [{ "url": "https://example.com/signup", "text": "Sign up", "internal": true }],
"source": "searlo.tech(scrape)",
"credits": 1
}url is the final URL after redirects and requestedUrl is what you sent. via is http or browser (serp for a Google results page). A page that comes back empty is returned but not cached.
| Status | errorCode | Meaning | Charged |
|---|---|---|---|
422 | TARGET_REFUSED | The site refused every attempt, or answered with an error of its own. targetStatus holds the site's own HTTP status; retryable is true only when that was a 429 or a 5xx | No |
422 | TARGET_NOT_ALLOWED | The URL points to a private or reserved network address (localhost, a private range, link-local, a cloud metadata address). Only pages on the public internet are fetched (retryable: false) | No |
422 | TARGET_UNREACHABLE | The site could not be fetched at all: its server refuses the secure-connection handshake, it redirects in a loop, or the page is larger than the size limit. targetError says which (retryable: false) | No |
Map
POST /mapEvery URL a site publishes, before you pay to crawl it: robots.txt, then its sitemaps (sitemap indexes included), then the homepage's links when there is no sitemap.
| Field | Type | Required | Description |
|---|---|---|---|
url | string | Yes | The site, max 2048 chars |
limit | number | No | Maximum URLs (1–5000, default: 1000) |
search | string | No | Only URLs containing this text (max 200 chars) |
includeSubdomains | boolean | No | Include subdomains |
sitemapOnly | boolean | No | Sitemaps only, no homepage-link fallback |
cache, maxAge | No | See Freshness |
{
"searchParameters": { "url": "https://example.com", "type": "map", "limit": 1000 },
"origin": "https://example.com",
"discovery": "sitemap",
"sitemaps": ["https://example.com/sitemap.xml"],
"robots": { "present": true, "sitemapCount": 1 },
"totalResults": 214,
"urls": [
{ "url": "https://example.com/pricing", "lastModified": "2026-09-01" },
{ "url": "https://example.com/blog/launch", "lastModified": null }
],
"source": "searlo.tech(map)",
"credits": 1
}discovery is sitemap, links (homepage links, when the site publishes no sitemap) or none.
Crawl
POST /crawlQueue a crawl of one site and get a job id back at once. Starting needs a minimum balance (configurable); after that, credits are charged per page as the crawl runs, so a job with limit: 500 on a twelve-page site costs 12 (from 2026-11-01, plus 4 for each of those pages that needed a browser). robots.txt is obeyed by default.
| Field | Type | Required | Description |
|---|---|---|---|
url | string | Yes | Start URL |
maxDepth | number | No | Link depth from the start URL (0–10, default: 2) |
limit | number | No | Maximum pages (1–5000, default: 100) |
includePaths | string[] | No | Only follow paths matching these globs, e.g. /blog/* (up to 25) |
excludePaths | string[] | No | Skip paths matching these globs (up to 25) |
formats | string[] | No | markdown, text, html, links, json_ld (default: ["markdown"]) |
allowSubdomains | boolean | No | Follow links to subdomains (default: false) |
allowExternal | boolean | No | Follow links off the site (default: false) |
respectRobots | boolean | No | Obey robots.txt (default: true) |
useSitemap | boolean | No | Seed the crawl from the site's sitemaps (default: true) |
concurrency | number | No | Pages fetched at once (1–8, default: 3) |
render | boolean | No | true always renders the page; false never renders. By default a page is rendered only when the plain fetch returns no usable content (an empty JavaScript shell, or a page the site refused), on accounts that have bought credits. From 2026-11-01 a rendered page costs 5 |
onlyMainContent | boolean | No | Default: true |
storeContent | boolean | No | false keeps a link graph with no page bodies (default: true) |
webhook | object | No | { "url": "https://…", "secret": "…" }, called once when the crawl ends |
The snake_case spellings (max_depth, include_paths, exclude_paths, allow_subdomains, allow_external, respect_robots, use_sitemap, only_main_content, store_content) are accepted too.
{
"success": true,
"jobId": "crawl_0123456789abcdef01234567",
"status": "queued",
"url": "https://example.com/docs",
"options": { "maxDepth": 2, "limit": 50, "includePaths": ["/docs/*"], "excludePaths": [], "formats": ["markdown"], "allowSubdomains": false, "allowExternal": false, "respectRobots": true, "concurrency": 3, "render": null, "onlyMainContent": true, "useSitemap": true, "storeContent": true },
"maxCredits": 50,
"statusUrl": "/api/v1/crawl/crawl_0123456789abcdef01234567",
"resultsUrl": "/api/v1/crawl/crawl_0123456789abcdef01234567/results",
"createdAt": "2026-09-29T10:00:00.000Z"
}The response is 202. maxCredits is a ceiling, not a quote; from 2026-11-01 it allows 5 per page unless the job sets render: false.
Batch Scrape
POST /scrape/batchUp to 1000 URLs as one job, with no discovery: the job fetches exactly the URLs you send. They are normalised and de-duplicated first, so compare submitted with urlCount. Like a crawl, it needs a minimum balance to start and then bills 1 credit per page fetched (from 2026-11-01, 5 for a page that needed a browser). Status, results, cancellation and webhooks use the crawl endpoints below.
| Field | Type | Required | Description |
|---|---|---|---|
urls | string[] | Yes | 1–1000 http(s) URLs |
formats | string[] | No | markdown, text, html, links, json_ld (default: ["markdown"]). images, headings and tables are accepted but not stored |
render | boolean | No | true always renders the page; false never renders. By default a page is rendered only when the plain fetch returns no usable content (an empty JavaScript shell, or a page the site refused), on accounts that have bought credits. From 2026-11-01 a rendered page costs 5 |
onlyMainContent | boolean | No | Default: true |
concurrency | number | No | Pages fetched at once (1–8, default: 3) |
storeContent | boolean | No | false keeps only metadata and links (default: true) |
webhook | object | No | { "url": "https://…", "secret": "…" }, called once when the job ends |
maxAge, cache, includeLinks and excludeSelectors are accepted on a batch but have no effect: every page is fetched live, at 1 credit (5 from 2026-11-01 if it needed a browser).
The 202 response has the crawl shape plus mode: "batch", urlCount (what was queued) and submitted (what you sent).
Job Status and Results
GET /crawl/{jobId}
GET /crawl/{jobId}/results
GET /crawl
DELETE /crawl/{jobId}All free, for crawls and batches alike.
{
"success": true,
"jobId": "crawl_0123456789abcdef01234567",
"status": "completed",
"url": "https://example.com/docs",
"options": { "maxDepth": 2, "limit": 50, "includePaths": ["/docs/*"], "excludePaths": [], "formats": ["markdown"], "allowSubdomains": false, "allowExternal": false, "respectRobots": true, "concurrency": 3, "render": null, "onlyMainContent": true, "useSitemap": true, "storeContent": true },
"progress": { "discovered": 64, "completed": 50, "failed": 2, "skipped": 3 },
"creditsCharged": 50,
"stopReason": "page limit reached",
"webhook": { "url": "https://example.com/hooks/crawl", "deliveredAt": "2026-09-29T10:04:10.000Z", "attempts": 1 },
"createdAt": "2026-09-29T10:00:00.000Z",
"startedAt": "2026-09-29T10:00:02.000Z",
"finishedAt": "2026-09-29T10:04:09.000Z"
}status is queued, running, completed, failed or cancelled. stopReason says how a job ended: page limit reached, all urls processed (a batch), credit balance exhausted, job deadline reached (jobs run for a configurable maximum time) or cancelled by caller. A job that runs out of credits keeps the pages it already fetched.
GET /crawl/{jobId}/results pages through what was fetched:
| Parameter | Type | Required | Description |
|---|---|---|---|
limit | number | No | Pages per response (1–200, default: 50) |
cursor | string | No | nextCursor from the previous response |
status | string | No | completed (default), failed, skipped, queued or all |
{
"success": true,
"jobId": "crawl_0123456789abcdef01234567",
"status": "completed",
"totalResults": 50,
"hasMore": false,
"nextCursor": null,
"pages": [
{
"url": "https://example.com/docs/intro",
"finalUrl": null,
"depth": 1,
"parentUrl": "https://example.com/docs",
"status": "completed",
"statusCode": 200,
"via": "http",
"title": "Introduction",
"description": "Get started with Example.",
"language": "en",
"canonicalUrl": "https://example.com/docs/intro",
"wordCount": 640,
"markdown": "# Introduction\n...",
"fetchedAt": "2026-09-29T10:00:05.000Z"
}
]
}totalResults counts the pages in this response, not the whole job. Skipped pages carry a reason (e.g. disallowed by robots.txt) and failed ones an error. GET /crawl lists your jobs, newest first (limit 1–100, default 20; optional status). DELETE /crawl/{jobId} stops a job; pages already fetched stay billed and readable. Jobs and their pages are kept for a configurable retention period.
Job Webhooks
When a crawl or batch ends, Searlo POSTs once to webhook.url:
{
"jobId": "crawl_0123456789abcdef01234567",
"event": "crawl.completed",
"status": "completed",
"url": "https://example.com/docs",
"stopReason": "page limit reached",
"progress": { "discovered": 64, "completed": 50, "failed": 2, "skipped": 3 },
"creditsCharged": 50,
"resultsUrl": "https://api.searlo.tech/api/v1/crawl/crawl_0123456789abcdef01234567/results",
"startedAt": "2026-09-29T10:00:02.000Z",
"finishedAt": "2026-09-29T10:04:09.000Z"
}Headers: X-Searlo-Job-Id, X-Searlo-Timestamp, X-Searlo-Event and, when you set a secret, X-Searlo-Signature, signed the same way as Async Tasks webhooks. The URL must be HTTPS and publicly reachable. A job webhook is attempted once and not retried: webhook.attempts and webhook.deliveredAt on the job show whether it landed.
Extract
POST /extractURLs plus a field schema in, one record per URL out. The URLs are fetched several at a time inside one overall time budget (75 seconds by default), so a long list takes about as long as its slowest pages, not the sum of them. Pages that arrive in time are served and billed; the rest come back as rows that say why.
| Field | Type | Required | Description |
|---|---|---|---|
urls | string[] | One of urls, url | 1–20 http(s) URLs |
url | string | One of urls, url | A single URL |
schema | object | Yes | 1–30 fields, each name mapped to a type: string, number, integer, float, boolean or array. Names use letters, digits and _, do not start with a digit, max 64 chars |
useLlm | boolean | No | Use a language model for fields the page does not declare (default: true). It only has an effect when the deployment has a model configured; llmAvailable in the response says whether it does |
onlyMainContent | boolean | No | Default: true |
Values come first from what the page declares (schema.org JSON-LD, then Open Graph and meta tags), then from unambiguous patterns in the text (prices, currencies, emails, phone numbers), then from the model if it is enabled. That first pass recognises common field names (name, description, price, currency, rating, reviews, image, sku, brand, author, datePublished, availability, url, email, phone, address and their usual aliases); other names are filled only by the model. A field nothing can fill comes back null and is listed in missingFields: it is not guessed. Your balance must cover one credit per URL before the call starts (402 otherwise). Billing is one credit per page actually fetched, or from 2026-11-01, 5 for a row whose via is browser: a row that failed, timed out or was never reached is free.
{
"searchParameters": { "type": "extract", "urls": 3, "fields": ["name", "price", "currency", "availability"] },
"mode": "structured+llm",
"llmAvailable": true,
"totalResults": 3,
"partial": true,
"data": [
{
"url": "https://shop.example.com/p/widget",
"success": true,
"data": { "name": "Widget Pro", "price": 49.99, "currency": "USD", "availability": null },
"_sources": { "name": "json-ld", "price": "json-ld", "currency": "json-ld" },
"missingFields": ["availability"],
"via": "http",
"llmUsed": false,
"llmSkipped": "time budget"
},
{
"url": "https://shop.example.org/p/gadget",
"success": false,
"data": null,
"errorCode": "TARGET_REFUSED",
"error": "target site refused the request (HTTP 403)",
"retryable": false,
"targetStatus": 403
},
{
"url": "https://slow.example.net/p/thing",
"success": false,
"data": null,
"errorCode": "NOT_FETCHED",
"error": "not fetched: the request's time budget ran out before this URL's turn",
"retryable": true
}
],
"source": "searlo.tech(extract)",
"credits": 1
}_sources gives each filled field's origin: json-ld, meta, text or llm. credits is the number of pages fetched. Every row has url, success and data; the rest depends on the row:
| Field | Description |
|---|---|
success | true when the page was fetched (1 credit), false when it was not (free, with data: null) |
errorCode | On a failed row: TARGET_REFUSED (the site refused; targetStatus is its own HTTP status), TARGET_NOT_ALLOWED (a private or reserved address), TARGET_UNREACHABLE (the site cannot be fetched at all), INVALID_REQUEST (the URL was not accepted), SCRAPE_FAILED (the page could not be fetched right now), TIMEOUT (it did not arrive inside the time budget) or NOT_FETCHED (the budget ran out before this URL's turn) |
error | On a failed row: what happened, in a sentence |
retryable | On a failed row: whether the same URL may succeed on a retry. true for SCRAPE_FAILED, TIMEOUT, NOT_FETCHED and a refusal that was the site's own 429 or 5xx; a site's 401 or 403 will not change |
partial | At the top level, 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. Absent otherwise |
llmSkipped | On a fetched row: "time budget" when the page came in too late to ask the model for its missing fields. missingFields lists them; send that URL again on its own to give the model its time |
A private or reserved address among the urls fails the whole call with 422 TARGET_NOT_ALLOWED before anything is fetched.
Search and Scrape
POST /search-and-scrapeA web search, then the result pages scraped, in one call.
| Field | Type | Required | Description |
|---|---|---|---|
q | string | Yes | Search query (1–500 chars) |
limit | number | No | Results to scrape (1–20, default: 5). The search step returns at most 10 |
scrape | boolean | No | false returns the search results only, for 1 credit (default: true) |
formats | string[] | No | markdown, text, html, json_ld (default: ["markdown"]) |
onlyMainContent | boolean | No | Default: true |
render | boolean | No | true always renders the page; false never renders. By default a page is rendered only when the plain fetch returns no usable content (an empty JavaScript shell, or a page the site refused), on accounts that have bought credits. From 2026-11-01 a rendered page costs 5 |
gl | string | No | Country code, max 5 chars |
hl | string | No | Language code, max 5 chars |
cache, maxAge | No | Apply to the scraped pages (see Freshness) |
{
"searchParameters": { "q": "vector database benchmarks", "type": "search-and-scrape", "limit": 3, "scrape": true },
"totalResults": 3,
"scrapedCount": 2,
"results": [
{
"position": 1,
"title": "Vector database benchmarks, 2026",
"link": "https://www.example.com/benchmarks",
"snippet": "We ran each database on the same hardware...",
"domain": "example.com",
"scraped": true,
"via": "http",
"statusCode": 200,
"metadata": { "title": "Vector database benchmarks, 2026", "language": "en", "wordCount": 2310 },
"markdown": "# Vector database benchmarks, 2026\n..."
},
{ "position": 3, "title": "Benchmark notes", "link": "https://unreachable.example.net/post", "snippet": "...", "domain": "unreachable.example.net", "scraped": false, "error": "scrape failed" }
],
"source": "searlo.tech(search-and-scrape)",
"credits": 3
}Each row is a web search result (position, title, link, snippet, domain, …) with its page attached. A page that could not be scraped comes back with scraped: false and an error, and is not billed.
Webpage Fetch (GET or POST)
GET /search/webpage reads one page from a query string, for clients that can only send GET: one URL, one format, the page's metadata. POST /search/webpage takes the same fields as a JSON body, plus excludeSelectors (up to 20 CSS selectors to remove before extraction). 1 credit; from 2026-11-01, 5 when the page needed a browser (via: "browser"). A repeat call can be answered from cache (X-Cache: HIT) and is billed the same; to control freshness, use POST /scrape with maxAge.
| Parameter | Type | Required | Description |
|---|---|---|---|
url | string | Yes | An http(s) URL, max 2048 chars |
format | string | No | markdown (default), text or html |
render | boolean | No | true always renders the page; false never does. By default a page is rendered only when the plain fetch returns no usable content (an empty JavaScript shell, or a page the site refused), on accounts that have bought credits. From 2026-11-01 a rendered page costs 5 |
onlyMainContent | boolean | No | Strip navigation, headers and footers (default: true) |
includeLinks | boolean | No | false drops links from the content (default: true) |
includeImages | boolean | No | false drops images from the content (default: true) |
chunk | boolean | No | Also return chunks: the content split into overlapping pieces for embedding |
chunkMaxTokens | number | No | Chunk size, 256–16384 tokens |
timeout | number | No | Milliseconds, 1000–60000 |
{
"searchParameters": { "url": "https://example.com/blog/benchmarks", "type": "webpage", "format": "markdown" },
"url": "https://example.com/blog/benchmarks",
"requestedUrl": "https://example.com/blog/benchmarks",
"domain": "example.com",
"statusCode": 200,
"via": "http",
"bytes": 48213,
"title": "Vector database benchmarks, 2026",
"description": "We ran each database on the same hardware...",
"author": null,
"publishedDate": "2026-03-02",
"openGraph": { "title": "Vector database benchmarks, 2026", "image": "https://example.com/og.png" },
"wordCount": 2310,
"content": "# Vector database benchmarks, 2026\n...",
"source": "searlo.tech(webpage)",
"credits": 1
}url is where the page ended up after redirects; requestedUrl is what you sent. via is http for a plain fetch and browser for a rendered one.
A site that refuses the fetch is 422 TARGET_REFUSED (targetStatus is the site's own status), a private or reserved address is 422 TARGET_NOT_ALLOWED, and a site no attempt could fetch is 422 TARGET_UNREACHABLE, as on POST /scrape. None is charged.
Async Tasks
Queue a request, get a task id back at once, and collect the result later by polling or by webhook. The task runs the same service the synchronous route uses.
Endpoints
POST /async/task
POST /async/task/batch
GET /async/task/{taskId}
GET /async/status
DELETE /async/queueCredits: the endpoint's price, deducted when the task is queued. Kept when the task completes; refunded when it fails or is cancelled. Reading tasks and status is free.
Request
| Field | Type | Required | Description |
|---|---|---|---|
endpoint | string | Yes | What to run (see the table below) |
request | object | No | The parameters for that endpoint. Must include q (ai/… tasks also accept prompt), except for goto |
webhook | object | No | { "url": "https://…", "secret": "…" }. url is required inside it; secret (max 256 chars) signs each delivery |
priority | number | No | 1–100 (default: 50). Higher runs first |
idempotencyKey | string | No | Max 128 chars. Sending the same key again returns the existing task (duplicate: true, HTTP 200) without a new charge |
The field is endpoint, not type, and the webhook is webhook: { url, secret }, not webhookUrl. Unknown top-level fields are stripped without an error: a request that sends webhookUrl is queued with no webhook, and one that sends type instead of endpoint is rejected with 400.
endpoint | Credits | Runs |
|---|---|---|
serp | 3 per page; from 2026-11-01, +5 when the AI Overview had to be completed by rendering the page | Full SERP. request takes the GET /serp fields. A country location (or its uule) is applied as gl; a city or state fails the task at once with CITY_LOCATION_UNAVAILABLE, refunded |
news/serp | 2 per page | GET /search/news/serp, with the same location rule |
rankings/check | 3 per 10 results of depth | Rank Checks. Send keyword and domain as usual and repeat the keyword as q, which the queue requires |
ai/chatgpt, ai/copilot | 8 | AI Answers |
ai/perplexity | 2 | AI Answers |
ai/gemini | 4 | AI Answers |
web | 1 | Web search. request goes to the search service as is: use num (1–10), start, country and language, not limit, page, gl and hl |
maps | 1 | Maps search. Use num for the result count; gl, hl, ll, lat, lng and zoom work as on /search/maps |
shopping | 1 | Shopping search. Use num for the result count, plus gl and hl |
goto | 0 | Resolves Google /goto redirect links: url, or urls (up to 50) |
The validator also accepts keywords/…, domain/…, ai/aimode and ai/grok; those endpoints are switched off in production, so do not queue them.
Response
POST /async/task answers 202 with the queued task:
{
"success": true,
"duplicate": false,
"task": {
"taskId": "5f0c2b1e-7a3d-4c1b-9d0e-2a6f8b9c1d3e",
"endpoint": "serp",
"status": "QUEUED",
"priority": 50,
"attempts": 0,
"creditsHeld": 6,
"creditsUsed": 0,
"creditsRefunded": false,
"request": { "q": "best running shoes", "gl": "us", "pages": 2 },
"webhook": { "url": "https://example.com/hooks/searlo", "attempts": 0, "lastStatus": null, "deliveredAt": null },
"queuedAt": "2026-09-29T10:00:00.000Z",
"startedAt": null,
"completedAt": null,
"durationMs": null,
"expiresAt": "2026-10-06T10:00:00.000Z"
}
}GET /async/task/{taskId} returns { "success": true, "task": { … } } in the same shape, or 404 for an unknown id. status is QUEUED, PROCESSING, COMPLETED, FAILED or CANCELLED.
- •
resultappears once the task isCOMPLETED. Forserp,news/serpandai/…it is the body the synchronous route returns; forrankings/checkit is the check'sresultobject withsuccess: true. - •
error(code,message,retryable) appears when the task isFAILED. - •A retryable failure (capacity, an unusable upstream page) is queued again after a delay, the engine's
Retry-Afterwhen it gave one, up to a configurable number of attempts. After that, or on a non-retryable error, the task isFAILEDand its credits are refunded (creditsRefunded: true). Errors in the request itself (VALIDATION_ERROR,CITY_LOCATION_UNAVAILABLE,UNKNOWN_LOCATION,INVALID_DOMAIN, a query shape Google refuses on sight) fail at once, without retries. - •A result larger than a configurable size has its bulkiest top-level fields removed, listed in
result._trimmed. - •Tasks are deleted after a configurable retention period;
expiresAtsays when.
Batch
POST /async/task/batch takes { "tasks": [ … ] } with 1–500 task objects, each shaped like a single request. Each task is charged when it is queued. The response is 202 when at least one task was accepted, 400 otherwise:
{
"success": true,
"accepted": 2,
"total": 3,
"tasks": [
{ "accepted": true, "duplicate": false, "taskId": "5f0c2b1e-7a3d-4c1b-9d0e-2a6f8b9c1d3e", "status": "QUEUED", "creditsHeld": 8 },
{ "accepted": true, "duplicate": false, "taskId": "9b1d7e0a-2c4f-4e8b-a6d3-1f2e3d4c5b6a", "status": "QUEUED", "creditsHeld": 2 },
{ "accepted": false, "error": "request.q is required", "endpoint": "ai/copilot" }
]
}If your credits run out part-way, the batch stops there: the remaining tasks are not queued and do not appear in tasks, so compare tasks.length with total. A task that fails validation (an unknown endpoint, say) rejects the whole batch with 400.
Queue Status
GET /async/status returns your queue: queued, processing, priorityBreakdown ([{ "priority": 50, "count": 12 }]), last24h (completed, failed) and worker (the answering process's inflight and concurrency per lane). DELETE /async/queue cancels every task still QUEUED and refunds it: { "success": true, "cleared": 12, "creditsRefunded": 36 }.
Webhooks
When a task ends as COMPLETED or FAILED (not when it is queued again for a retry), Searlo POSTs to webhook.url:
{
"taskId": "5f0c2b1e-7a3d-4c1b-9d0e-2a6f8b9c1d3e",
"endpoint": "serp",
"status": "COMPLETED",
"result": { "success": true, "organic": [], "peopleAlsoAsk": [] },
"creditsUsed": 6,
"completedAt": "2026-09-29T10:01:12.000Z"
}A failed task sends error instead of result. Headers:
- •
X-Searlo-Task-Id, andX-Searlo-Event(task.completedortask.failed) - •
X-Searlo-Timestamp: milliseconds since the epoch - •
X-Searlo-Signature(when you set a secret):v1=followed by the hex HMAC-SHA256, keyed with your secret, of<timestamp>.<raw body>
Any 2xx counts as delivered; redirects are not followed. A failed delivery (another status, or no answer within a configurable timeout) is retried after 5 minutes, 30 minutes, 2 hours and 12 hours, then given up. The URL must be HTTPS and resolve to a public address; it is checked when you queue the task and again before each delivery.
Search Analytics
Retrieve analytics about your own search usage. Free (0 credits).
Endpoint
GET /search/analyticsParameters
| Parameter | Type | Required | Description |
|---|---|---|---|
startDate | string | No | ISO date — filter from this date |
endDate | string | No | ISO date — filter up to this date (must be after startDate) |
searchType | string | No | Filter by type (e.g. web, image, news, batch) |
Response
{
"success": true,
"data": {
"totalSearches": 1284,
"searchesByType": { "web": 900, "image": 200, "news": 184 },
"recentSearches": [ { "query": "...", "searchType": "web", "createdAt": "..." } ],
"popularQueries": [ { "query": "react", "count": 42 } ],
"creditsUsed": 1284,
"averageResponseTime": 412
},
"message": "Analytics retrieved successfully"
}Email Verifier
Validate an email address's deliverability.
Verify Endpoint
POST /email-verifier/verifyCredits: 5 per request. Rate-limited by the separate email_verifier tier group (see Rate Limiting). Verification is also paced: when it is busy the call answers 429 VERIFIER_BUSY with a Retry-After (30 seconds by default), and is not charged.
Body
| Field | Type | Required | Description |
|---|---|---|---|
email | string | Yes | Email address to verify |
Response
Responses use the standard envelope (statusCode, success, message, data).
{
"statusCode": 200,
"success": true,
"message": "Email verified",
"data": {
"email": "[email protected]",
"status": "valid",
"domain": "example.com",
"message": "Mailbox exists",
"details": {
"format": true,
"mailboxStatus": "...",
"mailboxType": "...",
"mailboxExchange": "..."
},
"verifiedAt": "2026-05-20T10:30:00.000Z",
"source": "searlo"
}
}status is one of: valid, invalid, accept_all, unknown, error.
History Endpoint
GET /email-verifier/historyCredits: Free (0). Returns your past verifications, paginated.
| Parameter | Type | Required | Description |
|---|---|---|---|
page | number | No | Page number (default: 1) |
limit | number | No | Results per page (1–100, default: 20) |
Account & Utilities
Your balance, and what this API serves right now. All free (0 credits); each needs your API key, like every other call.
Endpoints
GET /credits/balance
GET /credit/balance
GET /search/capabilities
GET /search/countries
GET /search/statesCredit Balance
GET /credits/balance returns the credits available to your account, and nothing else about it. It works with an API key (x-api-key, or Authorization: Bearer sk_...) and with a dashboard login. It is free, and like any call it counts against your request limits.
{
"success": true,
"credits": {
"available": 2500,
"reserved": 6,
"expiresAt": "2027-09-01T10:00:00.000Z",
"expiringCredits": 2500
}
}| Field | Description |
|---|---|
available | Credits you can spend now. It is the balance GET /user/profile shows (0 while the account is on hold) |
reserved | Present when known: how much of available requests still in flight, or awaiting settlement, already hold |
expiresAt / expiringCredits | Present when some credits expire: when the next ones do, and how many expire then |
The answer is never cached (Cache-Control: no-store).
GET /credit/balance is the fuller view: the same balance with a low-balance flag and the expiry of each grant.
{
"statusCode": 200,
"data": {
"balance": 2500,
"available": 2500,
"isLowBalance": false,
"lastUpdated": "2026-09-29T08:12:03.000Z",
"expiry": {
"policyApplies": true,
"durationDays": 365,
"totalExpiringAmount": 2500,
"nonExpiringAmount": 0,
"nextExpiryAt": "2027-09-01T10:00:00.000Z",
"daysUntilNextExpiry": 336,
"nextExpiryAmount": 2500,
"breakdown": [
{ "sourceType": "purchase", "amount": 2500, "grantedAt": "2026-09-01T10:00:00.000Z", "expiresAt": "2027-09-01T10:00:00.000Z", "daysRemaining": 336, "status": "active" }
]
}
},
"message": "Credit balance retrieved successfully",
"success": true
}| Field | Description |
|---|---|
available | Credits you can spend now (balance is the same number) |
isLowBalance | Below the low-balance threshold (100 credits by default) |
expiry.nextExpiryAt / nextExpiryAmount | When credits next expire, and how many expire then |
expiry.breakdown | Each purchase or grant still holding credits, soonest expiry first |
Every metered response also carries X-Credits-Deducted and X-Credits-Remaining, so a client rarely needs to poll this.
Capabilities
GET /search/capabilities lists every endpoint this deployment serves, what it costs, whether it is switched on, and how it has done over the last two hours. Read it instead of hard-coding which endpoints are available: a switched-off endpoint answers 403 FEATURE_NOT_AVAILABLE (not charged), and this tells you before you pay the round trip.
{
"success": true,
"generatedAt": "2026-09-29T17:20:00.000Z",
"capabilities": [
{
"id": "serp/html",
"method": "GET",
"path": "/serp/html",
"credits": 2,
"creditsNote": "per page; 8 with js=true (rendered, AI Overview opened out)",
"group": "google",
"enabled": true,
"state": "live",
"health": { "window": "2h", "ok": 412, "fail": 6, "successRate": 0.99 }
},
{
"id": "backlinks",
"method": "GET",
"path": "/backlinks",
"credits": 0,
"group": "backlinks",
"enabled": false,
"reason": "No backlink provider is bound to this deployment. The endpoints answer 501 rather than returning synthesised link data.",
"state": "off",
"health": { "window": "2h", "ok": 0, "fail": 0, "successRate": null }
}
],
"states": { "live": "serving", "degraded": "on, but most recent calls failed", "off": "switched off here" }
}state is off when the endpoint is switched off (reason says why), degraded when at least 3 calls in the window and fewer than half of them succeeded, and live otherwise. successRate is a fraction (0–1), null with no calls in the window. The response may be cached for 30 seconds.
Countries and States
GET /search/countries lists the countries the AI answer endpoints accept as gl. The search endpoints accept any two-letter gl, applied as the results country.
{
"success": true,
"engine": "all",
"countries": [
{ "code": "US", "name": "United States", "states": true, "exit": true },
{ "code": "GB", "name": "United Kingdom", "states": false, "exit": true }
]
}GET /search/states lists the US states with Google's name and uule token for each. State- and city-level location is not available yet (Location): the response says targeting: false, and a state passed as location or uule returns 400 CITY_LOCATION_UNAVAILABLE. Only country=US is available; any other country returns 400.
{
"success": true,
"country": "US",
"exitPinning": false,
"targeting": false,
"message": "State- and city-level location is not available yet: a state passed as location returns 400 CITY_LOCATION_UNAVAILABLE. Country (gl) and language (hl) are applied.",
"states": [
{ "code": "TX", "name": "Texas", "location": "Texas,United States", "uule": "w+CAIQICIT..." }
]
}exitPinning: false: results are not localized to the state.
Rate Limiting
API requests are rate-limited per user with sliding windows. Every account gets the same limits, free credits included: buying credits does not change them.
Limits are tracked per endpoint group, and each group has its own buckets:
| Group | Covers | Per Second | Per Minute | Per Hour | Per Day |
|---|---|---|---|---|---|
search | /search/*, /serp, /rankings, AI answers, web data, async tasks | 600 | 20,000 | 400,000 | 1,800,000 |
local | /search/maps, /search/places, /search/reviews, /search/place-details | 20 | 48 | 2,400 | 16,000 |
email_verifier | /email-verifier/* | 40 | 200 | 10,000 | 40,000 |
mcp | /mcp | 60 | 1,200 | 16,000 | 150,000 |
local is a tighter cap inside the search budget: a maps, places, reviews or place-details call must pass both. API-key and dashboard requests share one bucket per user. On top of the windows, an account can have at most 64 web, image, site, advanced and batch searches in flight at once (see Searches in Flight).
Per API Key
Each API key also has its own caps, inside the account's budget: 20,000 requests a minute and 400,000 a day by default for a new key. The dashboard shows each key's caps. They are fixed windows that start with the key's first request in each window. An account can hold several keys, and all of them draw on the account's limits above.
High-volume accounts get custom limits for the account and for each key, and reserved capacity on the SERP hosts: ask support with your expected daily volume and peak rate.
Searches in Flight
Web, image, site, advanced and batch search (and the GET /search alias) also cap how many of an account's requests may run at once. Past the cap a request is refused at once with 429, code 4203 and errorCode CONCURRENCY_LIMIT, a Retry-After of 1 second and an X-Concurrency-Limit header that carries the cap: 64 in flight per account, on every plan. It is refused before any credit is reserved, so it is never charged. A slot frees as soon as a response finishes: retry after the second.
{
"success": false,
"message": "Too many searches in flight for this account (at most 64 at once). Retry in 1s.",
"code": 4203,
"errorCode": "CONCURRENCY_LIMIT",
"retryable": true,
"retryAfter": 1,
"limit": 64
}Rate Limit Headers
Every rate-limited response carries the state of each window, and the Unix time (seconds) at which the earliest one resets:
X-RateLimit-Limit-Second: 600
X-RateLimit-Remaining-Second: 599
X-RateLimit-Limit-Minute: 20000
X-RateLimit-Remaining-Minute: 19987
X-RateLimit-Limit-Hour: 400000
X-RateLimit-Remaining-Hour: 399421
X-RateLimit-Limit-Day: 1800000
X-RateLimit-Remaining-Day: 1771305
X-RateLimit-Reset: 1790690460Handling Rate Limits
A 429 Too Many Requests from the account limits has code: 4201:
{
"success": false,
"message": "Rate limit exceeded. Please slow down and try again later.",
"code": 4201,
"retryAfter": 60,
"limits": { "perSecond": { "limit": 600, "remaining": 0, "resetAt": 1790690000 } }
}Wait retryAfter seconds (the same value is in the Retry-After header) before retrying. limits shows each window's state; the example is trimmed. A key's own cap answers the same way with code: 4202 and "message": "API key rate limit exceeded.". The in-flight cap answers code: 4203 (above).
A 429 can also mean a lane is full rather than a limit reached: AT_CAPACITY (Full SERP, AI answers, shopping), BUSY (SERP HTML) and VERIFIER_BUSY (e-mail verifier) carry a Retry-After too, and are not charged. See Error Handling.
Error Handling
Every error answers JSON with success: false and a human-readable message, and almost every one adds a stable machine-readable name for the failure. Branch on that name and the status, not on message, which may be reworded. The name is in code; on a few errors, marked † below, it is in errorCode instead, so read code and fall back to errorCode.
Error Response Format
{
"success": false,
"code": "AT_CAPACITY",
"message": "Google SERP is at capacity. Retry in 60s — immediate retries will be refused.",
"retryable": true,
"retryAfterSec": 60
}- •
retryablesays whether the same request can succeed later. When it istrue, waitRetry-After(header) orretryAfterSec(body) before retrying. - •Validation errors add an
errorsarray of{ field, message }:
{
"success": false,
"code": "VALIDATION_ERROR",
"message": "Query validation failed: \"q\" is required",
"errors": [{ "field": "q", "message": "\"q\" is required" }]
}- •Insufficient-credit errors (
402) adddetails:
{
"success": false,
"error": "Insufficient credits",
"code": "INSUFFICIENT_CREDITS",
"details": { "required": 5, "available": 2, "shortfall": 3 },
"message": "This request requires 5 credit(s), but you only have 2 credit(s) available. Please purchase more credits to continue.",
"topUpUrl": "https://dashboard.searlo.tech/credits?tab=buy&source=api_402"
}Codes
Every status and code the API answers, in one table. A call that answers with one of them is never charged: it releases the credits it held, and the 429s for rate limits and searches in flight are refused before any credit is reserved. A 200 that finds nothing (an empty list) is not charged either. The one exception is a failed row inside a 200 POST /rankings/batch, which is billed with the batch (see Refunds). † marks a name carried in errorCode.
| Status | code | Meaning | Retry |
|---|---|---|---|
400 | VALIDATION_ERROR | A parameter is missing, malformed or out of range (errors lists which). Also a URL that cannot be fetched as written, and an image page past Google's last | Fix the request |
400 | INVALID_JSON | The body is not valid JSON | Fix the request |
400 | INVALID_IDEMPOTENCY_KEY | The idempotency key is longer than 255 characters | Fix the request |
400 | CITY_LOCATION_UNAVAILABLE | location or uule names a city or a state; only countries are applied (Location) | No |
400 | UNKNOWN_LOCATION | location is not a country we recognise; suggestions lists close matches | Fix the request |
400 | INVALID_DOMAIN | A rank check's domain cannot be parsed | Fix the request |
400 | PAGE_OUT_OF_RANGE † | Web, site and advanced search: the page starts past result 101. maxPage is the last page that can be served | No |
400 | DATE_FILTER_UNAVAILABLE † | A date filter (dateRestrict, dateRange) combined with search operators such as site: or filetype: | Remove the filter or the operators |
400 | INVALID_REQUEST † | Web data: the fetcher did not accept the URL; message says why | Fix the request |
400 / 404 | PLACE_NOT_FOUND † | Place details: the placeId names no business or q matched nothing (400), or Google does not know the id (404) | No |
401 | UNAUTHORIZED | A missing, invalid or expired API key, or a removed account | Fix the key |
402 | INSUFFICIENT_CREDITS | Not enough credits for this call. details gives required, available and shortfall, and topUpUrl links to buying more. On POST /extract, on crawl and batch-scrape starts and on async tasks the name is in errorCode † | After a top-up |
403 | FORBIDDEN | The account is suspended | No |
403 | FEATURE_NOT_AVAILABLE | The endpoint is switched off on this deployment; retryable: false | No |
403 | API_KEY_SCOPE_FORBIDDEN | An API key used on an account-management route (API keys, data export, account deletion, payments, billing); those need a dashboard login. GET /user/profile stays open to keys | No |
404 | NOT_FOUND | Unknown route or resource | No |
409 | IDEMPOTENCY_REQUEST_IN_PROGRESS | A request with this idempotency key is still running | Shortly |
413 | PAYLOAD_TOO_LARGE | The request body is too large | No |
422 | QUERY_NOT_SERVED | Full SERP and SERP HTML: a query shape these endpoints do not serve (search operators, e-mail lookups, long OR chains) | No |
422 | COUNTRY_UNAVAILABLE | Full SERP and SERP HTML: this gl cannot be served right now | Later |
422 | IDEMPOTENCY_KEY_REUSED | This idempotency key was already used for a different request | With a new key |
422 | TARGET_REFUSED † | Web data: the site refused the fetch; targetStatus is its own status | Only if retryable |
422 | TARGET_NOT_ALLOWED | Web data: a private or reserved network address | No |
422 | TARGET_UNREACHABLE † | Web data: no attempt could fetch the site (its secure-connection handshake is refused, it redirects in a loop, or the page is too large); targetError says which | No |
429 | 4201 | The account's rate limit | After Retry-After |
429 | 4202 | The API key's own rate limit | After Retry-After |
429 | 4203 | The account has too many searches in flight (errorCode CONCURRENCY_LIMIT; see Rate Limiting) | After Retry-After (1 second) |
429 | AT_CAPACITY | Full SERP, rank checks, AI answers, shopping: at capacity | After Retry-After |
429 | BUSY | SERP HTML: at capacity | After Retry-After |
429 | VERIFIER_BUSY | E-mail verifier: verification is paced; Retry-After is 30 seconds by default | After Retry-After |
500 | INTERNAL_ERROR | Something failed on our side | Yes, with backoff |
503 | UPSTREAM_UNAVAILABLE | The upstream (Google, an AI engine, or our own backend) did not give a usable answer; retryable: true | Yes, after Retry-After |
503 | UPSTREAM_RATE_LIMITED | Search is backing off from its upstream; retryAfter is the wait | Yes, after Retry-After |
503 | AT_CAPACITY | Web, image, news and the other search endpoints at capacity (a 429 on SERP and AI answers) | Yes, after Retry-After |
503 | WARMING | The search endpoint is starting up | Yes, after Retry-After |
503 | UPSTREAM_TIMEOUT | Our backend did not answer in time; retryable: true | Yes, after Retry-After |
503 | CREDIT_SYSTEM_UNAVAILABLE | Credit checks are briefly unavailable, so the call was refused before it ran; retryable: true | Yes, after Retry-After |
503 | REVIEWS_WITHHELD | Reviews: Google sent the place without its reviews; reviewCount is what it reports; retryable: true | Yes |
503 | SERVICE_UNAVAILABLE | The service cannot take the call right now; see retryable | If retryable |
503 | UPSTREAM_FAILED | A search failed and a retry will not help; retryable: false | No |
The rate-limit 429s carry a numeric code (4201, 4202, 4203); every other code is a string. Errors are JSON on every status, a 5xx included: we answer 503 rather than 502 or 504, which the network in front of us would replace with its own HTML page. Every retryable 503 carries Retry-After.
Idempotency keys
Send X-Idempotency-Key (or Idempotency-Key), up to 255 characters, to make a retry of a paid call safe. Within an hour of the first use:
- •the same key with the same request returns the stored response, marked
Idempotent-Replayed: true, without running or charging again; - •the same key with a different request is
422IDEMPOTENCY_KEY_REUSED, not charged; - •the same key while the first request is still running is
409IDEMPOTENCY_REQUEST_IN_PROGRESS: retry shortly (Retry-After: 2).
A request that failed is not kept, so you can retry it with the same key. A key longer than 255 characters is 400 INVALID_IDEMPOTENCY_KEY. Async tasks take their own idempotencyKey in the body (Async Tasks).
Refunds
A call that fails is not charged: any response with status 400 or above, or with success: false, releases the credits it reserved. An answer with no results (an empty organic, news, videos, images, places, reviews or shopping list) is not charged either, and the body's credits always equals the X-Credits-Deducted header. In POST /search/batch and POST /serp/batch a failed or empty row costs nothing; a row inside a 200 POST /rankings/batch is billed like the rest of that batch (read each row's success and retryable). A failed async task is refunded.
Retrying
Retry only what says it can succeed later: 429s, and 5xx responses with retryable: true. Wait Retry-After when it is present, and back off otherwise (for example 2, 5, then 10 seconds). Because failures are refunded, a retry never bills twice.
MCP Server
Connect AI agents like Claude, Cursor, VS Code Copilot, and Windsurf to Searlo via Model Context Protocol (MCP).
Endpoint
https://api.searlo.tech/api/v1/mcpTransport
Streamable HTTP — the latest MCP specification. No stdio, no Docker, no local bridge needed.
Authentication
Use your API key in the X-API-Key header. The same key works for both MCP and the REST API.
Server Info
GET /mcp/info (no key needed) returns the server's name, version and transport, and the tools it exposes with their credit costs. Read it rather than hard-coding the list below.
Available Tools
| Tool | Credits | Description |
|---|---|---|
web_search | 1 | Google web search: organic results, knowledge panel, featured snippet |
news_search | 1 | Google News articles with publisher and date |
image_search | 1 | Google Images: image URL, thumbnail, dimensions, format and source page |
shopping_search | 1 | Google Shopping listings: price, merchant, shipping, rating |
maps_search | 1 | Google Maps places: rating, reviews, address, phone, hours, booking link |
scholar_search | 1 | Google Scholar works, with citation counts and the ids to walk the citation graph |
ai_answer | 4–8 | An AI answer engine's answer and the sources it cited: chatgpt 8 (default), gemini 4, copilot 8 |
scrape | 1 | One page as markdown, text, html, links, images, headings, tables or JSON-LD |
site_map | 1 | The URLs a site publishes, from robots.txt and its sitemaps |
search_and_scrape | 1 + 1 per page read | Web search, then the top results read as markdown |
check_balance | 0 | Current credit balance and low-balance status |
get_usage_stats | 0 | Credit usage for a period |
get_search_history | 0 | Your past searches |
get_search_stats | 0 | Search analytics: timeline, top queries, type distribution |
get_transaction_history | 0 | Credit transactions |
list_api_keys | 0 | Your API keys, masked |
A failed tool call is not charged. toon output (the default on the search tools) is a compact tabular format that takes about 63% fewer tokens than the same results as JSON.
Tool Parameters
- •
web_search:query(required, 1–2048 chars),num(1–10, default 10),page(default 1),gl(2-letter country),lr(results language, e.g.en),safe(off,medium,high),format(toon,jsonorenhanced) - •
news_search:query(required),num(1–10),page(1–100),gl,hl,dateRestrict(d7= last 7 days; alsow,m,y),format(toonorjson) - •
image_search:query(required),num(1–10),page(1–100),size(icontohuge),imgType(photo,clipart,lineart,face,stock,animated),imgColorType(color,gray,mono,trans),gl,hl,format - •
shopping_search:query(required, 1–500 chars),num(1–60, default 55),gl,hl,format - •
maps_search:query(required, 1–500 chars),num(1–20, default 20),page,gl,hl,ll(Google's@lat,lng,zoomzform, e.g.@30.27,-97.74,13z) orlat/lng/zoom(1–21),format - •
scholar_search:query(required, 1–500 chars),num(1–20, default 10),page,hl,yearFrom,yearTo,sortByDate,includePatents,includeCitations,cluster(all versions of a work),cites(works citing it),format - •
ai_answer:query(required, 1–500 chars),engine(chatgpt, the default,copilotorgemini;perplexityis paused, andaimodeandgrokare switched off),cache(default true) - •
scrape:url(required),formats(up to 8 ofmarkdown,text,html,links,images,headings,tables,json_ld; default["markdown"]),onlyMainContent(default true),render,includeLinks,includeImages,excludeSelectors(up to 20),waitForSelector,timeout(1000–60000 ms),cache(default true),maxChars(500–200000) - •
site_map:url(required),limit(1–5000, default 1000),search(keep URLs containing this),includeSubdomains,sitemapOnly,cache - •
search_and_scrape:query(required, 1–500 chars),limit(results to read, 1–20, default 5),scrape(falsereturns the search only, for 1 credit),formats(markdown,text,html,json_ld),onlyMainContent,render,gl,hl,cache,maxChars - •
get_usage_stats:days(1–365, default 30) - •
get_search_history:limit(1–10, default 10),page(default 1),searchType(web,imageornews) - •
get_search_stats:startDate,endDate(ISO dates),groupBy(hour,day,weekormonth; defaultday) - •
get_transaction_history:limit(1–10, default 10),page(default 1),type(deduction,grant,refundorpurchase) - •
check_balance,list_api_keys: no parameters
Quick Setup
Paste the config into your AI client's MCP settings file and replace sk_your_api_key with your key.
Changelog
Changes to the API's behaviour and contract, newest first. Additive fields are listed too, so a strict client knows what to expect.
2026-10-20
- •Pricing may change during November–December 2026. What it costs us to serve some requests changes at the turn of the year, so we may adjust prices in those two months. Any change is announced in advance here and by email. Credits you have already bought keep working, and packs stay valid for 12 months from purchase.
- •Full SERP: rendered AI Overview costs 8 from 2026-11-01.
GET /serp,GET /search/serp,POST /serp/batchand asyncserptasks cost 3 credits per page as today, or 8 when the AI Overview had to be completed by rendering the page in a browser. The response saysaioverview.rendered: truewhen that happened (live now, so you can see which of your calls it affects). A page whose overview arrives complete, or a request withaioverview=false, stays 3.GET /serp/featuresstays 3 and never renders;GET /serp/htmlstays 2 (8 withjs=true). - •Web data: a page that needed a browser costs 5 from 2026-11-01. On
POST /scrape,GET|POST /search/webpage,POST /extract,POST /search-and-scrape,POST /crawlandPOST /scrape/batch, a page that had to be rendered (render: true,actions, or automatic rendering when a plain fetch returns no usable content, such as a JavaScript-only page) costs 5 credits instead of 1. Pages fetched without a browser stay 1. Responses already sayvia: "browser"orvia: "http". A cached copy costs the same as the page it copies, a failed fetch is never charged, and the live-fetch surcharge (maxAgeorcache: false) still applies on top. A crawl'smaxCreditsceiling allows for the browser price unless the job setsrender: false.POST /mapis unchanged. - •Accounts that have never bought credits. Every endpoint works on the sign-up credits, but the automatic browser fallback is off (an explicit
render: trueoractionsstill renders) and a web search that comes back thin is not topped up with live results. Both switch on with the first purchase. In effect now.
2026-10-15
- •Place details menus from Google Search. Google Maps now shows signed-out visitors a reduced place record without the menu, so
menucame backnull. For a place that serves food,GET /search/place-detailsreads the menu (url,sectionsof items with prices) from the place's Google Search panel. Addsplace.menuStatus(found,none,pending,unavailable) and the parametermenu=falseto skip the lookup.menu.photosis empty for now. Same price.
2026-10-04 to 2026-10-05
- •Credit balance.
GET /credits/balancereturns{ success, credits: { available, reserved?, expiresAt?, expiringCredits? } }for an API key or a dashboard login. Free.availableis the balanceGET /user/profileshows (Account & Utilities). - •API keys. A key is for the data API and read-only views of its account. Creating or deleting keys, data export, account deletion and payments need a dashboard login (
403API_KEY_SCOPE_FORBIDDEN);GET /user/profilestays open to keys. Keys are stored hashed, and lists show a masked preview. - •Errors. One table of every status and code (Error Handling). New:
400PAGE_OUT_OF_RANGE,DATE_FILTER_UNAVAILABLEandINVALID_IDEMPOTENCY_KEY,422TARGET_UNREACHABLE,4294203(CONCURRENCY_LIMIT),503CREDIT_SYSTEM_UNAVAILABLEandREVIEWS_WITHHELD. A switched-off endpoint answers403FEATURE_NOT_AVAILABLE(was503); a site that refuses the fetch answers422TARGET_REFUSED(was503). No502or504leaves the API: both are answered as503JSON. None of these is charged. - •Idempotency keys are bound to the request they came with: the same key and request replays the stored response (
Idempotent-Replayed: true, no charge), the same key with a different request is422IDEMPOTENCY_KEY_REUSED. - •SafeSearch on the SERP.
safe=activeoroff(andtrueorfalse) is accepted onGET /serpandGET /search/serp.POST /serp/batchandGET /serp/featureshonoursafe, which they ignored before. - •Date filter.
dateRestrict(d7,w2,m1,y1) works onGET /search/webtogether withglandhl, onGET /search/newsand onGET /search/advanced. Combined with search operators it returns400DATE_FILTER_UNAVAILABLE, not charged. - •Fresh answers.
cache=falseforces a fresh answer on web, image, news, maps and places search. Same credits. - •Web search pages. A page that starts past result 101 returns
400PAGE_OUT_OF_RANGEwithmaxPageat once, instead of a slow failure. - •Image search. Results carry
width,height,thumbnailUrlwith its size,linkandsourceUrl(the page the image is on),source,domain,formatandimageId.size(alsoimgSize),imgType,imgColorTypeandimgDominantColornow take effect. - •Maps, places and place details. New
businessStatus(OPERATIONAL,CLOSED_TEMPORARILY,CLOSED_PERMANENTLYornull) andopenNow.openStatecomes back in the language ofhl. With noll,glsets where the search looks (51 countries). Aliteanswer is never cached. - •Reviews. The first page of reviews (about eight) comes back in any language, with
hasMore. Newtimestamp(Unix seconds); an owner's reply carriesdateandlanguage. A place Google sent without its reviews answers503REVIEWS_WITHHELD, not charged. - •Extract. URLs are fetched in parallel inside one time budget. A failed row carries
errorCodeandretryable;partialsays the budget ran out before every URL was fetched;llmSkippedsays the model was not asked. One credit per page fetched; failed rows are free. - •Web data.
scrape,map,crawl,scrape/batchandextractaccept internationalised domain names (https://www.löschwagen.com). A malformed URL, or one with a control character, is a400. - •Searches in flight. An account can have a limited number of web, image, site, advanced and batch searches running at once; past it,
4294203(Rate Limiting). - •No charge for nothing. An answer with no results is free on every endpoint, SERP pages are billed per page delivered, and a failed row inside a
200POST /serp/batchcosts nothing.
2026-10-02
- •Place details.
GET /search/place-detailsreturns one place's full record byplaceId(ChIJ…),featureId,cidorq: every maps field plus all of Google's attributes and the menu (url,sectionsof items with prices,photos). 1 credit. - •Attributes on maps and places.
GET /search/mapsandGET /search/placesaddattributes, Google's "About" list keyed by Google's own ids (attributes.payments.requires_cash_only). - •Reviews by Place ID.
GET /search/reviewsacceptsplaceId. - •Opening date on maps and places.
GET /search/mapsandGET /search/placesaddopeningDate(2026-11-01) andopeningDateText(Opens 1 Nov, in yourhl) to a place listed before it opens;nullotherwise. Same price. - •SERP HTML price.
GET /serp/htmlcosts 2 credits per page (was 1), and 8 per page withjs=true(was 4). Failed calls are still refunded. - •Full SERP AI Overview from the served page.
GET /serpreads the AI Overview in full from the page as Google served it, so most overviews no longer wait for a rendered page: about 2 s instead of 8-10 s. An overview Google loads after the page comes back aspending: trueand is still read from the rendered page. Overview sources come back resolved (urlthe cited page,redirectUrlthe/goto,resolved: true). Same price.
2026-09-30
- •Credits are valid for one year per grant (was 90 days) on accounts created after the policy launch; existing credits were moved to one year from when they were granted.
GET /credit/balancereportsexpiry.durationDays: 365. Legacy accounts keep unlimited credits. - •Google News results page from the live page.
GET /search/news/serp(and thenews/serpasync task and the Cloro-compatible news monitor) is fetched live and parsed like Full SERP: about a second per page, cards with title, source, date, snippet, thumbnail and resolved article links. - •Location.
locationtakes a country by name or two-letter code on every Google endpoint and applies it asgl; a country'suuleis applied the same way. City- and state-levellocation, and any otheruule, return400CITY_LOCATION_UNAVAILABLEand are not charged: Google no longer localizes results byuule(Location).GET /serp/htmlaccepts a countrylocation.GET /search/statesaddstargeting: false. - •Error codes. Error bodies carry a stable
codeon search, SERP, SERP HTML, AI answer, rank check and validation errors (Error Handling). Failed batch rows carry the samecode. - •Async tasks. Errors in the request itself (a location, a domain, a query shape) fail at once and are refunded, instead of being retried.
- •Full SERP. Each page is fetched live and parsed; the AI Overview is read in full from the rendered page, and
aioverview.completesays whether it was.aiModeAvailablereports the page's AI Mode tab. A multi-page request with a failed page is no longer cached. - •SERP HTML.
aio.showAll(pressed,absent,complete,failed) andshow-allinX-Searlo-Aio. - •Web data.
POST /search/webpageacceptsexcludeSelectors; a page whose plain fetch returns no usable content is rendered.
2026-09-29
- •SERP HTML (
GET /serp/html): Google's results page as served (1 credit at launch; 2 since 2026-10-02) or rendered with the AI Overview opened out (js=true, 4 credits at launch; 8 since 2026-10-02), desktop or iOS, as JSON orformat=raw. - •AI answer snapshots:
html=trueonGET /search/ai/{engine}adds the answer page's HTML (html,pageUrl) at no extra charge. - •Full SERP:
rawHtml=truereturns the page alongside the JSON (+1 credit). - •Account & Utilities:
GET /credit/balance,GET /search/capabilities,GET /search/countries,GET /search/statesdocumented. - •MCP: all 16 tools documented.