Documentation
API Reference
Full reference for every endpoint. Base URL is http://localhost:7431 (compose) or your deploy URL. Smart mode is default. No API key needed for self-host unless API_KEYS is set. Swagger UI at /swagger in debug mode.
1. Scrape — POST /v1/scrape · GET /v1/scrape
Scrape a URL to markdown + metadata. Smart tries static (Colly) first, falls back to dynamic (Chromedp) for SPAs. Supports single URL (url) or sync multi-URL (urls: 1–10, no Redis, parallel 5, ordered results). GET mirrors POST via query params.
Request — single URL (POST)
curl -X POST http://localhost:7431/v1/scrape \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com",
"mode": "smart",
"onlyMainContent": true,
"blockAds": true,
"include_links": true,
"screenshot": true,
"screenshot_opts": { "full_page": true, "format": "jpeg", "quality": 80 },
"images": true,
"image_format": "blob",
"max_images": 5,
"summary": true,
"summary_sentences": 5,
"extract_schema": { "title": { "selector": "h1" }, "links": { "selector": "a", "attr": "href", "multiple": true } },
"actions": [{ "type": "scroll_to_bottom" }]
}'Request — GET
curl "http://localhost:7431/v1/scrape?url=https://example.com&mode=smart&include_links=true"Request — sync multi-URL (urls: [])
curl -X POST http://localhost:7431/v1/scrape \
-H "Content-Type: application/json" \
-d '{"urls": ["https://example.com","https://example.org"], "mode": "smart", "images": true}'Response — single URL
{
"url": "https://example.com",
"markdown": "# Example Domain\n\n...",
"html": "<!doctype html>...",
"metadata": { "title": "Example Domain", "description": "..." },
"links": [{ "url": "https://www.iana.org/domains/example", "text": "More information...", "isInternal": false }],
"screenshot": { "format": "jpeg", "width": 1280, "height": 800, "full_page": true, "size_bytes": 12345, "truncated": false },
"images": [{ "url": "https://...", "alt": "..." }],
"extracted": { "title": "Example Domain" },
"summary": "This domain is for use in..."
}Response — multi-URL (results[])
{
"results": [
{ "url": "https://example.com", "markdown": "# ...", "metadata": { "title": "..." }, "html": "..." },
{ "url": "https://example.org", "error": "scrape failed: ..." }
]
}
// On partial failure an entry carries "error" and no markdown; order matches input.2. Search — POST /v1/search · GET /v1/search
Hybrid search: SearXNG (primary, free) → Brave API (fallback, 1 QPS) → Stealth (chromedp, reuses shared allocator, gofakeit UA). SEARXNG_ENDPOINT, BRAVE_SEARCH_API_KEY, STEALTH_ENABLED control backends.
Request
curl -X POST http://localhost:7431/v1/search \
-H "Content-Type: application/json" \
-d '{"query": "self-hosted scraping", "limit": 5, "offset": 0, "category": "general", "rerank": true}'
# GET
curl "http://localhost:7431/v1/search?q=self-hosted%20scraping&limit=5&category=news"Response
{
"query": "self-hosted scraping",
"results": [{ "title": "...", "url": "https://...", "description": "...", "highlights": ["…120-char window…"], "relevance": 0.85 }],
"hasMore": true,
"nextOffset": 5,
"count": 1
}3. Crawl (async) — POST /v1/crawl
Async BFS crawl via Asynq/Redis. Requires REDIS_URL. Returns 202 with job id. Poll GET /v1/crawl/:id. Domain-locked, deduped, skips non-HTML, bounded queue (drops excess, never deadlocks).
Request
curl -X POST http://localhost:7431/v1/crawl \
-H "Content-Type: application/json" \
-d '{
"url": "https://docs.example.com",
"maxDepth": 3,
"limit": 20,
"mode": "smart",
"exclude_paths": ["/admin/*", "/login"],
"include_paths": ["/docs/**"],
"webhook_url": "https://myapp.example.com/hooks/tomoshibi",
"webhook_secret": "s3cret"
}'Response — 202 Accepted
{
"id": "e8a932c0-82af-4a11-bd4a-6f17e29b1111",
"url": "https://docs.example.com",
"maxDepth": 3,
"limit": 20
}Tuning via env: CRAWL_CONCURRENCY (default 4, max 10), CRAWL_DOMAIN_DELAY (1s), CRAWL_MAX_RETRIES (2, never retry 4xx), CRAWL_TIMEOUT (30m → status timeout), CRAWL_SCRAPE_TIMEOUT (30s per page).
4. Crawl Status — GET /v1/crawl/:id
Request
curl http://localhost:7431/v1/crawl/e8a932c0-82af-4a11-bd4a-6f17e29b1111Response — in progress
{ "id": "...", "queue": "default", "state": "active" }Response — completed
{
"id": "...",
"queue": "default",
"state": "completed",
"crawl": {
"status": "completed | partial | failed | cancelled | timeout",
"total_pages": 5,
"max_depth": 3,
"limit": 20,
"pages": [{ "url": "https://...", "title": "...", "preview": "first 300 chars..." }]
},
"failed_urls": [{ "url": "https://.../404", "error": "scraping failed: ..." }]
}Frontend poll pattern: GET /v1/crawl/:id every 5s until state is completed or failed. Render pages[] with title + preview.
5. Map — POST /v1/map
URL discovery without scraping. Reads robots.txt → sitemap.xml (recursive sitemap-index, up to 5,000 URLs), falls back to one-level link discovery.
Request
curl -X POST http://localhost:7431/v1/map \
-H "Content-Type: application/json" \
-d '{"url": "https://docs.example.com", "search": "/docs", "limit": 200}'Response
{ "url": "https://docs.example.com", "count": 2, "links": [{ "url": "https://.../intro", "source": "sitemap" }, { "url": "https://.../api", "source": "link" }] }6. Batch Scrape — POST /v1/batch/scrape · GET /v1/batch/:id
Async batch scrape (Redis). Enqueue up to 20 URLs in one call.
Request — enqueue
curl -X POST http://localhost:7431/v1/batch/scrape \
-H "Content-Type: application/json" \
-d '{"urls": ["https://a.example.com", "https://b.example.com"]}'Response — 202
{ "batch_id": "3f2a...", "tasks": [{ "id": "task-id-1", "url": "https://a.example.com" }, { "id": "task-id-2", "url": "https://b.example.com" }] }Status — GET /v1/batch/:id
{ "batch_id": "3f2a...", "total": 2, "completed": 1, "failed": 0, "tasks": [ ... ] }7. Monitor — POST /v1/monitor · GET /v1/monitor/:id · DELETE /v1/monitor/:id
Change tracking (Redis). Scrapes on a schedule, hashes markdown (SHA-256), fires signed webhook on change. First check records baseline without notifying. Minimum interval 3600s (1h).
Create
curl -X POST http://localhost:7431/v1/monitor \
-H "Content-Type: application/json" \
-d '{
"url": "https://pricing.example.com",
"interval_seconds": 3600,
"webhook_url": "https://myapp.example.com/hooks/price-changed",
"webhook_secret": "s3cret"
}'Status & delete
curl http://localhost:7431/v1/monitor/<id>
# → { config, last_hash, next_check }
curl -X DELETE http://localhost:7431/v1/monitor/<id>Webhook payload
{
"monitor_id": "abc123",
"url": "https://pricing.example.com",
"changed": true,
"hash_old": "aaa...",
"hash_new": "bbb...",
"changed_at": "2026-08-01T12:00:00Z"
}
// Header: X-Tomoshibi-Signature: sha256=<hmac-hex> (HMAC-SHA256 of body with webhook_secret)