Web Extract — Structured Data from the Open Web
Extract structured JSON from web pages, search engines, and entire sites in ONE call — {title, summary, sections, key_metrics, outgoing_links, author, date, page_type, ...} fields, no second LLM pass to parse HTML.
How to use it
Claude Code
- Run the line below. It pulls the whole folder into
~/.claude/skills/web-extract, including the files SKILL.md points to. - Describe your job in plain words. Claude Code follows the skill from there.
npx degit SerendipityOneInc/ZooData-Skills/web-extract#main ~/.claude/skills/web-extractFor one project only, change the path to .claude/skills/web-extract. This skill also uses webtools.py, data.json — copying SKILL.md alone won't be enough. See the folder on GitHub.
Claude (web or desktop app)
- On this page open ⋯ → Download .md.
- Save it as SKILL.md in a folder, zip the folder, then Customize → Skills → + → Create skill → Upload a skill.
- Pick the file and Save. Claude shows the name and description and runs a security scan.
- Check the skill is switched on.
- Start a new chat and describe your job in plain words. The AI follows the skill from there.
ChatGPT or another app
- ChatGPT: make a Project and paste it into Instructions.
- Neither? Paste it at the top of a new chat — it works for that chat.
Not working?
- Check which app you pasted it into — the steps above name the right one.
- Some skills need the paid tier of Claude or ChatGPT.
Paste into Claude, ChatGPT or Cursor.
Source of Web Extract — Structured Data from the Open Web
Show the full text226 lines
| name | description | metadata |
|---|---|---|
| web-extract | > Extract structured JSON from web pages, search engines, and entire sites in ONE call — {title, summary, sections, key_metrics, outgoing_links, author, date, page_type, ...} fields, no second LLM pass to parse HTML. Six endpoints: scrape (single URL), scrape-interactive (JS-rendered pages with click/scroll/type), search (Google SERP + deep-scrape), map (URL discovery), crawl + crawl-status (async recursive crawl). Markdown/raw HTML on request. USE when the user needs page DATA — product pricing/specs, article fields, link graphs, JS-heavy SPAs, Google results with content. Prefer over browser-act (automation/screenshots) and WebFetch (static, no JS, no structured fields). Not for citation-rich research (use deep-research). Trigger (EN): scrape this URL, extract data from page, crawl this site, deep-scrape search results, map a domain's URLs, render this JS page. 触发词:抓取/爬取/网页提取/结构化抽取/搜索带内容/全站爬取/JS 渲染抓取/点击后抓取. Requires ZOODATA_API_KEY (free key: https://zoodata.ai/en/api-keys). | version: "0.2.4 author: SerendipityOneInc homepage: https://github.com/SerendipityOneInc/ZooData-Skills openclaw: {"requires": {"env": ["ZOODATA_API_KEY"]}, "primaryEnv": "ZOODATA_API_KEY"} |
Web Extract — Structured Data from the Open Web
Backed by the ZooData WebTools API. Six HTTP endpoints. One API key. Structured JSON by default — no second LLM pass to parse fields.
Files
| File | Purpose |
|---|---|
{skill_base_dir}/scripts/webtools.py |
Thin CLI wrapper — one subcommand per endpoint. Has the Cloudflare-UA, crawl-warmup, and nested-data-shape quirks baked in. Run --help for params. |
{skill_base_dir}/references/reference.md |
Full request/response schemas, error codes, billing, edge cases. Load when you need exact field names. |
Why pick this skill (vs the alternatives in this environment)
| Tool | What it gives you | When to pick it |
|---|---|---|
web-extract (this skill) |
Page → {title, summary, sections, key_metrics, outgoing_links, ...} JSON in one call |
You need page DATA (price, specs, fields, link graphs) — downstream code or LLM can use the JSON directly without re-parsing |
browser-act |
Browser session: click, scroll, type, screenshot | You need to interact with a page (login flow, take a screenshot, fill a form) or visually verify rendering |
WebFetch (built-in) |
Static URL → markdown | You need a single static page as prose, no JS rendering, no structured fields |
deep-research |
Multi-source research with citations | You need a synthesized report drawing from many web sources, not raw data |
monid |
Generic tool-discovery layer | You're not sure which tool to use yet and want to browse options |
Key advantage: every other tool above forces a second pass (re-LLM the markdown / re-parse the HTML / extract structure manually). The underlying API returns the structured fields directly — saves a round-trip and tokens.
Credential
Required: ZOODATA_API_KEY.
Get a free key (1,000 credits) at zoodata.ai/en/api-keys.
export ZOODATA_API_KEY='hms_live_xxx'
# OR persist to disk (keep the file private — 0600; it holds a bearer credential):
mkdir -p ~/.zoodata && chmod 700 ~/.zoodata
(umask 077; echo '{"api_key":"hms_live_xxx"}' > ~/.zoodata/config.json)
Capabilities & Data Flow
- Network: only
https://api.zoodata.aiWebTools endpoints (BearerZOODATA_API_KEY). Target pages are fetched server-side by ZooData, not from this machine. - Execution: bundled CLI
{skill_base_dir}/scripts/webtools.py(Python 3, stdlib-only) with exactly the documented subcommands (scrape,interactive,search,map,crawl,crawl-status,crawl-wait,check) — the full surface is the declared surface. - Local files: none; reads the optional credential store
~/.zoodata/config.json. - Sent to the API: the target URLs, search queries, and crawl options you request.
- Credits: every call consumes credits (crawl bills per page). For large crawls or deep-scrapes, state the estimated cost and confirm before submitting.
Two ways to drive it
webtools.pyCLI (preferred — quirks baked in): UA header, warmup tolerance, retry/backoff, JSON-first defaults all handled. Just runpython {skill_base_dir}/scripts/webtools.py <subcommand>.- Raw curl / HTTP (when CLI isn't installed or for ad-hoc calls): every example below also shows the raw POST. Always set
User-Agent: web-extract-skill/1.0— the Cloudflare edge rejects the default Python-urllib UA with HTTP 403 (see Tips).
Endpoint base URL: https://api.zoodata.ai/openapi/v2/webtools/*. All POST with JSON body, except crawl/{id} (GET).
Default format: JSON
This skill defaults to formats: ["json"] for every scrape / scrape-interactive / search-with-scrapeOptions call. The API itself defaults to ["markdown"], but JSON gives the agent structured fields (title, summary, sections, key_metrics, outgoing_links, page_type, …) that compose better with downstream tools and are usually smaller to put back into context.
Switch to markdown only when the user explicitly asks for "the article text" / "clean prose" / "as markdown", or when the page is genuinely article-shaped (long-form blog post / news / docs) and structured fields wouldn't help. Switch to rawHtml only when the user needs DOM access (custom CSS selector extraction, table parsing the API didn't structure, embedded <script> data, etc.). Request multiple formats in one call (["json","markdown"]) when you genuinely need both — but never silently fall back to markdown when the user didn't ask for it.
When to use
| User intent | Endpoint | Cost |
|---|---|---|
| Fetch one URL as structured JSON (default) | POST /scrape formats:["json"] |
1 credit |
| Fetch one URL as markdown (user explicitly asked) | POST /scrape formats:["markdown"] |
1 credit |
| Fetch one URL as raw HTML (user needs DOM) | POST /scrape formats:["rawHtml"] |
1 credit |
| Same as above, but page needs JS / login / click-through / scroll | POST /scrape-interactive |
1 credit |
| Google search → list of result URLs | POST /search (SERP-only) |
1 credit |
| Google search → URLs + structured JSON for each | POST /search scrapeOptions:{format:"json"} |
1 credit per returned result |
| List all URLs reachable from a website | POST /map |
1 credit |
| Recursively pull every page of a site (async) | POST /crawl + GET /crawl/{id} poll loop |
1/submit + 1/poll |
Quick start (CLI — recommended)
Shorthand: WT="python {skill_base_dir}/scripts/webtools.py"
# 1. Scrape one URL as structured JSON (default)
$WT scrape https://example.com
# → data.json = {title, summary, sections, key_metrics, outgoing_links, author, date, page_type, ...}
# 2. Scrape as markdown (user explicitly asked for prose)
$WT scrape https://example.com --format markdown
# 3. Need BOTH JSON and markdown in one call
$WT scrape https://example.com --format json --format markdown
# 4. JS-heavy page — wait, click "Load more", scrape
$WT interactive https://example.com --actions '[
{"type":"wait","milliseconds":1500},
{"type":"click","selector":"button.load-more"},
{"type":"wait","milliseconds":1000}
]'
# 5. SERP-only search (cheap — 1 credit total, regardless of result count)
$WT search "best wireless earbuds 2026" --limit 10
# 6. Deep-scrape every search result as JSON (1 credit per result)
$WT search "best wireless earbuds 2026" --limit 5 --deep-scrape
# 7. Search restricted to specific domains, last 24h
$WT search "python async" --limit 20 --tbs qdr:d \
--include-domains "python.org,realpython.com"
# 8. Discover every URL on a docs site under /api/
$WT map https://docs.example.com --include-paths "/api/.*" --limit 1000
# 9. Crawl + poll in one shot (handles warmup, paginates pages internally)
$WT crawl-wait https://blog.example.com --limit 200 --max-depth 3 \
--poll-interval 10 --max-wait 1800
# 9b. Or submit + manual poll
$WT crawl https://blog.example.com --limit 200 --max-depth 3
$WT crawl-status job_xxx --skip 0 --limit 100
# 10. Verify auth + credits (1 credit — scrapes example.com)
$WT check
Run $WT <subcommand> --help for every flag on any subcommand.
Same calls via raw curl
When the CLI isn't available (no python3 / offline / restricted env), every endpoint also takes a plain JSON POST:
curl -sS -X POST https://api.zoodata.ai/openapi/v2/webtools/scrape \
-H "Authorization: Bearer $ZOODATA_API_KEY" \
-H "Content-Type: application/json" \
-H "User-Agent: web-extract-skill/1.0" `# REQUIRED — see Tips` \
-d '{"url":"https://example.com","formats":["json"]}'
See references/reference.md for the full JSON body shape of every endpoint. Any non-curl HTTP client must set a custom User-Agent header — Cloudflare's edge blocks the default Python-urllib UA with HTTP 403 (error code 1010).
Key parameters at a glance
Full schemas are in references/reference.md. Load that when you need exact field names or are debugging.
/scrape: url (required), formats: ["json"|"markdown"|"rawHtml"] — skill default ["json"] (API itself defaults to ["markdown"] if formats omitted; always pass it explicitly). Max 3 formats per call.
/scrape-interactive: url, formats, actions: [...] — 7 action types (wait/click/write/press/scroll/scrape/executeJavascript). Max 50 actions, cumulative wait ≤ 60s.
/search: query (supports inline Google operators), limit (1–20, default 10), sources (["web","news","images"]), tbs (qdr:d|w|m|y), includeDomains/excludeDomains (bare hostnames, max 20 each), scrapeOptions: {format:"markdown"|"json"} (presence triggers deep-scrape).
/map: url, limit (1–100000, default 5000), search (keyword rank filter), sitemapMode (include/only/skip), includeSubdomains (default true), includePaths/excludePaths (regex), ignoreQueryParameters (default true).
/crawl: url, limit (1–10000, default 100), maxDepth (1–10), includePaths/excludePaths, allowSubdomains, sitemapMode, ignoreQueryParameters (default false — opposite of /map), crawlEntireDomain.
GET /crawl/{job_id}: query params skip, limit (1–1000). data is an array of page-scrape blobs. meta.total is the running total; compare against data.length to detect completion.
Tips
- 🚨 Cloudflare 1010 trap (Python / non-curl HTTP clients). The Cloudflare edge in front of
api.zoodata.aiblocks default PythonPython-urllib/X.YUser-Agent → HTTP 403. ALWAYS set an explicitUser-Agentheader (any non-default value works, e.g.User-Agent: web-extract-skill/1.0). curl is fine out of the box; Python / Go / Node fetch / requests all need it. - Crawl
GET /crawl/{job_id}warmup gotcha: for ~5–10 seconds afterPOST /crawlreturns the job_id, polling may returnNOT_FOUND. This is the job still registering, not a real "not found." Tolerate it: retry with 5s sleep up to ~3 times before giving up. crawl-statusresponse shape:datais an object{id, status, completed, total, data: [pages...]}— the page array is nested atdata.data. Status values:"queued","running","completed","failed". (Failedsuccess:falseresponses cost 0 credits.)- A
success:trueresponse can still contain an error page. Always checkdata.meta.statusCode(or per-resultmeta.statusCodefor search) before trusting content. - Deep-scrape
/searchmay return fewer thanlimitresults — pages with no extractable content in the chosen format are dropped. - Domain filters are bare hostnames only.
https://github.comorgithub.com/path→ 422. Usegithub.com. includePaths/excludePathsare regex, not glob. Use/blog/.*not/blog/**.- Crawl job IDs are tenant-scoped. Polling someone else's job returns 404 (indistinguishable from "job not found" — by design).
- Refused requests (
ACCESS_DENIED,RATE_LIMITED) are NOT billed.TIMEOUTandCONTENT_UNAVAILABLEare. - Polling costs 1 credit per call. For large crawls, batch your polls — fetch with
limit=1000and space polls ≥10s apart. - Format choice (skill default = JSON):
jsonis the default — gives structured fields (title/summary/sections/key_metrics/outgoing_links/page_type/...) that downstream tools can consume directly. Usemarkdownonly when the user asks for clean prose or the page is article-shaped (blog post / docs); userawHtmlonly when DOM access is needed (custom selectors, table parsing the JSON layer doesn't expose).
On Missing Key (no credentials configured)
BEFORE calling any endpoint, verify a credential is configured. Reliable check: python {skill_base_dir}/scripts/webtools.py check — exits 2 if no key is found in env (ZOODATA_API_KEY) or the config file (~/.zoodata/config.json). A [ -z "$ZOODATA_API_KEY" ] test alone is NOT sufficient.
When no key is found through any mechanism:
- STOP. Do not call any endpoint.
- Do NOT fall back to "partial output from training data" / "describe the page from common knowledge" / "for reference only" preview. This skill's deliverable is structured extraction of THIS URL's actual content — without the data, there is no deliverable.
- Tell the user, in their language, all three of:
- "
ZOODATA_API_KEYis not set — I need this to fetch the page." - Get a free key (1,000 credits, no credit card): https://zoodata.ai/en/api-keys
- Configure via one of:
export ZOODATA_API_KEY='hms_live_xxx'(session only)mkdir -p ~/.zoodata && chmod 700 ~/.zoodata && (umask 077; echo '{"api_key":"hms_live_xxx"}' > ~/.zoodata/config.json)(persistent; keep the file private — 0600)
- "
On 401 Invalid Key
When any endpoint returns HTTP 401:
- STOP further calls immediately. A rejected key won't be accepted on retry — every subsequent call will return 401 too.
- Tell the user: their
ZOODATA_API_KEYwas rejected (invalid, revoked, or expired). Direct them to zoodata.ai/en/api-keys. - If you collected partial output before failure, show it and mark partial. Do not fabricate the rest — no "training-data fallback" / "page description from common knowledge" substitution.
On 402 Credit Exhausted
When any endpoint returns HTTP 402:
- STOP further calls.
- Report partial findings already gathered, plus the
creditsRemainingnumber from the last successful call. - Point the user at zoodata.ai/en/pricing to top up. Do not fabricate the rest — no "common-sense page summary" filler in place of a real fetch.
See also
zoodata— commerce endpoints (Amazon products / markets / reviews / brands). Pair with webtools when you need to validate Amazon findings against the open web (competitor sites, news, off-Amazon reviews).amazon-market-analysis— uses the commerce side; webtools complements it for non-Amazon channel research.
| 1 | |
| 2 | name web-extract |
| 3 | description > |
| 4 | Extract structured JSON from web pages, search engines, and entire sites |
| 5 | in ONE call — {title, summary, sections, key_metrics, outgoing_links, |
| 6 | author, date, page_type, ...} fields, no second LLM pass to parse HTML. |
| 7 | Six endpoints: scrape (single URL), scrape-interactive (JS-rendered pages |
| 8 | with click/scroll/type), search (Google SERP + deep-scrape), map (URL |
| 9 | discovery), crawl + crawl-status (async recursive crawl). Markdown/raw |
| 10 | HTML on request. |
| 11 | |
| 12 | USE when the user needs page DATA — product pricing/specs, article fields, |
| 13 | link graphs, JS-heavy SPAs, Google results with content. Prefer over |
| 14 | browser-act (automation/screenshots) and WebFetch (static, no JS, no |
| 15 | structured fields). Not for citation-rich research (use deep-research). |
| 16 | |
| 17 | Trigger (EN): scrape this URL, extract data from page, crawl this site, |
| 18 | deep-scrape search results, map a domain's URLs, render this JS page. |
| 19 | 触发词:抓取/爬取/网页提取/结构化抽取/搜索带内容/全站爬取/JS 渲染抓取/点击后抓取. |
| 20 | |
| 21 | Requires ZOODATA_API_KEY (free key: https://zoodata.ai/en/api-keys). |
| 22 | metadata |
| 23 | version "0.2.4" |
| 24 | author SerendipityOneInc |
| 25 | homepage https://github.com/SerendipityOneInc/ZooData-Skills |
| 26 | openclaw {"requires": {"env": ["ZOODATA_API_KEY"]}, "primaryEnv": "ZOODATA_API_KEY"} |
| 27 | |
| 28 | |
| 29 | # Web Extract — Structured Data from the Open Web |
| 30 | |
| 31 | Backed by the ZooData WebTools API. Six HTTP endpoints. One API key. **Structured JSON by default — no second LLM pass to parse fields.** |
| 32 | |
| 33 | ## Files |
| 34 | |
| 35 | | File | Purpose | |
| 36 | |------|---------| |
| 37 | | `{skill_base_dir}/scripts/webtools.py` | Thin CLI wrapper — one subcommand per endpoint. Has the Cloudflare-UA, crawl-warmup, and nested-data-shape quirks baked in. Run `--help` for params. | |
| 38 | | `{skill_base_dir}/references/reference.md` | Full request/response schemas, error codes, billing, edge cases. Load when you need exact field names. | |
| 39 | |
| 40 | ## Why pick this skill (vs the alternatives in this environment) |
| 41 | |
| 42 | | Tool | What it gives you | When to pick it | |
| 43 | |---|---|---| |
| 44 | | **`web-extract`** (this skill) | Page → `{title, summary, sections, key_metrics, outgoing_links, ...}` JSON in one call | You need **page DATA** (price, specs, fields, link graphs) — downstream code or LLM can use the JSON directly without re-parsing | |
| 45 | | `browser-act` | Browser session: click, scroll, type, screenshot | You need to **interact** with a page (login flow, take a screenshot, fill a form) or visually verify rendering | |
| 46 | | `WebFetch` (built-in) | Static URL → markdown | You need a **single static page** as prose, no JS rendering, no structured fields | |
| 47 | | `deep-research` | Multi-source research with citations | You need a **synthesized report** drawing from many web sources, not raw data | |
| 48 | | `monid` | Generic tool-discovery layer | You're not sure which tool to use yet and want to browse options | |
| 49 | |
| 50 | **Key advantage**: every other tool above forces a second pass (re-LLM the markdown / re-parse the HTML / extract structure manually). The underlying API returns the structured fields directly — saves a round-trip and tokens. |
| 51 | |
| 52 | ## Credential |
| 53 | |
| 54 | Required: `ZOODATA_API_KEY`. |
| 55 | Get a free key (1,000 credits) at [zoodata.ai/en/api-keys]. |
| 56 | |
| 57 | |
| 58 | export ZOODATA_API_KEY='hms_live_xxx' |
| 59 | # OR persist to disk (keep the file private — 0600; it holds a bearer credential): |
| 60 | mkdir -p ~/.zoodata && chmod 700 ~/.zoodata |
| 61 | (umask 077; echo '{"api_key":"hms_live_xxx"}' > ~/.zoodata/config.json) |
| 62 | |
| 63 | |
| 64 | ## Capabilities & Data Flow |
| 65 | |
| 66 | **Network**: only `https://api.zoodata.ai` WebTools endpoints (Bearer `ZOODATA_API_KEY`). Target pages are fetched server-side by ZooData, not from this machine. |
| 67 | **Execution**: bundled CLI `{skill_base_dir}/scripts/webtools.py` (Python 3, stdlib-only) with exactly the documented subcommands (`scrape`, `interactive`, `search`, `map`, `crawl`, `crawl-status`, `crawl-wait`, `check`) — the full surface is the declared surface. |
| 68 | **Local files**: none; reads the optional credential store `~/.zoodata/config.json`. |
| 69 | **Sent to the API**: the target URLs, search queries, and crawl options you request. |
| 70 | **Credits**: every call consumes credits (crawl bills per page). For large crawls or deep-scrapes, state the estimated cost and confirm before submitting. |
| 71 | |
| 72 | ## Two ways to drive it |
| 73 | |
| 74 | **`webtools.py` CLI** (preferred — quirks baked in): UA header, warmup tolerance, retry/backoff, JSON-first defaults all handled. Just run `python {skill_base_dir}/scripts/webtools.py <subcommand>`. |
| 75 | **Raw curl / HTTP** (when CLI isn't installed or for ad-hoc calls): every example below also shows the raw POST. **Always set `User-Agent: web-extract-skill/1.0`** — the Cloudflare edge rejects the default Python-urllib UA with HTTP 403 (see Tips). |
| 76 | |
| 77 | Endpoint base URL: `https://api.zoodata.ai/openapi/v2/webtools/*`. All POST with JSON body, except `crawl/{id}` (GET). |
| 78 | |
| 79 | ## Default format: JSON |
| 80 | |
| 81 | **This skill defaults to `formats: ["json"]` for every scrape / scrape-interactive / search-with-scrapeOptions call.** The API itself defaults to `["markdown"]`, but JSON gives the agent structured fields (title, summary, sections, key_metrics, outgoing_links, page_type, …) that compose better with downstream tools and are usually smaller to put back into context. |
| 82 | |
| 83 | **Switch to `markdown` only when** the user explicitly asks for "the article text" / "clean prose" / "as markdown", or when the page is genuinely article-shaped (long-form blog post / news / docs) and structured fields wouldn't help. **Switch to `rawHtml`** only when the user needs DOM access (custom CSS selector extraction, table parsing the API didn't structure, embedded `<script>` data, etc.). Request multiple formats in one call (`["json","markdown"]`) when you genuinely need both — but never silently fall back to markdown when the user didn't ask for it. |
| 84 | |
| 85 | ## When to use |
| 86 | |
| 87 | | User intent | Endpoint | Cost | |
| 88 | |---|---|---| |
| 89 | | Fetch one URL as structured JSON (default) | `POST /scrape` `formats:["json"]` | 1 credit | |
| 90 | | Fetch one URL as markdown (user explicitly asked) | `POST /scrape` `formats:["markdown"]` | 1 credit | |
| 91 | | Fetch one URL as raw HTML (user needs DOM) | `POST /scrape` `formats:["rawHtml"]` | 1 credit | |
| 92 | | Same as above, but page needs JS / login / click-through / scroll | `POST /scrape-interactive` | 1 credit | |
| 93 | | Google search → list of result URLs | `POST /search` (SERP-only) | 1 credit | |
| 94 | | Google search → URLs + structured JSON for each | `POST /search` `scrapeOptions:{format:"json"}` | 1 credit per returned result | |
| 95 | | List all URLs reachable from a website | `POST /map` | 1 credit | |
| 96 | | Recursively pull every page of a site (async) | `POST /crawl` + `GET /crawl/{id}` poll loop | 1/submit + 1/poll | |
| 97 | |
| 98 | ## Quick start (CLI — recommended) |
| 99 | |
| 100 | Shorthand: `WT="python {skill_base_dir}/scripts/webtools.py"` |
| 101 | |
| 102 | |
| 103 | # 1. Scrape one URL as structured JSON (default) |
| 104 | $WT scrape https://example.com |
| 105 | # → data.json = {title, summary, sections, key_metrics, outgoing_links, author, date, page_type, ...} |
| 106 | |
| 107 | # 2. Scrape as markdown (user explicitly asked for prose) |
| 108 | $WT scrape https://example.com --format markdown |
| 109 | |
| 110 | # 3. Need BOTH JSON and markdown in one call |
| 111 | $WT scrape https://example.com --format json --format markdown |
| 112 | |
| 113 | # 4. JS-heavy page — wait, click "Load more", scrape |
| 114 | $WT interactive https://example.com --actions '[ |
| 115 | {"type":"wait","milliseconds":1500}, |
| 116 | {"type":"click","selector":"button.load-more"}, |
| 117 | {"type":"wait","milliseconds":1000} |
| 118 | ]' |
| 119 | |
| 120 | # 5. SERP-only search (cheap — 1 credit total, regardless of result count) |
| 121 | $WT search "best wireless earbuds 2026" --limit 10 |
| 122 | |
| 123 | # 6. Deep-scrape every search result as JSON (1 credit per result) |
| 124 | $WT search "best wireless earbuds 2026" --limit 5 --deep-scrape |
| 125 | |
| 126 | # 7. Search restricted to specific domains, last 24h |
| 127 | $WT search "python async" --limit 20 --tbs qdr:d \ |
| 128 | --include-domains "python.org,realpython.com" |
| 129 | |
| 130 | # 8. Discover every URL on a docs site under /api/ |
| 131 | $WT map https://docs.example.com --include-paths "/api/.*" --limit 1000 |
| 132 | |
| 133 | # 9. Crawl + poll in one shot (handles warmup, paginates pages internally) |
| 134 | $WT crawl-wait https://blog.example.com --limit 200 --max-depth 3 \ |
| 135 | --poll-interval 10 --max-wait 1800 |
| 136 | |
| 137 | # 9b. Or submit + manual poll |
| 138 | $WT crawl https://blog.example.com --limit 200 --max-depth 3 |
| 139 | $WT crawl-status job_xxx --skip 0 --limit 100 |
| 140 | |
| 141 | # 10. Verify auth + credits (1 credit — scrapes example.com) |
| 142 | $WT check |
| 143 | |
| 144 | |
| 145 | Run `$WT <subcommand> --help` for every flag on any subcommand. |
| 146 | |
| 147 | ## Same calls via raw curl |
| 148 | |
| 149 | When the CLI isn't available (no `python3` / offline / restricted env), every endpoint also takes a plain JSON POST: |
| 150 | |
| 151 | |
| 152 | curl -sS -X POST https://api.zoodata.ai/openapi/v2/webtools/scrape \ |
| 153 | -H "Authorization: Bearer $ZOODATA_API_KEY" \ |
| 154 | -H "Content-Type: application/json" \ |
| 155 | -H "User-Agent: web-extract-skill/1.0" `# REQUIRED — see Tips` \ |
| 156 | -d '{"url":"https://example.com","formats":["json"]}' |
| 157 | |
| 158 | |
| 159 | See `references/reference.md` for the full JSON body shape of every endpoint. **Any non-curl HTTP client must set a custom `User-Agent` header** — Cloudflare's edge blocks the default Python-urllib UA with HTTP 403 (error code 1010). |
| 160 | |
| 161 | ## Key parameters at a glance |
| 162 | |
| 163 | Full schemas are in `references/reference.md`. Load that when you need exact field names or are debugging. |
| 164 | |
| 165 | **`/scrape`**: `url` (required), `formats: ["json"|"markdown"|"rawHtml"]` — **skill default `["json"]`** (API itself defaults to `["markdown"]` if `formats` omitted; always pass it explicitly). Max 3 formats per call. |
| 166 | |
| 167 | **`/scrape-interactive`**: `url`, `formats`, `actions: [...]` — 7 action types (`wait`/`click`/`write`/`press`/`scroll`/`scrape`/`executeJavascript`). Max 50 actions, cumulative `wait` ≤ 60s. |
| 168 | |
| 169 | **`/search`**: `query` (supports inline Google operators), `limit` (1–20, default 10), `sources` (`["web","news","images"]`), `tbs` (`qdr:d|w|m|y`), `includeDomains`/`excludeDomains` (bare hostnames, max 20 each), `scrapeOptions: {format:"markdown"|"json"}` (presence triggers deep-scrape). |
| 170 | |
| 171 | **`/map`**: `url`, `limit` (1–100000, default 5000), `search` (keyword rank filter), `sitemapMode` (`include`/`only`/`skip`), `includeSubdomains` (default true), `includePaths`/`excludePaths` (regex), `ignoreQueryParameters` (default **true**). |
| 172 | |
| 173 | **`/crawl`**: `url`, `limit` (1–10000, default 100), `maxDepth` (1–10), `includePaths`/`excludePaths`, `allowSubdomains`, `sitemapMode`, `ignoreQueryParameters` (default **false** — opposite of /map), `crawlEntireDomain`. |
| 174 | |
| 175 | **`GET /crawl/{job_id}`**: query params `skip`, `limit` (1–1000). `data` is an array of page-scrape blobs. `meta.total` is the running total; compare against `data.length` to detect completion. |
| 176 | |
| 177 | ## Tips |
| 178 | |
| 179 | **🚨 Cloudflare 1010 trap (Python / non-curl HTTP clients).** The Cloudflare edge in front of `api.zoodata.ai` blocks default Python `Python-urllib/X.Y` User-Agent → HTTP 403. ALWAYS set an explicit `User-Agent` header (any non-default value works, e.g. `User-Agent: web-extract-skill/1.0`). curl is fine out of the box; Python / Go / Node fetch / requests all need it. |
| 180 | **Crawl `GET /crawl/{job_id}` warmup gotcha**: for ~5–10 seconds after `POST /crawl` returns the job_id, polling may return `NOT_FOUND`. This is the job still registering, not a real "not found." Tolerate it: retry with 5s sleep up to ~3 times before giving up. |
| 181 | **`crawl-status` response shape**: `data` is an **object** `{id, status, completed, total, data: [pages...]}` — the page array is nested at `data.data`. Status values: `"queued"`, `"running"`, `"completed"`, `"failed"`. (Failed `success:false` responses cost 0 credits.) |
| 182 | **A `success:true` response can still contain an error page.** Always check `data.meta.statusCode` (or per-result `meta.statusCode` for search) before trusting content. |
| 183 | **Deep-scrape `/search` may return fewer than `limit`** results — pages with no extractable content in the chosen format are dropped. |
| 184 | **Domain filters are bare hostnames only.** `https://github.com` or `github.com/path` → 422. Use `github.com`. |
| 185 | **`includePaths` / `excludePaths` are regex, not glob.** Use `/blog/.*` not `/blog/**`. |
| 186 | **Crawl job IDs are tenant-scoped.** Polling someone else's job returns 404 (indistinguishable from "job not found" — by design). |
| 187 | **Refused requests (`ACCESS_DENIED`, `RATE_LIMITED`) are NOT billed.** `TIMEOUT` and `CONTENT_UNAVAILABLE` are. |
| 188 | **Polling costs 1 credit per call.** For large crawls, batch your polls — fetch with `limit=1000` and space polls ≥10s apart. |
| 189 | **Format choice (skill default = JSON)**: `json` is the default — gives structured fields (`title/summary/sections/key_metrics/outgoing_links/page_type/...`) that downstream tools can consume directly. Use `markdown` only when the user asks for clean prose or the page is article-shaped (blog post / docs); use `rawHtml` only when DOM access is needed (custom selectors, table parsing the JSON layer doesn't expose). |
| 190 | |
| 191 | ## On Missing Key (no credentials configured) |
| 192 | |
| 193 | **BEFORE calling any endpoint**, verify a credential is configured. Reliable check: `python {skill_base_dir}/scripts/webtools.py check` — exits 2 if no key is found in env (`ZOODATA_API_KEY`) or the config file (`~/.zoodata/config.json`). A `[ -z "$ZOODATA_API_KEY" ]` test alone is NOT sufficient. |
| 194 | |
| 195 | When no key is found through any mechanism: |
| 196 | |
| 197 | **STOP.** Do not call any endpoint. |
| 198 | **Do NOT fall back to "partial output from training data" / "describe the page from common knowledge" / "for reference only" preview.** This skill's deliverable is structured extraction of THIS URL's actual content — without the data, there is no deliverable. |
| 199 | **Tell the user, in their language**, all three of: |
| 200 | "`ZOODATA_API_KEY` is not set — I need this to fetch the page." |
| 201 | **Get a free key** (1,000 credits, no credit card): https://zoodata.ai/en/api-keys |
| 202 | **Configure** via one of: |
| 203 | `export ZOODATA_API_KEY='hms_live_xxx'` (session only) |
| 204 | `mkdir -p ~/.zoodata && chmod 700 ~/.zoodata && (umask 077; echo '{"api_key":"hms_live_xxx"}' > ~/.zoodata/config.json)` (persistent; keep the file private — 0600) |
| 205 | |
| 206 | ## On 401 Invalid Key |
| 207 | |
| 208 | When any endpoint returns HTTP 401: |
| 209 | |
| 210 | **STOP further calls immediately.** A rejected key won't be accepted on retry — every subsequent call will return 401 too. |
| 211 | **Tell the user**: their `ZOODATA_API_KEY` was rejected (invalid, revoked, or expired). Direct them to [zoodata.ai/en/api-keys]. |
| 212 | If you collected partial output before failure, show it and mark partial. **Do not fabricate** the rest — no "training-data fallback" / "page description from common knowledge" substitution. |
| 213 | |
| 214 | ## On 402 Credit Exhausted |
| 215 | |
| 216 | When any endpoint returns HTTP 402: |
| 217 | |
| 218 | **STOP further calls.** |
| 219 | Report partial findings already gathered, plus the `creditsRemaining` number from the last successful call. |
| 220 | Point the user at [zoodata.ai/en/pricing] to top up. **Do not fabricate** the rest — no "common-sense page summary" filler in place of a real fetch. |
| 221 | |
| 222 | ## See also |
| 223 | |
| 224 | [`zoodata`] — commerce endpoints (Amazon products / markets / reviews / brands). Pair with webtools when you need to validate Amazon findings against the open web (competitor sites, news, off-Amazon reviews). |
| 225 | [`amazon-market-analysis`] — uses the commerce side; webtools complements it for non-Amazon channel research. |
| 226 |
Discussion
Browse more free Claude skills or everything in Operations.