Extract
Batch Extract URLs
Extract a list of URLs in one job instead of one request per page: each URL comes back as Markdown, plain text or HTML, exactly like Extract, with results in input order. Duplicates are removed and invalid URLs are reported in the 202 and never billed. Returns 202 with a job id at once; poll the status URL or take a signed webhook when it finishes. Billed per successful page at Extract's price (normal 1, advanced 1, ultra 2); failed pages are free.
Authorizations
AuthorizationstringheaderrequiredBearer authentication header of the form Bearer <token>, where <token> is your Scavio API key (e.g. Bearer sk_live_your_key).
Body
application/jsonurlsstring[]requiredThe pages to extract, 1-1,000 URLs of up to 2048 characters each. Duplicates are removed after normalising (a trailing slash or a bare host upgraded to https counts as the same URL). Must be within your plan's limit: Free 10, PAYG 50, Project 100, Bootstrap 500, Startup 1,000, Growth 1,000.
Example: ["https://example.com/a", "https://example.com/b"]
formatenum<string>default:markdownReturn format for every page, as on Extract.
markdown— Clean Markdown with links kept, ready for an LLM or RAG. The default.text— The same content flattened to plain text.html— The raw page as served.
modeenum<string>default:normalFetch tier for every URL, and the price per page.
normal— The standard fetch. 1 credit per successful page.advanced— Renders JavaScript. 1 credit per successful page.ultra— For the hardest pages. 2 credits per successful page.
webhook_urlstringOptional public https URL that receives one signed POST when the batch completes, fails or is cancelled. The signing secret comes back once, in the 202, as webhook_secret.
Idempotency-KeystringheaderOptional, max 255 characters. Repeating the POST with the same key within 24 hours returns the original job instead of starting a second one.
Notes
Batch or crawl? Use batch when you already know the URLs: from Map, a sitemap, a search result or your own list. It can span any number of sites. Use Crawl when you want one site's pages discovered for you.
Pricing: successful pages only. Each URL that comes back is charged once at the tier price (credits_per_page in the 202). URLs refused up front (in invalid_urls), pages that fail or 404, and starting, polling and cancelling cost nothing. Credits are taken page by page; if your balance runs out, the batch stops with stopped_reason: "insufficient_credits" and keeps the pages it already fetched. If every URL is invalid the POST returns 400 with invalid_urls and no job is created.
Robots.txt. Batch behaves like Extract: you named each URL, so it is fetched as you asked. Crawl and Map, which discover pages for you, always obey robots.txt.
Plan limits. URLs per batch, and crawl and batch jobs running at once:
| Plan | Pages per crawl | Active jobs | URLs per batch | Map links |
|---|---|---|---|---|
| Free | 10 | 1 | 10 | 500 |
| PAYG | 100 | 1 | 50 | 1,000 |
| Project | 500 | 2 | 100 | 5,000 |
| Bootstrap | 2,000 | 3 | 500 | 5,000 |
| Startup | 5,000 | 5 | 1,000 | 5,000 |
| Growth | 10,000 | 10 | 1,000 | 5,000 |
Retention. Results are kept 3 days after the batch finishes, up to 2 MB of content per page and 250 MB per job; a job runs for at most 6 hours. Read them from Batch Status and stop a batch early with Cancel Batch.
Webhooks. Set webhook_url to a public https URL and Scavio POSTs one signed JSON event when the batch ends: event is completed, failed or cancelled, job is the same summary the status endpoint returns, and sent_at is the send time. Results are not in the body; fetch them from the status URL. Headers: Scavio-Signature, Scavio-Event and Scavio-Job-Id. A delivery that does not get a 2xx within 10 seconds is retried twice, 30 seconds and 5 minutes after the first attempt. Redirects are not followed, and the URL must use a public host name (no IP addresses or private hosts).
Verify every delivery. The 202 response returns webhook_secret (whsec_...) once; store it. Scavio-Signature is t=<unix seconds>,v1=<hex>, where v1 is the HMAC-SHA256 of <t>.<raw request body> keyed with that secret. Recompute it over the raw bytes, compare in constant time, and reject timestamps more than 5 minutes old:
import hashlib, hmac, time
def verify_scavio_signature(secret: str, header: str, raw_body: bytes, tolerance: int = 300) -> bool:
"""header is the Scavio-Signature value: t=<unix>,v1=<hex>."""
parts = dict(p.strip().split("=", 1) for p in header.split(",") if "=" in p)
t, v1 = parts.get("t", ""), parts.get("v1", "")
if not t.isdigit() or not v1 or abs(time.time() - int(t)) > tolerance:
return False
signed = t.encode() + b"." + raw_body
expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, v1)
# Flask example
from flask import Flask, request, abort
app = Flask(__name__)
WEBHOOK_SECRET = "whsec_from_the_202_response"
@app.post("/scavio/webhook")
def scavio_webhook():
if not verify_scavio_signature(WEBHOOK_SECRET, request.headers.get("Scavio-Signature", ""), request.get_data()):
abort(400)
event = request.get_json()
print(event["event"], event["job"]["id"], event["job"]["pages"]["succeeded"])
return "", 204A delivery body looks like this:
{
"event": "completed",
"job": {
"id": "crawl_01a12d2e-f813-73c2-8c07-4c1f0ae402df",
"type": "crawl",
"status": "completed",
"stopped_reason": null,
"error": null,
"url": "https://example.com/",
"format": "markdown",
"mode": "normal",
"pages": { "discovered": 1, "queued": 0, "succeeded": 1, "failed": 0, "skipped": 0 },
"credits_used": 1,
"created_at": "2026-10-11T22:56:55.312Z",
"started_at": "2026-10-11T22:57:00.424Z",
"completed_at": "2026-10-11T22:57:02.665Z",
"expires_at": "2026-10-14T22:57:02.665Z",
"webhook": { "status": "pending", "attempts": 1 },
"status_url": "https://api.scavio.dev/api/v1/extract/crawl/crawl_01a12d2e-f813-73c2-8c07-4c1f0ae402df"
},
"sent_at": "2026-10-11T22:57:02.701Z"
}Request
from scavio import ScavioClient
client = ScavioClient(api_key="sk_live_your_key")
urls = [
"https://hono.dev/docs/api/routing",
"https://hono.dev/docs/api/context",
"https://hono.dev/docs/api/request",
]
# wait=True polls the job and returns every result, in input order
done = client.extract.batch(urls, format="markdown", wait=True)
for r in done["data"]["results"]:
print(r["status"], r["url"], r["credits"])Response