Lookup
POST /v2/lookup runs a web 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. It is EnConvert's answer to Firecrawl /search:
one call where you would otherwise hit a search API, parse its results,
then fan out a scraper.
Serper backs it today. 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_live_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 — 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": []
}
Worth flagging up front.
/v2/lookupis a V2 endpoint with its own quota counter,lookup_queries, separate from V1 conversions and from the perceive counter. V1 conversion plans do not include lookup queries — you need a V2-inclusive plan to call it. See the pricing page for the per-tier allowances, and the V2 overview for how the V2 counters relate.
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 — there is 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_live_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.
- Quota gate. Before anything bills, the handler checks the
lookup_queriescounter for your plan. A disabled plan or an exhausted monthly quota is rejected with402, so nothing is charged on a rejection. - Search. The query goes to the search provider (Serper today) on the
endpoint for your
category. Recency, country, locale, location, page size, and autocorrect map onto the provider's parameters. - Normalise. Each provider hit is flattened into a neutral
LookupResult—title,url,snippet,position, and the category-specific extras land inextraso the contract never grows a column per provider quirk. - Charge and audit. The search succeeded, so one
lookup_queriesis consumed and onech_lookup_queriesaudit row is written. The row id comes back aslookup_idfor support correlation. - 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/perceiveoperation: its own quota decrement, its own operation row, its ownoperation_id. Auto-perceive requests Markdown only — no screenshot, PDF, or LLM extraction.
Auto-perceive is best-effort. A single URL that fails, or the perceive quota 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 runs sequentially through the shared singleton and consumes one perceive_operations from your quota — 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. |
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 — 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, lower if quota 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. |
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 perceive quota 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 quota. See
the perceive retrieve section
for the details.
Code examples
curl — web search
curl -X POST https://api.enconvert.com/v2/lookup \
-H "X-API-Key: sk_live_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_live_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_live_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_live_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_live_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 is exposed as a tool — see the MCP server page.
Plan gating
Lookup queries are metered on their own counter, lookup_queries,
separate from V1 conversions and from the perceive counter that
the perceive endpoint uses. V1 conversion
plans include no lookup quota; you need a V2-inclusive plan.
Two things bill independently on a lookup call:
| What you do | Counter consumed |
|---|---|
| Run a search | one lookup_queries |
| Auto-perceive N result URLs | one perceive_operations per URL actually rendered |
So a call with perceive_top: 3 consumes one lookup_queries and up to
three perceive_operations. If your perceive quota runs out mid-loop, the
search still returns — auto-perceive simply stops at the boundary and
notes it in warnings, rather than failing the whole request.
Per-tier allowances for both counters live on
the pricing page; they are configured per plan, not hardcoded
in the endpoint. (At the time of writing, the Free plan default carries 25
lookup_queries and 50 perceive_operations per month.)
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 lookup_queries quota 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 — our fault, 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 |
| Auto-perceive outputs | Markdown only |
| Auto-perceive concurrency | Sequential (one render at a time) |
| Cost per search | 0.06 cents flat |
| Perceived-page signed URL expiry | 15 minutes |
Monthly lookup_queries |
Plan-dependent (see pricing) |