Sitemap
Discover the URLs on a domain — start a job, read its status, download the results file, get a webhook, and what it costs.
For AI agents: https://bytekit.com/llms.txt — every docs page is available as markdown by appending .md
POST /v1/sitemap finds the URLs of a website, and GET /v1/sitemap/{id} reports on the job.
A valid request answers 202 with a job id. ByteKit discovers the URLs in the background and saves them as a results file that you download from results_url.
Start a job
curl -i -X POST https://api.bytekit.com/v1/sitemap \
-H "Authorization: Bearer sk_live_YOUR_KEY_HERE" \
-H "Content-Type: application/json" \
-d '{"url": "https://bytekit.com", "strategy": "quick", "max_urls": 20}'{
"id": "sm_c013b3fa71f94ff292f5c2fa74f8d77b",
"url": "https://bytekit.com",
"strategy": "quick",
"max_depth": 2,
"max_urls": 20,
"status": "pending",
"total_urls": null,
"sources": null,
"total_bytes": null,
"results_url": null,
"warnings": null,
"error_code": null,
"error_message": null,
"compact": false,
"process": null,
"metadata": null,
"created_at": "2026-10-10T14:59:44.602Z",
"completed_at": null,
"expires_at": null,
"cache": "miss",
"cache_age_s": null
}This request was run for real and returned
202.cacheismissbecause no earlier job could answer it; see Cache.
Only url is required. Keep the id: it is how you read the job.
| Field | Default | What it does |
|---|---|---|
url | (required) | The site to discover. Only the scheme and host are used: a path, query string or fragment is accepted but ignored, and the 202 response carries a warnings entry saying so |
strategy | standard | quick, standard or deep: where ByteKit looks for URLs. See Strategies |
max_depth | 2 | From 1 to 5. How many levels of page links a deep job follows |
max_urls | 5000 | From 1 to 100000. A new job stops collecting at this many URLs |
process | null | An object with an action and options. ByteKit accepts it and returns it on the job, and it has no other effect |
compact | false | true makes the results file a plain list of URLs, one per line, with no header |
metadata | none | An object of your own, up to 4096 bytes when serialized. It is returned on the job and in webhook deliveries. A key that starts with _ is rejected with 422 validation_error |
webhook_url | none | An https:// URL to notify about the job. An http:// URL is rejected with 400 invalid_url and the reason scheme_unsupported. See Webhooks |
webhook_secret | null | Signs the webhook deliveries. It is ignored when there is no webhook_url |
cache_ttl | 30d | How old a cached job may be and still answer the request. It is a string: 0, hours such as 48h, or days such as 30d, up to 90 days. A number is rejected with 422 validation_error |
The 202 response reports url after ByteKit has normalised it: the scheme and host are lowercased, an international host is in punycode, non-ASCII characters are percent-encoded, and surrounding spaces are trimmed. If the result is only a scheme and host, with or without a trailing /, it is returned as scheme://host. If it carries a path, query string or fragment, it is returned whole, together with the warnings entry. GET returns the host as scheme://host.
Strategies
| Strategy | Where it looks |
|---|---|
quick | The sitemap files the site publishes. Every URL has the source sitemap |
standard | The same, plus URLs from your account's completed scrapes of the host in the last 90 days. These have the source known |
deep | The same as standard, plus URLs found by following links on pages, up to max_depth levels. These have the source links |
The sources run in that order, and each one fills only the room that max_urls leaves. A URL found by more than one source is counted once, under the first.
A deep job follows links only from URLs the earlier sources found, so on a site with no sitemap files and no known URLs it finds no links.
The job's sources field counts the URLs per source. For a deep job run for real it read {"known": 1, "links": 4, "sitemap": 119}.
Check the job
GET /v1/sitemap/{id} answers 200 with the job whatever its status: pending, processing, completed or failed. Poll it every few seconds until the status is completed or failed. The jobs on this page finished within a few seconds.
curl -i https://api.bytekit.com/v1/sitemap/sm_c013b3fa71f94ff292f5c2fa74f8d77b \
-H "Authorization: Bearer sk_live_YOUR_KEY_HERE"{
"id": "sm_c013b3fa71f94ff292f5c2fa74f8d77b",
"url": "https://bytekit.com",
"strategy": "quick",
"max_depth": 2,
"max_urls": 20,
"status": "completed",
"total_urls": 20,
"sources": { "sitemap": 20 },
"total_bytes": 196,
"results_url": "https://…",
"warnings": [],
"error_code": null,
"error_message": null,
"compact": false,
"process": null,
"metadata": null,
"created_at": "2026-10-10T14:59:44.602Z",
"completed_at": "2026-10-10T14:59:47.405Z",
"expires_at": "2027-01-08T14:59:47.405Z",
"billing": {
"factors": [{ "name": "endpoint", "value": 1, "reason": "sitemap" }],
"raw_bytes": 196,
"multiplier": 1,
"billed_bytes": 196
}
}This request was run for real and returned
200. The signedresults_urlis shortened.
| Field | Meaning |
|---|---|
status | pending, processing, completed or failed |
total_urls | How many URLs the results hold. null until the job completes |
sources | The URLs per source: sitemap, known or links |
total_bytes | The size in bytes of the gzip-compressed results file. This is what the job is billed on; see Cost |
results_url | A signed link to the results file, present once the job is completed. Each GET of a completed job returns a new link, valid for 7 days |
warnings | Notes about the run, such as a source that could not be read |
error_code, error_message | Why a job failed. null otherwise |
billing | What the job was billed: raw_bytes, the factors applied, the multiplier and billed_bytes |
expires_at | When the job expires: 90 days after it completes, except that a cache hit keeps the expiry of the job that answered it. null until the job completes |
A completed 200 also carries the headers X-Raw-Bytes, X-Billing-Multiplier and X-Billable-Bytes, which show the same figures as billing.
When a job fails
curl -i -X POST https://api.bytekit.com/v1/sitemap \
-H "Authorization: Bearer sk_live_YOUR_KEY_HERE" \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com", "strategy": "quick", "max_urls": 20}'This request was run for real and returned
202, with the same fields as above andcachemiss. The job then failed, and polling it returned200:
{
"id": "sm_a380b0ea64dd4b19a854699c58933499",
"url": "https://example.com",
"strategy": "quick",
"max_depth": 2,
"max_urls": 20,
"status": "failed",
"total_urls": null,
"sources": null,
"total_bytes": null,
"results_url": null,
"warnings": ["robots.txt unavailable (non-success fetch)"],
"error_code": "no_urls_found",
"error_message": "No URLs discovered for this domain",
"compact": false,
"process": null,
"metadata": null,
"created_at": "2026-10-10T14:59:54.408Z",
"completed_at": "2026-10-10T14:59:55.941Z",
"expires_at": null
}A robots.txt or sitemap file that ByteKit cannot read does not fail a job by itself. It adds an entry to warnings, and discovery goes on with the other sources. A sitemap file that is not there (404 or 410) or is empty adds no warning, and neither does a page that cannot be fetched while a deep job follows links.
These are the error_code values you may meet:
error_code | When |
|---|---|
no_urls_found | Discovery ended without finding a URL |
blocked | The site blocks ByteKit's requests, so discovery did not start |
job_timeout | The job did not finish in time |
internal_error | A failure on ByteKit's side |
worker_crash | The job stopped making progress, and ByteKit failed it after a long wait |
A job that fails with worker_crash sends no sitemap.failed webhook, so poll the job as well as listening for the webhook.
Download the results
curl --compressed "$RESULTS_URL"url,source
https://bytekit.com/docs/changelog,sitemap
https://bytekit.com/docs,sitemap
https://bytekit.com/docs/introduction,sitemapThis request was run for real and returned
200. The file holds a header and 20 rows, and the first rows are shown.
The results file is a gzip-compressed CSV, served with Content-Encoding: gzip, so --compressed makes curl decode it. A client that does not decode gzip receives the compressed bytes, and their count is total_bytes.
It has the header url,source and one row per discovered URL, and source is sitemap, known or links. In that format a URL that contains a comma is wrapped in double quotes.
With compact: true the file is a plain list of URLs, one per line, with no header. The lastmod and depth columns are not emitted.
Cache
Before it queues discovery, ByteKit looks for a completed job of your account that has the same scheme and host and the same strategy, that finished within cache_ttl, and whose max_depth and max_urls are at least the ones you sent.
A job that was itself a cache hit is never matched, only the job that ran discovery is, so cache_ttl counts from when that job finished.
The cache field of the 202 response says what it found. GET leaves out cache and cache_age_s.
cache | Meaning |
|---|---|
miss | No such job. A new job is queued |
hit | A job matched. The response is already completed and has a results_url, and cache_age_s is the age of that job in seconds |
bypass | cache_ttl is 0, so the cache was not read and a new job is queued |
Sending the request above again a few seconds later returned a hit:
{
"id": "sm_6aa7e5ea6a854f19976754b0fdc8f438",
"url": "https://bytekit.com",
"strategy": "quick",
"max_depth": 2,
"max_urls": 20,
"status": "completed",
"total_urls": 20,
"sources": { "sitemap": 20 },
"total_bytes": 196,
"results_url": "https://…",
"warnings": null,
"error_code": null,
"error_message": null,
"compact": false,
"process": null,
"metadata": null,
"created_at": "2026-10-10T14:59:53.441Z",
"completed_at": "2026-10-10T14:59:53.440Z",
"expires_at": "2027-01-08T14:59:47.405Z",
"billing": {
"factors": [
{ "name": "endpoint", "value": 1, "reason": "sitemap" },
{ "name": "cache_hit", "value": 0.5, "reason": "hit" }
],
"raw_bytes": 196,
"multiplier": 0.5,
"billed_bytes": 98
},
"cache": "hit",
"cache_age_s": 6
}This request was run for real and returned
202. The signedresults_urlis shortened.
A hit has an id of its own and points at the same results file as the job that answered it.
The file is returned as that job stored it. It can hold more URLs than the max_urls you sent, and it keeps the format that job was created with, whatever compact you send, because compact is not part of the match.
The compact field of a hit's response repeats your request, not the format of the file, so tell the format from the file: only a non-compact file starts with the header line url,source.
To get a new job that follows your request, send cache_ttl 0.
Webhooks
Send a webhook_url and ByteKit POSTs the outcome to it. The header X-ByteKit-Event is sitemap.completed when the job completes and sitemap.failed when it fails, so you can dispatch on the header without reading the body. A cache hit sends sitemap.completed as well. A failure that sends no sitemap.failed is described under When a job fails.
Without a webhook_url the job is poll-only. Set webhook_secret to have the deliveries signed; see the Webhooks guide for verification. The Scraping guide lists the other event types.
The body has two keys, event, which repeats the header, and data. This page shows no captured delivery, only the fields:
| Event | data holds |
|---|---|
sitemap.completed | id, url, strategy, status, total_urls, sources, total_bytes, results_url, warnings, metadata, compact and billing |
sitemap.failed | id, url, strategy, status, error_code, error_message, warnings and metadata |
For a cache hit, data holds all the fields that GET returns.
Cost
A completed job is billed on the byte length of its gzip-compressed results file, which is total_bytes. The bytes ByteKit fetches while it discovers the URLs are not part of the charge. Bandwidth billing has the rules.
In the job above, raw_bytes and billed_bytes are both 196. A cache hit is billed at half: in the hit above billing lists a cache_hit factor of 0.5, and billed_bytes is 98.
A job that ends failed is not billed. It has no billing, and its estimate is rolled back.
When you submit a job, your usage counter rises by an estimate of 102,400 bytes for it. A completed job replaces the estimate with its billed bytes, and a failed job rolls it back, as the Usage counting section describes. In a run for real the counter rose by 102,400 while a job was queued, and by 146 once it completed with billed_bytes 146.
Errors
Errors use the envelope described in the Errors guide. These are the ones you are most likely to meet on the sitemap routes:
| Status | Code | When |
|---|---|---|
400 | invalid_url | url or webhook_url is not a valid HTTP(S) URL. error.details.reason says why |
400 | request_too_large | The request body is over the limit. See Request size limits |
401 | unauthorized, invalid_api_key | The API key is missing or not valid |
402 | quota_exceeded, spending_cap_reached, billing_entitlement_impaired, billing_entitlement_expired | The balance or the subscription state blocks the request. See the Errors guide |
403 | host_forbidden, path_forbidden | The URL's host or path is not allowed |
404 | not_found | On GET, the job id does not exist, or belongs to another account |
422 | validation_error | A field failed validation, such as max_urls out of range, an unknown strategy, a cache_ttl in the wrong form, or a malformed job id |
429 | rate_limited, concurrency_limit | Too many requests per second, or too many in flight at once. See the Errors guide |
Next steps
- Webhooks — verify a delivery's signature
- Scraping — the event types
- Bandwidth billing — how a sitemap job is billed
- Rate limits — concurrency slots and request size
- API Reference: Create a sitemap, Get a sitemap job — full request and response schemas