Discover
POST /v2/discover enumerates a site's URLs without rendering a single
page. There is no browser, no screenshot, no PDF, no stored artifact —
just an HTTP-only crawl plus sitemap parsing that returns a flat list of
URLs and a count of where each one came from. It is the cheap "map the
site" primitive: point it at a domain, get back the pages worth
processing, then feed that list into the perceive endpoint
or a crawl-mode ingest job.
Here is the smallest useful call. Send a URL, get its map back:
curl -X POST https://api.enconvert.com/v2/discover \
-H "X-API-Key: sk_live_your_private_key" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com"
}'
The response is a plain URL list with provenance counters — no signed URLs, no operation ID, no polling:
{
"url": "https://example.com",
"mode": "hybrid",
"total": 47,
"urls": [
"https://example.com/",
"https://example.com/pricing",
"https://example.com/docs",
"https://example.com/blog/launch"
],
"pages_crawled": 12,
"truncated": false,
"robots_respected": false,
"sources": {"sitemap": 42, "crawl": 30},
"warnings": []
}
Worth flagging up front.
/v2/discoveris a V2 endpoint, gated by thediscover_enabledplan flag. Unlike most V2 endpoints it has no per-operation quota counter — it is cheap (no browser, no storage), so there is no monthly meter to exhaust. If your plan does not include V2 discover, the call returns402. See the pricing page for which plans carry it.
Endpoints
| Method | Path | Purpose |
|---|---|---|
POST |
/v2/discover |
Map a site's URLs over HTTP only and return a flat list with source counts. |
/v2/discover exposes a single path. It is stateless — there is no
operation row to re-fetch and no job to poll, so there is no companion
GET endpoint the way perceive has one.
Content-Type: application/json on the POST.
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/discover
is not on the key's list, the request is rejected with 403 and a
message naming the path that was blocked.
How discover works
One request runs entirely over HTTP. The singleton headless Chrome that
powers perceive is never touched — the crawl path
uses Crawl4AI's HTTP crawler strategy (an httpx GET plus an lxml
link parse per page), and the sitemap path reuses the same pure-HTTP
sitemap and feed helpers as the rest of the platform.
- Screen the seed. The URL you send is checked for SSRF before any
fetch: scheme, embedded credentials, blocked hostnames, and the
resolved IP are all validated. A URL that resolves to a private,
loopback, link-local, or cloud-metadata address is rejected with
400. The seed is always included as the first entry in its own map. - Gather from sitemaps. In
sitemaporhybridmode, discover readsrobots.txt, probessitemap.xml(recursing into<sitemapindex>children), and pulls RSS/Atom feed pages. A missing or broken sitemap becomes a warning, not an error. - Crawl over HTTP. In
crawlorhybridmode, discover runs a breadth-first crawl from the seed up tomax_depth, harvesting the<a href>links from each fetched page's raw HTML. Every followed link is SSRF-screened before it is fetched. - Normalise and filter. The combined raw list is canonicalised
(fragments and tracking params stripped, default ports removed, query
keys sorted), then passed through the same-domain check, your include
and exclude regex patterns, the
robots.txtfilter, de-duplication, and finally themax_urlscap.
Because there is no render, a client-rendered single-page app returns
only its HTML shell in crawl mode — typically the seed plus zero or one
URL. That is correct behaviour for a no-browser endpoint, not a failure.
For SPAs, use sitemap mode (most SEO-aware SPAs publish a sitemap) or
fall back to per-URL perceive calls.
Request parameters
Core
| Parameter | Type | Default | Description |
|---|---|---|---|
url |
string |
— | The site to map. Must start with http:// or https://. Max 2,048 characters. Required. |
mode |
string |
"hybrid" |
sitemap, crawl, or hybrid. See Modes. |
max_urls |
integer |
100 |
Maximum URLs returned. 1–1,000. The list is capped here and truncated flags if more existed. |
max_depth |
integer |
2 |
Crawl depth from the seed, in crawl/hybrid mode. 1–5. |
same_domain_only |
boolean |
true |
Keep only URLs on the seed's host. When false, off-host links discovered during the crawl are kept too. |
Filtering
| Parameter | Type | Default | Description |
|---|---|---|---|
include_patterns |
string[] |
[] |
Python regex allowlist (re.search semantics, not glob). A URL must match at least one to be kept. Empty means allow all. Max 50 patterns. |
exclude_patterns |
string[] |
[] |
Python regex denylist. A URL matching any pattern is dropped. Applied after include_patterns. Max 50 patterns. |
respect_robots |
boolean |
false |
When true, URLs disallowed by the site's robots.txt are dropped from the returned list. |
The patterns are real Python regular expressions, compiled at validation
time. A malformed pattern is a 422 at the edge, not a 500 mid-crawl.
Because the semantics are re.search, a bare substring like
"/blog/" matches anywhere in the URL — anchor with ^/$ if you need
a positional match.
Worth flagging up front.
respect_robotsis enforced at output-filter time, not fetch time. Crawl4AI 0.8.9 has no native robots gate, so incrawlorhybridmode a disallowed page may still be fetched over HTTP and then discarded before it reaches the response. Unlike perceive, whererespect_robots=truerejects a disallowed URL with403, discover never returns403for a robots rule — it silently drops the URL and reports nothing.
Modes
mode |
What it does |
|---|---|
sitemap |
robots.txt sitemap entries, probed sitemap.xml (with index recursion), and RSS/Atom feed pages. Instant, no crawl. |
crawl |
Breadth-first HTTP-only crawl from the seed, harvesting <a href> links from raw markup. |
hybrid (default) |
The de-duplicated union of sitemap and crawl. |
To be straight about it: crawl mode bounds the number of pages it
actually fetches to min(max_urls, 50). A max_urls of 1,000 does not
issue a thousand HTTP GETs — discover stops fetching at 50 pages, but
each page contributes many <a href> links, so the returned list can
still reach max_urls. The 50-page fetch ceiling keeps the synchronous
request well under the 300-second request timeout. pages_crawled in the
response tells you exactly how many GETs ran.
Response
POST /v2/discover returns this object directly — there is no async job
and no second call.
| Field | Type | Description |
|---|---|---|
url |
string |
The seed URL you sent. |
mode |
string |
The mode that ran: sitemap, crawl, or hybrid. |
total |
integer |
Number of URLs in urls (after dedup, filtering, and the cap). |
urls |
string[] |
The de-duplicated, normalised, capped URL list. The seed is always the first candidate. |
pages_crawled |
integer |
HTTP GETs issued by the crawl. 0 in pure sitemap mode. |
truncated |
boolean |
true when more unique URLs existed than max_urls allowed. |
robots_respected |
boolean |
Echoes the respect_robots value you sent. |
sources |
object |
Raw URL count per source before dedup/filter, e.g. {"sitemap": 42, "crawl": 30}. The counts overlap and sum above total. |
warnings |
string[] |
Non-fatal notes: a missing sitemap, a crawl that failed, an unreachable robots.txt. |
The sources counts are raw — they are the number of URLs each path
produced before normalisation, the same-domain check, your filters, and
de-duplication ran. They will routinely add up to more than total,
because hybrid mode finds the same pages from both the sitemap and the
crawl. Use them to see which path is carrying the map, not as a
post-filter tally.
Code examples
curl — default hybrid map
curl -X POST https://api.enconvert.com/v2/discover \
-H "X-API-Key: sk_live_your_private_key" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com"
}'
curl — sitemap only, blog pages, capped at 500
curl -X POST https://api.enconvert.com/v2/discover \
-H "X-API-Key: sk_live_your_private_key" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com",
"mode": "sitemap",
"max_urls": 500,
"include_patterns": ["/blog/"],
"exclude_patterns": ["/tag/", "/author/"],
"respect_robots": true
}'
Python
import requests
response = requests.post(
"https://api.enconvert.com/v2/discover",
headers={"X-API-Key": "sk_live_your_private_key"},
json={
"url": "https://example.com",
"mode": "hybrid",
"max_urls": 200,
"include_patterns": [r"/docs/"],
},
)
response.raise_for_status()
data = response.json()
print(f"found {data['total']} URLs from {data['sources']}")
for page_url in data["urls"]:
print(page_url)
Node.js
const res = await fetch("https://api.enconvert.com/v2/discover", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-API-Key": "sk_live_your_private_key"
},
body: JSON.stringify({
url: "https://example.com",
mode: "hybrid",
max_urls: 200,
include_patterns: ["/docs/"]
})
});
const data = await res.json();
console.log(`found ${data.total} URLs from`, data.sources);
data.urls.forEach((pageUrl) => console.log(pageUrl));
A common pattern is to discover first, then render: take data.urls and
hand them to the perceive endpoint — single calls for
a handful of pages, or its batch path for the whole list.
Plan gating
/v2/discover is gated by the discover_enabled plan flag and behaves
differently from the metered V2 endpoints. The metered ones —
perceive among them — each burn a monthly counter
(perceive_operations, lookup_queries, ingest_pages, watch_checks,
distill_operations). Discover has no such counter: it is HTTP-only, it
stores nothing, so there is no per-operation meter to deplete.
What that means in practice:
- If your plan carries
discover_enabled, you can call it. There is no monthly discover allowance to run out. - If your plan does not, the call returns
402with an upgrade prompt. - Admin plans bypass the gate entirely.
Which plans include discover_enabled is set per tier; current allowances
live on the pricing page. V1 conversion plans do not include
V2 endpoints — a discover call never consumes V1 conversion quota.
Error responses
| Status | Condition |
|---|---|
400 Bad Request |
URL is not http(s), carries embedded credentials, has no hostname, or resolves to a private, loopback, link-local, or cloud-metadata address (SSRF protection). |
401 Unauthorized |
Missing or invalid API key / JWT token. |
402 Payment Required |
Discover is not enabled on your current plan. |
403 Forbidden |
/v2/discover is not in the API key's allowed endpoints. |
422 Unprocessable Entity |
Request validation failed: bad mode enum, max_urls outside 1–1,000, max_depth outside 1–5, over 50 include/exclude patterns, a malformed regex, or a url over 2,048 characters. |
500 Internal Server Error |
URL discovery failed unexpectedly. The client gets a generic message; full detail goes to server logs only. |
Note that a failed sitemap fetch or a crawl fault does not produce an
error status — those degrade to entries in the warnings array, and the
request still returns 200 with whatever was found. The full status-code
reference is in the error-codes guide.
Limits
| Limit | Value |
|---|---|
| URL length | 2,048 characters |
max_urls |
1–1,000 (default 100) |
max_depth |
1–5 (default 2) |
include_patterns |
50 patterns max |
exclude_patterns |
50 patterns max |
| Pages fetched in crawl mode | 50 (min(max_urls, 50)) |
| Request timeout | 300 seconds |
| Per-operation quota | None — no monthly discover counter |