September 11, 2026

Google CSE Alternative in 2026: How to Replace the Custom Search JSON API

Google CSE Alternative in 2026: How to Replace the Custom Search JSON API

TLDR: Google's Custom Search JSON API is closed to new customers and discontinues on January 1, 2027. Replacing it means choosing between configured website search, model grounding, and a standalone results API. Inventory your engine settings and request parameters first. Then test a limited response adapter, pagination, and failure states before evaluating retrieval quality. The Python example below runs offline by default.

What is shutting down, and what is not?

The deadline applies to the Custom Search JSON API, not automatically to every Programmable Search Engine product. Google's product overview distinguishes the JavaScript Search Element from the JSON API. Its older availability table is not a reason to ignore the newer, API-specific closure notice. Eligibility concerns customers, not merely whether someone already possesses any Google API key.

Engine scope also needs precision. Google's current transition guidance says new engines must use Sites to search, while existing engines already configured for Search the entire web can retain that option until January 1, 2027. Do not describe all historical CSE integrations as site-restricted. Record the actual configuration before migration; the same guidance warns that turning the whole-web option off cannot be reversed.

Which Google and standalone alternatives fit the contract?

Start with the output your application consumes, not a vendor ranking. The matrix separates Google-internal paths from standalone providers. These are documented interfaces and setup paths, not signup attempts or measurements of availability, coverage, latency, or answer quality. None establishes automatic compatibility with your existing engine.

PathDocumented query contractMigration decision
Google: Vertex AI Search website searchSearch an app backed by a website data store. The JSON API overview recommends this for up to 50 domains.Evaluate for defined website collections; provision an app and indexing configuration, not a replacement cx string.
Google: full-web contact routeThe closure notice directs full-web requirements to Google for information.Confirm access, interface, commercial terms, and timing with Google. Do not assume either instant self-service or universal unavailability.
Google: Grounding with Google SearchA Google Search tool grounds Gemini responses using public web information.Consider when generated answers are acceptable. It is not the CSE items contract; review Search Suggestions display requirements.
You.com SearchPOST query and count; optional web and news sections, snippets, and requested extracted content.Adapt results.web for a web-results consumer and preserve evidence beyond the legacy snippet.
Tavily SearchPOST query with max_results and search_depth; result content depends on depth, with optional generated answers.Pin depth and answer settings. Compare result evidence rather than treating an optional answer as a CSE snippet.
Exa SearchPOST search query, numResults, and optional contents; domain or path controls use includeDomains and excludeDomains.Specify the search and content configuration; evaluate filters and result evidence, not brand labels.
Brave Web SearchQuery q with count and offset; web.results can include description and requested extra_snippets.Translate pagination and evidence explicitly; assess Goggles separately if custom ranking matters.

Sources: Google migration notice, website data stores, Google Search grounding, You.com Search reference, Tavily Search reference, Exa Search reference, and Brave Web Search documentation.

For Google's website path, the setup documentation requires an app with Enterprise features enabled. Advanced website indexing adds domain-ownership verification and additional costs. Google's data preparation guide distinguishes URL-pattern limits from domain counts. Check those requirements against the sites you can legitimately index. Grounding has a different integration boundary: when Search Suggestions accompany a grounded response, Google's documentation requires their display. Neither path is fairly dismissed as something nobody can set up.

What must the CSE inventory include?

The request parameter cx identifies persistent engine configuration. Export or document that configuration separately from application code. Google's annotations support include, exclude, promotion, and demotion labels with URL patterns; promotions can trigger on exact queries or regular expressions. Those behaviors do not become an include_domains list automatically.

  • Call sites: endpoint, owning service, customer eligibility, cx values, request volume, and every request parameter actually emitted.
  • Engine behavior: site scope, subdomains, paths, exclusions, labels, refinements, ranking adjustments, and promotion triggers.
  • Consumer dependencies: items, title, link, snippet, PageMap data, image fields, spelling suggestions, total-result estimates, and next-page metadata.
  • Operational contract: authentication storage, timeout policy, cache keys, allowed query data, attribution requirements, and fallback behavior.

The CSE response reference documents these distinct result and metadata structures. This guide's adapter deliberately covers only a small web-item subset. If your product uses image search, promotions, or PageMap-derived cards, require separate implementations and acceptance tests. Renaming three fields cannot reproduce those features.

Which filters need semantic review?

CSE already exposes per-request controls. The cse.list reference includes siteSearch/siteSearchFilter, fileType, dateRestrict, exactTerms, excludeTerms, language restrictions, and SafeSearch. Request-level scoping is not a capability introduced by switching providers.

Compare meaning before renaming. Google's gl boosts country-of-origin matches, cr restricts country of origin, lr restricts document language, and hl sets interface language. You.com's country determines geographic focus and language selects result language. Its safesearch values are off, moderate, and strict, whereas Google's safe uses off or active. Define an explicit policy rather than equating similarly named controls.

Google's dateRestrict accepts numbered day, week, month, and year intervals. You.com's freshness parameter accepts named periods or a date range, and temporal language in the query can make the broader timeframe prevail. Test time boundaries and missing dates; do not silently translate every date expression or use recency as proof of a source's publication date.

You.com documents include_domains as a strict allowlist of up to 500 domains. It cannot combine with exclude_domains or boost_domains; those combinations return 422. Boosting is not filtering, and boost_domains can combine with exclude_domains. Evaluate subdomain and path behavior against actual requirements. Application-side filtering may remove unwanted results, but cannot recover permitted results absent from the retrieved set.

How do you translate pagination without changing the window?

CSE start is one-based and num ranges from 1 to 10. You.com's offset is a zero-based page multiplier of count, with range 0 through 9. For aligned pages, keep count equal to num and calculate offset = (start - 1) / num. Thus start=11,num=10 becomes offset=1,count=10, while start=11,num=5 becomes offset=2,count=5.

An unaligned start cannot be rounded down without changing the requested window. The example rejects it rather than silently fetching different positions. It also follows Google's literal documented start + num limit of 100 conservatively, including rejection of start=91,num=10. That wording sits alongside the broader “no more than 100 results” statement; this offline converter does not resolve endpoint boundary behavior experimentally.

These are position calculations, not promises of identical rankings. Do not replace a pagination loop with one larger request merely because a provider permits a larger count. Fix the page size throughout a comparison, record short pages and duplicates, and test the consumer's navigation. Requested counts are ceilings, not guaranteed populations. Both pagination definitions are in the linked Google and You.com references.

Can you test the adapter before obtaining credentials?

Yes. Save this complete Python 3 standard-library example as migrate.py and run python3 migrate.py. It validates controlled fixtures without network access. The optional HTTP branch follows the documented POST https://ydc-index.io/v1/search endpoint, X-API-Key header, and JSON body. It retains environment proxies and normal TLS verification, and refuses redirects rather than forwarding a credential.

The compatibility view maps url to link and keeps title only when present. It selects the first usable snippets passage, with description as fallback. Every original passage, optional contents object, and absent-versus-null distinction remains in the copied _you envelope. A highlight-only result is preserved there rather than mislabeled as a legacy snippet. Missing sections, empty arrays, unusable URLs, and errors produce different states.

import argparse
import copy
import http.client
import json
import os
import urllib.error
import urllib.parse
import urllib.request
ENDPOINT = "https://ydc-index.io/v1/search"
MAX_BYTES = 2_000_000
def cse_page(start=1, num=10):
    # Conservative: follow cse.list's stated start + num <= 100 rule.
    if type(start) is not int or type(num) is not int:
        raise ValueError("start and num must be integers")
    if start < 1 or not 1 <= num <= 10 or start + num > 100:
        raise ValueError("outside the documented CSE window")
    if (start - 1) % num:
        raise ValueError("unaligned start requires a separate window strategy")
    offset = (start - 1) // num
    if offset > 9:
        raise ValueError("outside You.com offset range")
    return {"count": num, "offset": offset}
def present(obj, key):
    return "absent" if key not in obj else "null" if obj[key] is None else "present"
def adapt(data):
    out = {"state": "error", "items": []}
    if not isinstance(data, dict) or "error" in data:
        return dict(out, reason="invalid_envelope_or_upstream_error")
    out["_you"] = copy.deepcopy(data)  # Preserve all passages and field presence.
    out["results_state"] = present(data, "results")
    results = data.get("results")
    if results is None:
        return dict(out, state="no_web_section")
    if not isinstance(results, dict):
        return dict(out, reason="invalid_results_type")
    out["web_state"] = present(results, "web")
    web = results.get("web")
    if web is None:
        return dict(out, state="no_web_section")
    if not isinstance(web, list):
        return dict(out, reason="invalid_web_type")
    if not web:
        return dict(out, state="empty")
    rejected = 0
    for hit in web:
        if not isinstance(hit, dict):
            rejected += 1
            continue
        url = hit.get("url")
        try:
            parsed = urllib.parse.urlsplit(url if isinstance(url, str) else "")
            valid = parsed.scheme in ("https", "http") and parsed.hostname
        except ValueError:
            valid = False
        if not valid:
            rejected += 1
            continue
        item = {"link": url}
        if "title" in hit:
            item["title"] = hit["title"]  # Preserve explicit null; do not invent a title.
        snippets = hit.get("snippets")
        passages = snippets if isinstance(snippets, list) else []
        first = next((s for s in passages if isinstance(s, str) and s.strip()), None)
        if first is not None:
            item["snippet"] = first
        elif "description" in hit:
            item["snippet"] = hit["description"]
        out["items"].append(item)
    out["rejected"] = rejected
    out["state"] = "partial" if rejected else "ok"
    if not out["items"]:
        out["state"] = "no_usable_results"
    return out
class NoRedirect(urllib.request.HTTPRedirectHandler):
    def redirect_request(self, req, fp, code, msg, headers, newurl):
        return None  # Do not forward credentials to a redirected endpoint.
def live_search(query, key, start=1, num=10, *, allow_network=False):
    if not allow_network:
        raise ValueError("network disabled; explicit opt-in required")
    # Nonblank validation is this client's policy, not a tested server rule.
    if not isinstance(query, str) or not query.strip():
        raise ValueError("provide a nonblank query")
    if not isinstance(key, str) or not key.strip() or "\n" in key or "\r" in key:
        raise ValueError("provide a valid key through the selected environment variable")
    payload = dict(query=query, **cse_page(start, num))
    request = urllib.request.Request(
        ENDPOINT, method="POST", data=json.dumps(payload).encode("utf-8"),
        headers={"X-API-Key": key, "Content-Type": "application/json"},
    )
    # Retain normal environment proxy settings and TLS verification.
    opener = urllib.request.build_opener(NoRedirect())
    try:
        with opener.open(request, timeout=30) as response:
            raw = response.read(MAX_BYTES + 1)
        if not raw or len(raw) > MAX_BYTES:
            return {"state": "error", "reason": "empty_or_oversized_body", "items": []}
        return adapt(json.loads(raw.decode("utf-8")))
    except urllib.error.HTTPError as exc:
        status = exc.code
        exc.close()
        reason = {401: "authentication", 429: "rate_limited"}.get(status, "http_error")
        return {"state": "error", "reason": reason, "http_status": status, "items": []}
    except (UnicodeError, ValueError):
        return {"state": "error", "reason": "invalid_json", "items": []}
    except (urllib.error.URLError, OSError, http.client.HTTPException):
        return {"state": "error", "reason": "transport", "items": []}
FIXTURE = {"results": {"web": [
    {"url": "https://example.com/a", "title": "Example",
     "snippets": ["First passage", "Second passage"], "page_age": None},
    {"url": "https://example.com/b", "description": None},
    {"url": "https://example.com/c", "description": "Fallback", "snippets": []}
]}}
def offline_checks():
    assert cse_page(1, 10) == {"count": 10, "offset": 0}
    assert cse_page(11, 10) == {"count": 10, "offset": 1}
    assert cse_page(11, 5) == {"count": 5, "offset": 2}
    for start, num in [(0, 10), (2, 10), (1, 11), (51, 5), (91, 10), (True, 5)]:
        try:
            cse_page(start, num)
        except ValueError:
            pass
        else:
            raise AssertionError("invalid page accepted")
    mapped = adapt(FIXTURE)
    assert mapped["items"][0]["snippet"] == "First passage"
    assert mapped["_you"] == FIXTURE and mapped["_you"] is not FIXTURE
    assert "title" not in mapped["items"][1]
    assert mapped["items"][1]["snippet"] is None
    assert mapped["items"][2]["snippet"] == "Fallback"
    assert adapt({})["results_state"] == "absent"
    assert adapt({"results": None})["results_state"] == "null"
    assert adapt({"results": {"web": []}})["state"] == "empty"
    assert adapt({"results": {"web": None}})["web_state"] == "null"
    assert adapt({"error": "fixture"})["state"] == "error"
    assert adapt({"results": {"web": "wrong"}})["state"] == "error"
    return {"state": "offline_checks_passed", "network_calls": 0}
def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("--live", action="store_true")
    parser.add_argument("--query")
    parser.add_argument("--key-env", default="YDC_API_KEY")
    parser.add_argument("--start", type=int, default=1)
    parser.add_argument("--num", type=int, default=10)
    args = parser.parse_args()
    if not args.live:
        print(json.dumps(offline_checks()))
        return 0
    try:
        result = live_search(args.query, os.environ.get(args.key_env, ""),
                             args.start, args.num, allow_network=True)
    except ValueError as exc:
        print(json.dumps({"state": "error", "reason": str(exc)}))
        return 1
    # Never print keys, query text, raw provider errors, or response contents.
    print(json.dumps({"state": result["state"], "items": len(result["items"]),
                      "reason": result.get("reason")}))
    return 1 if result["state"] == "error" else 0
if __name__ == "__main__":
    raise SystemExit(main())

To opt into a real request yourself, supply YDC_API_KEY through your secret manager or environment, then run python3 migrate.py --live --query "Python documentation" --key-env YDC_API_KEY. The argument names the environment variable, not the secret value. The CLI prints only status, count, and a sanitized reason. This revision made no authenticated calls and did not create provider accounts.

Nonblank query checks, the response-size cap, and the 30-second timeout are application choices, not tested server guarantees. No automatic retries run here. In production, distinguish authentication failures from rate limits, transport problems, and invalid responses before adopting a bounded retry policy. Never turn a failed request into an empty successful search.

How should cost and rollout gates be measured?

Separate account funding from usage metering. A credit balance is not inherently a different economic model from per-request billing. Compare billed operations, search depth, extraction pages, cache treatment, and any minimum commitments on current terms. Tavily's reference assigns different credit usage to search depths; You.com's reference describes extraction separately from ordinary search. Neither distinction proves which provider is cheaper for bursty or steady traffic.

Build a workload estimate from requests per session, pages requested, retries, and extraction choices, then validate it against invoices or usage records during an authorized pilot. Keep temporary credits separate from steady-state costs. Include parallel evaluation traffic in the migration budget, and record unknown charges as unknown rather than zero.

  1. Configuration gate: each cx dependency has an implemented replacement or a signed-off removal, including promotions and path-level exclusions.
  2. Contract gate: fixture tests pass for optional fields, empty results, malformed responses, authentication, rate limits, and supported page windows.
  3. Retrieval gate: a representative held-out set meets predefined relevance, permitted-domain coverage, freshness, and citation-support requirements.
  4. Operations gate: authorized tests establish tail latency, peak-load behavior, spending limits, and a monitored rollback procedure.

A small pilot finds adapter mistakes; no fixed 20-query sample guarantees migration quality. Size testing around important query slices and uncertainty. Choose parallel-run duration from traffic coverage and risk, not a mandatory one-month rule. Schedule cutover before January 1, 2027 with contingency time, and do not rely on the retiring API as a fallback after discontinuation.

For implementation context, see the search API guide and Python integration guide. Use the evaluation guide to define comparable evidence budgets and failure-inclusive metrics. The next step is an inventory and an offline test pass, followed by an explicitly authorized pilot, not an endpoint swap presented as proven compatibility.

Related Guides

Frequently Asked Questions

The Custom Search JSON API is closed to new customers and discontinues on January 1, 2027, according to Google's current API overview. That is not a blanket shutdown of every Programmable Search Engine offering. Separately, Google's engine transition guidance says existing engines configured for Search the entire web can retain that setting until January 1, 2027, while new engines must use Sites to search. Identify the API and engine mode your application actually uses before choosing a migration path.

Do not assume drop-in compatibility. Google's response includes items and CSE-specific metadata, while You.com Search documents results.web and optional fields. A limited adapter can map url to link, retain title when present, and use the first usable snippets passage for snippet with description as fallback. Preserve every passage and optional contents field separately, including absent versus null values. The article example does not recreate PageMap, promotions, image search, or total-result and next-page metadata, and its offline tests do not establish ranking equivalence.

Yes. You.com Search documents include_domains as a strict allowlist of up to 500 domains. It cannot combine with exclude_domains or boost_domains. Boosting affects ranking rather than excluding other domains. CSE itself already has per-request siteSearch and siteSearchFilter controls. Persistent CSE annotations and URL patterns and promotion rules need separate migration decisions; a domain list does not automatically reproduce them. Validate path scope, subdomains, exclusions, and permitted-domain coverage on your workload.

Google's overview says the Custom Search JSON API is closed to new customers. Having a Google API key or creating a Programmable Search Engine does not by itself establish eligibility. Existing-customer key-management questions require confirmation in the relevant Google project. The same overview recommends Vertex AI Search for up to 50 domains and a contact route for full-web requirements. Google's website-search setup documentation provides a data-store creation flow, with Enterprise app requirements and domain verification for advanced indexing. This article reviewed documentation; it did not create accounts or test new-key issuance.

    Share Article:

  1. LI Test

  2. LI Test

Related resources.

Self-Hosted LLM Serving: Picking a Stack That Survives Real Traffic

Self-Hosted LLM Serving: Picking a Stack That Survives Real Traffic

September 16, 2026

Blog

What Is On-Premise AI? Deploying Intelligence Inside Your Own Infrastructure

What Is On-Premise AI? Deploying Intelligence Inside Your Own Infrastructure

September 15, 2026

Blog

How to Run an LLM Locally: A Practical Walkthrough for Developers

How to Run an LLM Locally: A Practical Walkthrough for Developers

September 15, 2026

Blog

How to Add Web Search to the Vercel AI SDK With the You.com API

How to Add Web Search to the Vercel AI SDK With the You.com API

September 14, 2026

Blog

How to Build RAG With Web Search: A Practical Pipeline Guide

September 11, 2026

Blog