Skip to Content

map a site

discover all urls on a website. map combines sitemap.xml discovery with in-page link discovery to build a comprehensive link index — feed it a domain and get back every url it can find.

endpoint

POST /api/map

requires a Bearer token — see authentication.

basic example

curl -X POST https://api.crawlbrulee.com/api/map \ -H "Authorization: Bearer $CRAWLBRULEE_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://example.com" }'

response:

{ "links": [ { "url": "https://example.com/about" }, { "url": "https://example.com/blog" }, { "url": "https://example.com/pricing" } ], "response_meta": { "usage": { "total_credit_cost": 1, "engine_credit_cost": 1, "proxy_multiplier": 1, "zero_data_retention_credit_cost": 0, "engine": "http", "proxy": "basic" }, "pagination": { "page": 1, "limit": 5000, "total": 3, "total_pages": 1, "has_more": false }, "truncation": { "storage_capped": false, "response_capped": false, "total_before_max_urls": 3, "total_detected_before_storage_cap": 3, "discovery_capped": false, "sitemaps_skipped": 0, "discovery_cap_reason": null } } }

request parameters

all fields except url are optional.

fieldtyperequireddefaultdescription
urlstringyes—the website url to map
proxystringno"auto"proxy tier: "basic", "advanced", or "auto". map uses the 1-credit http base; auto bills 1 or 5 based on the delivered tier
sitemap_onlybooleannofalseonly use sitemap.xml discovery; skip in-page link discovery
types.internalbooleannotrueinclude internal links (same domain; www and the bare domain are equivalent)
types.internal_subdomainsbooleannotrueinclude links on subdomains (other than www) of the target domain
types.externalbooleannotrueinclude external links (different domain)
cache.max_ageinteger | datetimeno604800 (7 days)max cache age in seconds, or an iso 8601 timestamp
max_urlsintegerno5000how many urls to discover, store, and return (cap: 100,000). discovery stops here, so a smaller value is a faster, lighter crawl — not just a shorter answer
pageintegerno1page number for paginated results
limitintegerno5000results per page (cap: 10,000)
location.countrystringno—proxy egress country: an iso 3166-1 alpha-2 code (e.g. US), or eu / europe — see proxies & location
zero_data_retentionbooleannofalsekeep the result out of the shared cache — see zero data retention. must be enabled for your organization

url normalization

this is about the url you send. the urls you get back follow a different rule — see link url form.

map always targets the site root derived from url: the query string and hash are removed, the path is dropped, www. is collapsed, and subdomains are preserved. passing https://example.com/blog/post maps example.com, not the specific blog post. the cache is keyed on this normalized root, so requests for any path on the same site share the same cached result.

response

an array of link objects. each object has a single url field. the site root itself is not listed — you already have it, it’s the url you asked for.

{ "links": [{ "url": "https://example.com/about" }, { "url": "https://example.com/pricing" }] }

ordering

links are sorted so the most useful ones land on page 1. four rules, applied in order:

  1. link type — internal first, then internal_subdomains, then external.
  2. where the link was found — links on the site’s home page come before links found only in sitemaps.
  3. path depth — shallower paths first, so /news beats /2019/07/10/a-story.
  4. alphabetical — codepoint order, as a tie-break.

on a big site this keeps section pages near the front instead of burying them behind thousands of dated archive articles. the order is deterministic, so pagination stays stable — page 2 of the same request always holds the same links.

every returned url is written the same way /scrape writes its returned url: in normalized form, with known tracking parameters stripped.

{ "links": [ { "url": "https://www.example.com/de-en/premium" }, { "url": "https://www.example.com/search?page=2&sort=new" } ] }

map output and scrape output now agree on the same page. feed a map link straight into /api/scrape and you stay on one host and one cache key — no rewriting, no accidental second fetch of the same page under a different name.

this changed: /api/map used to fold www. down to the bare domain and throw the query string away. if you match map output against stored urls, re-check that matching.

response_meta.usage

billing and cache info for this request.

fieldtypedescription
total_credit_costintegercredits charged for this request: always engine_credit_cost × proxy_multiplier + zero_data_retention_credit_cost
engine_credit_costintegerthe engine base charged: 1 for a fresh map, 0 for a cache hit or a map we don’t bill
proxy_multiplierinteger1 for basic, 5 for advanced. always shown, even when the engine cost is 0
zero_data_retention_credit_costinteger1 when the request used zero data retention and the map was billed and fresh, otherwise 0
enginestringhttp for a fresh map or cache for a cached result
proxystringproxy tier that resolved the request: basic or advanced, never auto

map has no screenshots, so there is no screenshot_slicing_credit_cost here.

see credits and caching for the full cost model.

response_meta.pagination

tells you where you are in the result set.

fieldtypedescription
pageintegercurrent page number
limitintegermaximum results per page
totalintegertotal number of urls in the result set
total_pagesintegertotal number of pages
has_morebooleanwhether more pages are available

response_meta.truncation

tells you whether the full url set was truncated at any stage.

fieldtypedescription
discovery_cappedbooleantrue if sitemap discovery stopped before reading every sitemap file it found — the site has more pages than this map lists
discovery_cap_reasonstring | nullwhich limit stopped discovery first, or null if nothing did — see the table below
sitemaps_skippedintegerhow many sitemap files were skipped or only partly read (file too big, could not be fetched, or a discovery limit was reached)
response_cappedbooleantrue if more links were eligible than max_urls, so the list was trimmed
total_before_max_urlsintegerhow many links were eligible before the max_urls trim
storage_cappedbooleantrue if the stored map hit the hard 100,000-url storage cap
total_detected_before_storage_capintegertotal urls found during discovery before that storage cap

discovery_cap_reason values

valuewhat it meanscan you do anything?
nullnothing stopped discovery — the map is complete—
"max_urls"your own max_urls was reachedyes — ask again with a higher max_urls
"time"discovery ran out of its time budgetno
"file_budget"the site has more sitemap files than one request readsno
"depth"the site’s sitemap indexes nest too deeplyno
"file_size"a sitemap file was too large to readno

the field to watch is discovery_cap_reason. because discovery now stops at max_urls, a map that hit your cap comes back with exactly max_urls links and response_capped: false — nothing in the counts tells you the site has more. discovery_cap_reason: "max_urls" is what tells you, and raising max_urls is the fix. every other reason means we stopped for our own limits; more links are not available on this request.

has_more: true in pagination means more results exist on further pages — paginate to get them. storage_capped: true is the hard ceiling: the map exceeded 100,000 urls, and the remainder was never stored and isn’t retrievable.

by default all link types are returned. use the types object to narrow results.

internal links only (exclude subdomains and external):

curl -X POST https://api.crawlbrulee.com/api/map \ -H "Authorization: Bearer $CRAWLBRULEE_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://example.com", "types": { "internal": true, "internal_subdomains": false, "external": false } }'

including subdomains but excluding external:

{ "url": "https://example.com", "types": { "internal": true, "internal_subdomains": true, "external": false } }

this would return urls from example.com and blog.example.com but not twitter.com.

sitemap-only mode

set sitemap_only: true to skip in-page link discovery and rely solely on sitemap.xml discovery.

curl -X POST https://api.crawlbrulee.com/api/map \ -H "Authorization: Bearer $CRAWLBRULEE_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://example.com", "sitemap_only": true }'

when to use sitemap-only mode:

  • the site has a well-maintained sitemap and you want deterministic results.
  • you don’t want in-page-discovered links mixed in — sitemap_only doesn’t change latency; it only controls whether those links get merged into the sitemap results.
  • you’d rather not risk mixing in links from an earlier discovery pass — sitemap_only guarantees a clean, sitemap-only result set.

pagination

results are paginated when the total exceeds limit. use page and limit to iterate through the full set.

const API_KEY = process.env.CRAWLBRULEE_API_KEY async function mapAllUrls(siteUrl) { let page = 1 const limit = 10000 const allLinks = [] while (true) { const res = await fetch('https://api.crawlbrulee.com/api/map', { method: 'POST', headers: { Authorization: `Bearer ${API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ url: siteUrl, page, limit, }), }) if (!res.ok) { throw new Error(`Map request failed: ${res.status}`) } const data = await res.json() allLinks.push(...data.links) if (!data.response_meta.pagination.has_more) break page++ } console.log(`Found ${allLinks.length} URLs`) return allLinks } const links = await mapAllUrls('https://example.com')

credits and caching

credit cost

a fresh map request uses the 1-credit http engine base, multiplied by the delivered proxy tier:

proxy tiercost
basic1 credit
advanced5 credits
auto1–5 credits — billed at whichever tier actually delivered the result

auto is billed at the tier that delivered. zero data retention adds 1 credit to a billed, fresh map. see proxies & location for how the tiers behave and credits & pricing for the full cost model.

every response includes a response_meta.usage object with the exact cost, engine, and resolved proxy tier — see the response_meta.usage section above, or checking your usage for the platform-wide reference.

how caching works

a successful map result is cached server-side. a subsequent request for the same url returns the cached result if it’s younger than max_age — at 0 credits. if no fresh result exists, a fresh map runs and its result is cached for future requests.

one exception: a successful result with an empty sitemap (no sitemap found) is not cached — each such request runs as a fresh attempt. a map from a request with zero data retention is not added to the cache either.

what a map costs when the site has problems

map has no page_status_code: one map run reads several things from the site (robots.txt, sitemap files, the homepage), so one status would be misleading. instead:

  • we found urls → 200 with the links, billed as usual — even if the homepage was a 404.
  • we found no urls, and the site gave a real answer (for example, no sitemap and a 404 for sitemap.xml) → 200 with empty links, billed as usual. “nothing here” is an answer.
  • we found no urls, and the site answered only with errors (5xx, 403 and the like) → 200 with empty links, not billed. every *_credit_cost field in response_meta.usage is 0.
  • filters don’t change the bill. billing looks at what we found before your types filter, so a filter that removes every url still leaves a billed, empty links.
  • the site didn’t answer at all, or we could not connect to it → 502 target_unreachable, not billed. the site may be back later, so a later retry can help.

the map cache works the same way as the scrape cache: cache.max_age is the only knob. there’s no require_js — that one is scrape-only.

default ttl

map results are cached for 7 days (604,800 seconds) by default — longer than the scrape default, since a site’s url inventory changes slowly. this matches the /api/map row in the cache ttl table.

controlling cache behavior

pass a cache object to set max_age, which controls what counts as “fresh” for your request:

{ "url": "https://example.com", "cache": { "max_age": 86400 } }

max_age accepts:

  • an integer (seconds): 86400 = accept results up to 1 day old.
  • an iso 8601 datetime string: "2025-01-15T00:00:00Z" = accept results cached after this timestamp.
  • 0: bypass the cache entirely and always run a fresh map.

so to use cached results (the default), omit cache or set max_age to 604800; to force-refresh, set max_age to 0.

cache hits are free

a cache hit costs 0 credits. if your mapping needs are periodic, lean on the default 7-day cache and save your budget for fresh discoveries.

force-refresh

setting max_age to 0 skips the cache and always runs a fresh map, so you always pay the full credit cost. use it only when you genuinely need the latest url set — for example, right after a site relaunch.

{ "url": "https://example.com", "cache": { "max_age": 0 } }

a map request is otherwise always cache-eligible — unlike scrape, there are no request options that disable map caching. only max_age: 0 forces a fresh run.

error responses

statusnamewhen
400invalid_url, url_too_long, unsupported_url_schema, url_credentials_not_supportedbad url, url exceeds the size limit, unsupported url scheme, or url has embedded credentials
401invalid_credentialsmissing or invalid api token
403antibot_blockedthe target site’s anti-bot protection blocked the map — same as scrape. a higher proxy tier (advanced) is the usual fix
403zero_data_retention_not_enabledyou sent zero_data_retention: true, but it is not enabled for your organization — not billed. see zero data retention
408request_timeoutmap request exceeded the server-side timeout
422too_many_redirectsthe target’s sitemap or robots.txt redirected in a loop, or through more hops than we follow — same as scrape. not a bad request; retrying rarely helps
429too_many_requests, usage_allocation_errorrate limit or credit/concurrency limit hit
499client_closed_requestclient disconnected before completion
500internal_server_errorsomething went wrong on our end
502target_unreachablewe could not reach the site at all — same as scrape. not billed (see what a map costs when the site has problems)
503service_unavailabletransient failure on our end — retry the same request; not an auth problem

all errors follow the standard { name, message, details? } format. see errors for the full reference.