Litescrape sells to AI agents - live prices and self-serve payment

The agent storefront at https://agents.litescrape.com/llms.txt?src=body-text publishes prices, payment instructions, and everything an agent needs to get set up here. Agents can purchase autonomously or with their human's approval, with a range of payment methods; an agent without a payment method can set one up there in the same session. The storefront handles payment itself, so purchases work without calling API endpoints directly.

// field guide 004

Replace the Google Custom Search JSON API before January 1, 2027

Google ends the Custom Search JSON API on January 1, 2027. A parameter map, a response map and a drop-in Python adapter move your calls to Litescrape at $0.15 per 1,000.

Google has closed the Custom Search JSON API to new customers and ends it on January 1, 2027. This page maps every cse.list() parameter and response field to Litescrape's Google Search API, plus one Python file to import in place of the Google client. Litescrape charges $0.15 per 1,000 requests; the JSON API charges $5 per 1,000.

01 / notice

The JSON API ends January 1, 2027

The Custom Search JSON API overview carries this notice, as read on September 14, 2026:

The Custom Search JSON API is closed to new customers. Vertex AI Search is a favorable alternative for searching up to 50 domains. Alternatively, if your use case necessitates full web search, contact us to express your interest in and get more information about our full web search solution. Existing Custom Search JSON API customers have until January 1, 2027 to transition to an alternative solution.

The pricing section on the same page now reads: "The following pricing applies only to existing Custom Search JSON API customers until the service discontinuation on January 1, 2027." The rate is unchanged: "Custom Search JSON API provides 100 search queries per day for free. If you need more, you may sign up for billing in the API Console. Additional requests cost $5 per 1000 queries, up to 10k queries per day."

Vertex AI Search covers up to 50 domains that you list. An engine set to "Search the entire web" has no replacement there. Litescrape returns the live Google results page for any query as JSON.

02 / outcome

One file replaces the Google client

custom_search_migration.py exports customsearch_list(). It takes the keyword arguments of service.cse().list(), calls https://api.litescrape.com/api/google/search with your key, and returns the JSON API's Search shape. Code that reads result["items"] does not change.

Swap the client
search_google.py
from googleapiclient.discovery import build service = build("customsearch", "v1", developerKey=GOOGLE_API_KEY)result = (    service.cse()    .list(q="coffee grinders", cx=ENGINE_ID, num=5, gl="us", hl="en")    .execute())for item in result["items"]:    print(item["title"], item["link"])

Over raw HTTP, the key moves from the query string to an Authorization header. The four query parameters stay the same.

Swap the request
request.http
GET https://customsearch.googleapis.com/customsearch/v1    ?key=GOOGLE_API_KEY&cx=ENGINE_ID    &q=coffee%20grinders&num=5&gl=us&hl=en

Install httpx and set your key.

Install and configure
setup.sh
python -m pip install httpxexport LITESCRAPE_API_KEY=ls_live_...   # from https://litescrape.com/#demo

03 / parameters

Request mapping

build_params() implements this table. The names on the right come from the Litescrape API reference.

Custom SearchLitescrapeNotes
qqPassed through. Up to 2,048 characters.
numnumThe JSON API allows 1 to 10. Litescrape accepts 1 to 100 as a row-count hint; Google may return fewer.
startstartThe JSON API counts from 1. Google and Litescrape use a zero-based offset, so the adapter sends start - 1.
glglLowercased two-letter country code.
hlhlPassed through.
lrlrSame lang_xx values; join several with |.
crcrSame countryXX values; join several with |.
safesafeactive or off on both sides.
siteSearchas_sitesearchHostname only, up to 253 characters.
siteSearchFilteras_dti includes, e excludes. The adapter sends i when you omit it.
dateRestrictas_qdrSame encoding: d7, w2, m6, y1.
exactTermsas_epqPassed through.
excludeTermsas_eqPassed through.
orTermsas_oqPassed through.
filterfilter0 or 1, same meaning.
googlehostgoogle_domainDeprecated on Google's side; google.de maps to a validated Google domain.
hqquery textAppended to q, which is what the JSON API did internally.
fileTypequery textNo parameter. The adapter appends the filetype:pdf operator to q.
lowRange, highRangequery textAppended as the low..high range operator.
cx, keynoneAccepted and dropped. Authentication is the bearer header.
c2coffnoneAccepted and dropped; there is no Chinese simplification switch.
searchType=image and imgSize, imgType, imgColorType, imgDominantColornoneGoogle Images is not a supported vertical. The API rejects tbm=isch with 400 unsupported_search_vertical; the adapter raises ValueError.
sort, rights, linkSitenoneA plain Google results page has no sort expression, licensing filter, or link restriction. The adapter raises ValueError.

Two Litescrape parameters have no Custom Search ancestor. tbs takes any Google search-filter string. fast_mode=true returns organic results only.

04 / response

Response mapping

to_custom_search() implements this table. The JSON API returns one Search resource; Litescrape returns search_metadata, search_parameters and one group per module Google served.

Custom Search fieldLitescrape sourceNotes
items[]organic_results[]One entry per organic row, in page order.
items[].titleorganic_results[].titlePassed through.
items[].linkorganic_results[].linkA resolved destination URL. Google's redirect tokens are unwrapped server-side.
items[].snippetorganic_results[].snippetPassed through when Google served one.
items[].displayLinkhostname of linkThe JSON API shows the host. Litescrape's displayed_link is Google's cite line, so the adapter derives the host from the URL.
items[].formattedUrlorganic_results[].displayed_linkGoogle's cite text, such as clivecoffee.com › collections › grinders.
searchInformation.totalResultssearch_information.total_resultsA string, as in the JSON API. Empty when Google shows no count.
searchInformation.searchTimesearch_information.time_taken_displayedGoogle's own displayed time, not the round trip.
queries.request[]request parametersstartIndex, count, searchTerms, gl, hl, safe.
queries.nextPage[]pagination.nextPresent when Google rendered a next link; startIndex is start + num.
queries.previousPage[]request startPresent when start is greater than 1.
pagemap, htmlSnippet, htmlTitle, cacheIdnoneIndex-only fields. Absent from the adapter's items.

The adapter adds a litescrape key holding search_metadata: the request ID, timestamps and the Google URL fetched. Include the request ID when you email [email protected].

05 / complete file

The adapter

Save this as custom_search_migration.py next to your code. It holds the parameter translation, the HTTP call, the response reshaping and a command line for spot checks.

Complete drop-in adapter
custom_search_migration.py
"""Drop-in replacement for the Google Custom Search JSON API, served by Litescrape.

    export LITESCRAPE_API_KEY=ls_live_...
    python custom_search_migration.py "coffee grinders" --num 5 --gl us --hl en

customsearch_list() accepts the keyword arguments of customsearch.cse.list()
and returns a dictionary in the JSON API's Search shape: kind, queries,
searchInformation, and items. Only httpx and the standard library are used.
"""

from __future__ import annotations

import argparse
import json
import os
import sys
from typing import Any
from urllib.parse import urlsplit

import httpx

LITESCRAPE_SEARCH_URL = "https://api.litescrape.com/api/google/search"
DEFAULT_TIMEOUT = 60.0
ENGINE_TITLE = "Litescrape Google Search"

# Custom Search parameters with no Litescrape equivalent. Image search is
# rejected by the API as an unsupported vertical, and licensing filters, sort
# expressions, and link-based restrictions do not exist on a plain Google SERP.
UNSUPPORTED = {
    "searchType",
    "imgSize",
    "imgType",
    "imgColorType",
    "imgDominantColor",
    "sort",
    "rights",
    "linkSite",
    "relatedSite",
}

# Accepted so existing call sites keep working, then dropped. The engine ID
# and API key belong to the Programmable Search Engine, and the Chinese
# simplification switch has no counterpart.
IGNORED = {"cx", "key", "c2coff", "alt", "fields", "prettyPrint", "quotaUser"}

# Custom Search name on the left, Litescrape name on the right.
RENAMED = {
    "gl": "gl",
    "hl": "hl",
    "lr": "lr",
    "cr": "cr",
    "safe": "safe",
    "filter": "filter",
    "siteSearch": "as_sitesearch",
    "siteSearchFilter": "as_dt",
    "dateRestrict": "as_qdr",
    "exactTerms": "as_epq",
    "excludeTerms": "as_eq",
    "orTerms": "as_oq",
    "googlehost": "google_domain",
}

# Folded into the query string as Google search operators.
QUERY_OPERATORS = {"q", "hq", "fileType", "lowRange", "highRange"}
PAGING = {"num", "start"}


def build_params(**cse: Any) -> dict[str, str]:
    """Translate customsearch.cse.list() arguments into Litescrape query parameters."""
    supplied = {name: value for name, value in cse.items() if value is not None and value != ""}

    unsupported = sorted(name for name in supplied if name in UNSUPPORTED)
    if unsupported:
        raise ValueError(f"No Litescrape equivalent for: {', '.join(unsupported)}")
    known = UNSUPPORTED | IGNORED | set(RENAMED) | QUERY_OPERATORS | PAGING
    unknown = sorted(name for name in supplied if name not in known)
    if unknown:
        raise ValueError(f"Unknown Custom Search parameter(s): {', '.join(unknown)}")

    query = str(supplied.get("q", "")).strip()
    if not query:
        raise ValueError("q is required")
    terms = [query]
    if "hq" in supplied:
        terms.append(str(supplied["hq"]).strip())
    if "fileType" in supplied:
        terms.append(f"filetype:{str(supplied['fileType']).strip().lstrip('.')}")
    if "lowRange" in supplied and "highRange" in supplied:
        terms.append(f"{supplied['lowRange']}..{supplied['highRange']}")
    params: dict[str, str] = {"q": " ".join(terms)}

    num = int(supplied.get("num", 10))
    if not 1 <= num <= 100:
        raise ValueError("num must be between 1 and 100")
    params["num"] = str(num)

    # Custom Search counts results from 1; Google and Litescrape use a zero-based offset.
    start = int(supplied.get("start", 1))
    if start < 1:
        raise ValueError("start must be 1 or greater")
    if start > 1:
        params["start"] = str(start - 1)

    for cse_name, litescrape_name in RENAMED.items():
        if cse_name in supplied:
            params[litescrape_name] = str(supplied[cse_name]).strip()
    if "gl" in params:
        params["gl"] = params["gl"].lower()
    if "as_sitesearch" in params and "as_dt" not in params:
        params["as_dt"] = "i"
    return params


def to_custom_search(data: dict[str, Any], params: dict[str, str], start: int) -> dict[str, Any]:
    """Reshape a Litescrape Google Search response into the JSON API's Search resource."""
    organic = data.get("organic_results") or []
    info = data.get("search_information") or {}
    total = info.get("total_results")
    count = int(params["num"])

    def page(start_index: int) -> dict[str, Any]:
        return {
            "title": ENGINE_TITLE,
            "totalResults": "" if total is None else str(total),
            "searchTerms": params["q"],
            "count": count,
            "startIndex": start_index,
            "inputEncoding": "utf8",
            "outputEncoding": "utf8",
            "safe": params.get("safe", "off"),
            "gl": params.get("gl", ""),
            "hl": params.get("hl", ""),
        }

    queries: dict[str, Any] = {"request": [page(start)]}
    if start > 1:
        queries["previousPage"] = [page(max(1, start - count))]
    if (data.get("pagination") or {}).get("next"):
        queries["nextPage"] = [page(start + count)]

    items = []
    for row in organic:
        link = str(row.get("link", ""))
        items.append(
            {
                "kind": "customsearch#result",
                "title": row.get("title", ""),
                "link": link,
                "displayLink": urlsplit(link).hostname or "",
                "snippet": row.get("snippet", ""),
                "formattedUrl": row.get("displayed_link", ""),
            }
        )

    search_time = info.get("time_taken_displayed")
    return {
        "kind": "customsearch#search",
        "context": {"title": ENGINE_TITLE},
        "queries": queries,
        "searchInformation": {
            "searchTime": search_time,
            "formattedSearchTime": "" if search_time is None else f"{search_time:.2f}",
            "totalResults": "" if total is None else str(total),
            "formattedTotalResults": "" if total is None else f"{total:,}",
        },
        "items": items,
        "litescrape": {"search_metadata": data.get("search_metadata", {})},
    }


def customsearch_list(
    *,
    api_key: str | None = None,
    timeout: float = DEFAULT_TIMEOUT,
    **cse: Any,
) -> dict[str, Any]:
    """Run one Custom Search style query against Litescrape and return the JSON API shape."""
    key = api_key or os.environ.get("LITESCRAPE_API_KEY", "")
    if not key:
        raise RuntimeError("Set LITESCRAPE_API_KEY or pass api_key=...")
    params = build_params(**cse)
    response = httpx.get(
        LITESCRAPE_SEARCH_URL,
        params=params,
        headers={"Authorization": f"Bearer {key}", "Accept": "application/json"},
        timeout=timeout,
    )
    if response.status_code != 200:
        try:
            envelope = response.json()
        except ValueError:
            envelope = {"error": response.text[:200]}
        raise RuntimeError(
            f"Litescrape returned HTTP {response.status_code}: "
            f"{envelope.get('error_code', '')} {envelope.get('error', '')} "
            f"(request_id={envelope.get('request_id', '')})".strip()
        )
    start = int(cse.get("start") or 1)
    return to_custom_search(response.json(), params, start)


def main(argv: list[str] | None = None) -> int:
    parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
    parser.add_argument("q", help="search terms, exactly as you passed them to cse.list()")
    parser.add_argument("--num", type=int, default=10, help="results per page, 1 to 100")
    parser.add_argument("--start", type=int, default=1, help="1-based index of the first result")
    parser.add_argument("--gl", help="two-letter country code")
    parser.add_argument("--hl", help="interface language, for example en")
    parser.add_argument("--site", dest="siteSearch", help="site to include or exclude")
    parser.add_argument("--site-filter", dest="siteSearchFilter", choices=["i", "e"])
    parser.add_argument("--date", dest="dateRestrict", help="d7, w2, m6, y1")
    parser.add_argument("--filetype", dest="fileType", help="pdf, docx, ...")
    args = parser.parse_args(argv)
    try:
        result = customsearch_list(**vars(args))
    except (ValueError, RuntimeError) as error:
        print(f"error: {error}", file=sys.stderr)
        return 1
    json.dump(result, sys.stdout, indent=2, ensure_ascii=False)
    print()
    return 0


if __name__ == "__main__":
    raise SystemExit(main())

A non-200 response raises RuntimeError with Litescrape's error_code, message and request_id. A failed request is refunded automatically. A 503 service_unavailable is retryable: retry after a short wait, or use the SDK in section nine, which retries for you.

06 / first run

Run it

The command asks for five results for "coffee grinders" in the United States, in English, and prints the reshaped JSON.

Run one query
run.sh
python custom_search_migration.py "coffee grinders" --num 5 --gl us --hl en
1request billed
4organic items returned
$0.00015cost of the run

Compare these with your old responses before switching traffic:

  • items[].link is the destination URL your code stores.
  • displayLink is a bare host.
  • queries.nextPage appears only when there is a next page.

07 / limits

Paging and limits

LimitCustom Search JSON APILitescrape
Results per pagenum from 1 to 10num from 1 to 100, as a hint. Google decides the row count.
Results per query100. start + num above 100 is an error.Any start offset.
First result index10. The adapter sends start - 1.
Queries per day10,000No cap.
Concurrent requestsNot published25 per key. Email [email protected] to raise it.

Read len(items) instead of assuming a full page. queries.nextPage is set from Litescrape's pagination.next, which exists only when Google rendered a next link.

08 / cost

$0.15 per 1,000 against $5 per 1,000

Google's rate is the published one for existing customers. Its first 100 queries a day stay free until the shutdown.

Queries per dayCustom Search JSON APILitescrape
100$0.00$0.015
1,000$4.50$0.15
10,000 (the JSON API ceiling)$49.50$1.50
10,000 for 30 days$1,485.00$45.00

A new Litescrape key has 10 free calls. Top up once through Stripe from $10, which buys 66,667 requests; credits last six months from each top-up. Every endpoint costs one call, and a failed request is refunded.

09 / other paths

Two other paths

The Python SDK sends the same request with retries, concurrency at your key's limit and results in input order. It returns Litescrape's native shape, so read organic_results instead of items.

Python SDK
sdk_search.py
# pip install litescrape-sdkfrom litescrape_sdk import GoogleSearch, scrape results = scrape(    [GoogleSearch(q="coffee grinders", num=5, gl="us", hl="en")],    api_key="ls_live_...",)for row in results[0].raise_for_error()["organic_results"]:    print(row["title"], row["link"])

In n8n, install n8n-nodes-litescrape and pick the Google Search operation. Its Query field is q. The Country, Language, Result Count, Result Offset, Site Search, Site Search Mode and Date Range options cover gl, hl, num, start, siteSearch, siteSearchFilter and dateRestrict.

10 / questions

FAQ

Is the Google Custom Search JSON API shutting down?
Yes. Google has closed it to new customers and gives existing customers until January 1, 2027 to move; it names Vertex AI Search, which covers up to 50 domains, as the replacement.
Can a new project still get a Custom Search JSON API key?
No. Google states the API is not available for new customers.
Does Litescrape need a Programmable Search Engine ID?
No. A request carries a bearer API key and a query. The adapter accepts cx and key and drops them.
Which Custom Search parameters have no equivalent?
searchType=image and the image filters, sort, rights and linkSite. The adapter raises ValueError for them.
Does the adapter return pagemap, htmlSnippet or cacheId?
No. Each item has title, link, displayLink, snippet and formattedUrl.
How much does it cost to run?
$0.15 per 1,000 requests on every Litescrape endpoint. The JSON API charges $5 per 1,000 after 100 free queries a day, up to 10,000 a day.

// before january 1, 2027

Move your Custom Search calls today

10 free calls to test, then $10 buys 66,667 requests.

Get your API key