Web Search API for LLM Agents#

Private beta. Lookup is callable today with your normal API key, on every plan including Founding, and it draws on your monthly ops allowance like any other call. It is not announced or generally available: the request and response shapes can change without notice, and there is no stability or support commitment, so do not build anything load-bearing on it yet. The roadmap is on Coming Soon, and every release is announced in the changelog.

POST /v2/lookup is a web search API built for LLM agents: it runs a search in one of six categories and hands back a flat, provider-neutral result list. Set perceive_top and it also renders the top-N result URLs in a real browser, so an agent gets the search-engine results page (SERP) and the page content behind each hit in a single round trip. As a SERP API alternative it collapses the usual stack (hit a search API, parse its results, then fan out a scraper) into one call. It will be EnConvert's answer to Firecrawl /search.

Serper is the search provider behind it. The request and response speak a neutral search vocabulary (category, country, locale, time_filter), so a future provider swap does not change the contract you code against.

Here is the smallest useful call. Send a query, get the top web results back:

curl -X POST https://api.enconvert.com/v2/lookup \
  -H "X-API-Key: sk_your_private_key" \
  -H "Content-Type: application/json" \
  -d '{
    "query": "headless chrome pdf rendering"
  }'

The response is a flat result list plus provenance for support correlation:

{
    "lookup_id": 81423,
    "query": "headless chrome pdf rendering",
    "category": "web",
    "total": 10,
    "results": [
        {
            "title": "Generate PDFs with headless Chrome",
            "url": "https://example.com/guide/chrome-pdf",
            "snippet": "Render a page and print it to PDF...",
            "position": 1
        },
        {
            "title": "Print to PDF with the Chrome DevTools Protocol",
            "url": "https://example.dev/cdp/print-to-pdf",
            "snippet": "Page.printToPDF returns base64 PDF data...",
            "position": 2
        }
    ],
    "perceive_top": 0,
    "perceive_operation_ids": [],
    "credits": 1,
    "cost_cents": 0.06,
    "warnings": []
}

Endpoints#

Method Path Purpose
POST /v2/lookup Run one search and, optionally, auto-perceive the top-N result URLs.

/v2/lookup is a single-call endpoint with no separate status or retrieval path. When you auto-perceive, each rendered page becomes a first-class perceive operation with its own operation_id, which you can re-fetch later through GET /v2/perceive/{operation_id}.

Content-Type: application/json.


Authentication#

Authenticate with a private key in the X-API-Key header for server-to-server calls. This is the path the examples below use.

X-API-Key: sk_your_private_key

Public keys with a JWT bearer token also work, using the same flow as every other endpoint: generate a token with your pk_ key, then send it as Authorization: Bearer <token>. The full flow, including domain locking and token refresh, is in the authentication guide.

Each API key carries an allowed-endpoints allowlist. If /v2/lookup is not on the key's list, the request is rejected with 403.


How lookup works#

One request runs one provider search and, only if you ask for it, renders the top results afterward.

  1. Quota gate. Before anything bills, the handler checks your plan's unified monthly ops allowance. A disabled plan or an exhausted allowance is rejected with 402, so nothing is charged on a rejection.
  2. Search. The query goes to the search provider (Serper) on the endpoint for your category. Recency, country, locale, location, page size, and autocorrect map onto the provider's parameters.
  3. Normalise. Each provider hit is flattened into a neutral LookupResult carrying title, url, snippet, and position. The category-specific extras land in extra so the contract never grows a column per provider quirk.
  4. Charge and audit. The search succeeded, so one op is billed and one ch_lookup_queries audit row is written. The row id comes back as lookup_id for support correlation.
  5. Auto-perceive (optional). If perceive_top > 0, the top-N result URLs are rendered one at a time through the shared headless Chrome singleton, the same pipeline as the perceive endpoint. Each render is a full /v2/perceive operation: its own op billed against the shared allowance, its own operation row, its own operation_id. By default auto-perceive requests Markdown only, with no screenshot, PDF, or LLM extraction; send an enrich object to widen the outputs, run them in parallel, or add schema extraction and a synthesized answer.

Auto-perceive is best-effort. A single URL that fails, or the ops allowance running out part-way through, degrades to a warning and still returns the search results. The SERP is the primary product here, so a perception problem never sinks the whole call.


Request parameters#

Query and category#

Parameter Type Default Description
query string -- The search query. 1–512 characters, trimmed of leading/trailing whitespace. A query that is empty after trimming is rejected with 422. Required.
category string "web" One of web, news, images, scholar, patents, maps.

Targeting and recency#

Parameter Type Default Description
country string null Google gl country code, e.g. us, in. Max 8 characters.
locale string null Google hl interface language, e.g. en. Max 16 characters.
time_filter string null Restrict to results from the past period: hour, day, week, month, or year.
location string null Free-text location string, e.g. "Austin, Texas". Max 128 characters.
autocorrect boolean true Whether the provider may autocorrect the query spelling.

Pagination#

Parameter Type Default Description
num_results integer 10 Results per page. 1–100.
page integer 1 Page number. 1–10.

Auto-perceive#

Parameter Type Default Description
perceive_top integer 0 Auto-perceive the first N result URLs that have a navigable link. 0–10. Each one is a full browser render that bills one op from your monthly allowance, which is why it is capped at 10. For larger sets, take the url fields and call the perceive batch endpoint. 0 disables auto-perceive.

Enrichment (enrich)#

An optional enrich object tunes how the top-N results (perceive_top) are read, and can synthesize one grounded answer across them. When enrich is omitted, perceive_top keeps its default behaviour (Markdown-only, one result at a time).

Parameter Type Default Description
enrich.outputs string[] ["markdown"] Which perceive outputs to produce per enriched result, for example markdown, html_cleaned, links, screenshot, structured. See perceive outputs.
enrich.concurrency integer 3 How many result URLs to enrich in parallel. 1–5. Markdown/HTML renders parallelize; screenshot/PDF renders serialize on the shared browser.
enrich.schema object null Run schema-driven structured extraction against each enriched result. The extracted data appears under each result's perceive.structured. JSON-Schema object or a flat {field: description} map.
enrich.synthesize_answer boolean false Synthesize one cited, grounded answer to the query across the enriched results, returned as answer (with answer_sources). Uses the perceived page content when available, otherwise the result snippets.
enrich.answer_prompt string null A question to answer instead of the raw query. Only used when synthesize_answer is true. Max 1,000 characters.

enrich.schema and enrich.synthesize_answer use the LLM extraction tier. If that extraction step cannot run, they degrade to a warning and the rest of the response is unaffected.

{
  "query": "best open-source vector databases",
  "perceive_top": 3,
  "enrich": {
    "outputs": ["markdown"],
    "concurrency": 3,
    "synthesize_answer": true
  }
}

Categories#

Each category hits a different provider endpoint and surfaces a slightly different result shape. The universal fields (title, url, snippet, position) are always typed; category-specific fields land in extra.

category What it searches Notable fields populated
web General web results title, url, snippet, date, position
news News articles adds source, image_url
images Image results image_url, thumbnail_url, source (often no snippet)
scholar Academic results same shape as web; citation counts in extra
patents Patent results same shape as web; patent fields in extra
maps Local places url is the place website; snippet carries the address; rating, coordinates in extra

Worth flagging: for images and maps, url can be null for a given hit when the provider returns no navigable link. Auto-perceive skips any result whose url is null, so a perceive_top of 5 on a SERP with two URL-less hits perceives at most three pages.


Response#

The endpoint omits null fields, so a minimal web result carries only the fields that are actually populated.

Field Type Description
lookup_id integer The ch_lookup_queries audit-row id. Quote it to support. null if the audit write failed, though the results are still valid.
query string The (trimmed) query you sent.
category string The category searched.
country string Echo of the country you sent, if any.
locale string Echo of the locale you sent, if any.
time_filter string Echo of the time_filter you sent, if any.
total integer Number of results returned.
results LookupResult[] The result list. See below.
perceive_top integer How many results were actually perceived: at most the value you requested, and lower if the ops allowance ran out or URLs failed.
perceive_operation_ids string[] The per_... operation ids of the perceived results, in order.
answer_box object The provider's answer box, when present.
knowledge_graph object The provider's knowledge graph panel, when present.
answer string The synthesized cited answer across the enriched results. Present only when enrich.synthesize_answer is true and it succeeded.
answer_sources string[] The URLs used as grounding for answer, in citation order.
credits integer Provider credits consumed by this query.
cost_cents number Monetary cost of the search in cents. A flat 0.06 per query today.
warnings string[] Non-fatal notes: a skipped URL-less result, an auto-perceive failure, the ops allowance running out mid-loop.

LookupResult#

Field Type Description
title string Result title.
url string Canonical page link, the thing you would perceive. null for results with no navigable URL.
snippet string Result snippet. For maps, this carries the address.
position integer The result's position on the SERP.
source string Source/publisher, for news and images.
date string Published date, when the provider reports one.
image_url string Image URL, for images and news.
thumbnail_url string Thumbnail URL, for images.
extra object Category-specific fields not in the neutral set: ratings, coordinates, citation counts, and so on.
perceive PerceiveResponse The full inline perceive result for this URL, present only for the top-N when perceive_top > 0 and the render succeeded. Same object shape as the perceive endpoint.

Reading auto-perceive results#

When you send perceive_top, walk the results and check for the perceive field. It is present only on the results that were perceived, and only when their render succeeded. The Markdown for each lives behind a pre-signed download URL (a short-lived, signed link to object storage) under perceive.outputs.markdown.url, the same as a direct perceive call.

{
    "lookup_id": 81910,
    "query": "react server components data fetching",
    "category": "web",
    "total": 10,
    "results": [
        {
            "title": "Data fetching with RSC",
            "url": "https://example.com/rsc/data",
            "snippet": "Fetch on the server, stream to the client...",
            "position": 1,
            "perceive": {
                "operation_id": "per_3f9a2c1b8e7d4a6f90b1c2d3e4f5a6b7",
                "status": "completed",
                "url": "https://example.com/rsc/data",
                "outputs": {
                    "markdown": {
                        "url": "https://spaces.example.com/...signed...",
                        "size_bytes": 7421,
                        "content_type": "text/markdown; charset=utf-8",
                        "expires_in": 900
                    }
                },
                "cost_cents": 0.0,
                "duration_ms": 5840
            }
        }
    ],
    "perceive_top": 1,
    "perceive_operation_ids": [
        "per_3f9a2c1b8e7d4a6f90b1c2d3e4f5a6b7"
    ],
    "credits": 1,
    "cost_cents": 0.06,
    "warnings": []
}

Those signed URLs expire after 15 minutes. To download a perceived page later, re-fetch its operation with GET /v2/perceive/{operation_id} using the id from perceive_operation_ids. That re-signs the URLs and does not re-render, so it costs no ops. See the perceive retrieve section for the details.


Code examples#

curl -X POST https://api.enconvert.com/v2/lookup \
  -H "X-API-Key: sk_your_private_key" \
  -H "Content-Type: application/json" \
  -d '{
    "query": "open source vector database",
    "num_results": 20
  }'

curl: recent news, localised#

curl -X POST https://api.enconvert.com/v2/lookup \
  -H "X-API-Key: sk_your_private_key" \
  -H "Content-Type: application/json" \
  -d '{
    "query": "rbi monetary policy",
    "category": "news",
    "country": "in",
    "locale": "en",
    "time_filter": "week"
  }'

curl: search plus auto-perceive the top 3#

curl -X POST https://api.enconvert.com/v2/lookup \
  -H "X-API-Key: sk_your_private_key" \
  -H "Content-Type: application/json" \
  -d '{
    "query": "langchain retrieval augmented generation",
    "perceive_top": 3
  }'

Python#

import requests

response = requests.post(
    "https://api.enconvert.com/v2/lookup",
    headers={"X-API-Key": "sk_your_private_key"},
    json={
        "query": "langchain retrieval augmented generation",
        "perceive_top": 3,
    },
)
response.raise_for_status()
data = response.json()

# Pull the Markdown of every result that was perceived
for result in data["results"]:
    perceived = result.get("perceive")
    if not perceived:
        continue
    markdown_url = perceived["outputs"]["markdown"]["url"]
    page_text = requests.get(markdown_url).text
    print(result["url"], len(page_text), "chars")

for note in data["warnings"]:
    print("warning:", note)

Node.js#

const res = await fetch("https://api.enconvert.com/v2/lookup", {
    method: "POST",
    headers: {
        "Content-Type": "application/json",
        "X-API-Key": "sk_your_private_key"
    },
    body: JSON.stringify({
        query: "langchain retrieval augmented generation",
        perceive_top: 3
    })
});

const data = await res.json();

// Pull the Markdown of every result that was perceived
for (const result of data.results) {
    if (!result.perceive) continue;
    const markdownUrl = result.perceive.outputs.markdown.url;
    const pageText = await fetch(markdownUrl).then(r => r.text());
    console.log(result.url, pageText.length, "chars");
}

for (const note of data.warnings) {
    console.log("warning:", note);
}

If you call EnConvert from Claude, Cursor, or another Model Context Protocol (MCP) client, the search capability will be exposed as a tool there too. See the MCP server page.


Error responses#

The handler never echoes raw provider text to the client. Provider and SSRF detail stays in the server logs, and the client gets a clean, generic message.

Status Condition
401 Unauthorized Missing or invalid API key / JWT token.
402 Payment Required Lookup is not on your current plan, or your monthly ops allowance is exhausted.
403 Forbidden /v2/lookup is not in the API key's allowed endpoints.
422 Unprocessable Entity Request validation failed: empty/over-length query, an unknown category or time_filter, num_results or page out of range, perceive_top over 10.
502 Bad Gateway The search provider returned an error response or a non-retryable transport fault (SearchUpstreamError). Retrying may help.
503 Service Unavailable The search provider is misconfigured server-side (a missing key on our side, SearchConfigError), or it is temporarily unavailable: the circuit breaker is open, or the provider rate-limited us (SearchUnavailableError). Retry later.
500 Internal Server Error An unexpected failure. The message is generic; quote the time of the call to support.

A failing auto-perceive never raises an error of its own. It lands in warnings and the call still answers 200. The full status-code reference is in the error-codes guide.


Limits#

Limit Value
query length 1–512 characters (trimmed)
country length 8 characters
locale length 16 characters
location length 128 characters
num_results 1–100
page 1–10
perceive_top 0–10
Ops per call 1 for the query, plus 1 per auto-perceived result
Auto-perceive outputs Markdown by default; widened with enrich.outputs
Auto-perceive concurrency Sequential by default; 1–5 with enrich.concurrency
Cost per search 0.06 cents flat
Perceived-page signed URL expiry 15 minutes

Frequently asked questions#

How do I run a web search and get page content back in one REST API call?#

Send POST /v2/lookup with a query and set perceive_top (0–10). The top-N result URLs are rendered in a real browser, and each perceived result carries an inline perceive object whose Markdown sits behind a pre-signed URL at perceive.outputs.markdown.url.

Is /v2/lookup a SERP API alternative I can adopt without provider lock-in?#

Yes. Serper is the search provider behind it, but the request and response speak a neutral search vocabulary (category, country, locale, time_filter), so a future provider swap does not change the contract you code against.

What search categories does the lookup API support?#

Six: web (the default), news, images, scholar, patents, and maps. The universal fields (title, url, snippet, position) are always typed, and category-specific extras land in extra.

Why did lookup perceive fewer pages than my perceive_top value?#

Auto-perceive skips results whose url is null, stops if the monthly ops allowance runs out mid-loop, and degrades a failed render to a warning. The response's perceive_top reports how many pages were actually perceived, and warnings explains the gaps.