LangChain document loader and tool
Load web pages as LangChain Documents with SpicrawlLoader and let an agent read a URL with SpicrawlScrapeTool, using the open-source Python and JavaScript packages.
Two open-source packages add Spicrawl to LangChain, with the same classes in each language:
| Class | Use it to |
|---|---|
SpicrawlLoader | Turn URLs into Documents (markdown content plus metadata) for RAG and indexing. |
SpicrawlScrapeTool | Let an agent open a URL and read it. The tool is named spicrawl_scrape. |
Both call POST /v1/scrape with response_format set to markdown, under your API key, with the same scopes, limits and credits as a direct API call.
Install from GitHub
The packages are not on PyPI or npm yet. Install them from their GitHub repos, as below.
To give an agent all 24 spicrawl_* tools instead of one scrape tool, connect the hosted MCP server: see LangChain and LangGraph.
Install
Create an API key at app.spicrawl.com and export it. Both packages read SPICRAWL_API_KEY.
export SPICRAWL_API_KEY="spicrawl_live_..."Python 3.10 or later, with langchain-core 1.x.
pip install "git+https://github.com/OfficialSpicrawl/langchain-spicrawl.git"The agent example below also needs a model package:
pip install "langchain[openai]"Import from langchain_spicrawl.
Load pages as documents
SpicrawlLoader takes one URL or a list. It scrapes them one after the other, in order, and returns one Document per page.
from langchain_spicrawl import SpicrawlLoader
loader = SpicrawlLoader(["https://example.com", "https://example.org"])
docs = loader.load()
print(docs[0].page_content)
print(docs[0].metadata)
# {'source': 'https://example.com', 'final_url': 'https://example.com/',
# 'target_status': 200, 'credits_charged': 1, 'engine': 'fetch',
# 'cache_state': 'hit', 'request_id': '01M4QYNXGPFF9VRGS64F0JE092'}Python also has lazy_load(), aload() and alazy_load(). TypeScript has lazyLoad(), an async generator. Pass the documents to any text splitter and vector store, as with other LangChain loaders.
A page that needs JavaScript, or any other POST /v1/scrape field, goes in scrape_options (scrapeOptions in TypeScript). wait_for needs js_render.
loader = SpicrawlLoader(
"https://example.com/pricing",
scrape_options={"js_render": True, "wait_for": ".plans", "exclude_tags": ["nav", "footer"]},
continue_on_failure=True, # log a warning and skip a URL that fails
)Loader parameters
The first argument is urls, required. The rest are keyword arguments in Python and the second argument's object fields in TypeScript.
| Python | TypeScript | Default | Meaning |
|---|---|---|---|
api_key | apiKey | $SPICRAWL_API_KEY | Spicrawl API key. Without one the constructor raises. |
base_url | baseUrl | $SPICRAWL_BASE_URL, else https://api.spicrawl.com | API base URL. |
scrape_options | scrapeOptions | {} | Extra POST /v1/scrape fields sent with every URL. response_format is always markdown. |
continue_on_failure | continueOnFailure | False / false | Log a warning and skip a failed URL instead of raising. |
timeout | timeout | 120 seconds / 120000 milliseconds | Wait for each response. |
max_retries | maxRetries | 2 | Retries of errors the API marks retryable. |
Metadata
Both packages set the same keys, so documents from either can share one index. A key is left out when the API returned no value for it; title is left out when the page has no # heading.
| Key | Value |
|---|---|
source | The URL you passed. |
title | The first # heading in the markdown. |
final_url | The URL after redirects. |
target_status | The site's HTTP status. |
credits_charged | Credits this page cost. |
engine | The rendering tier that served the page: fetch, js or browser. |
cache_state | hit, miss or bypass. |
request_id | Quote it to Spicrawl support, or look it up with GET /v1/requests/{id}. |
Give an agent the scrape tool
SpicrawlScrapeTool takes no required arguments. The model sees two: url, and js_render, which it sets only when the first attempt came back empty because the page is built by JavaScript. Everything else is fixed by you when you create the tool. Set OPENAI_API_KEY for the model in these examples.
from langchain.agents import create_agent
from langchain_spicrawl import SpicrawlScrapeTool
tool = SpicrawlScrapeTool(max_chars=20_000)
agent = create_agent("openai:gpt-4.1", tools=[tool])
result = agent.invoke(
{"messages": [{"role": "user", "content": "Read https://example.com/pricing and list the plans."}]}
)
print(result["messages"][-1].content)Call the tool without an agent:
tool.invoke({"url": "https://example.com"})Tool parameters
| Python | TypeScript | Default | Meaning |
|---|---|---|---|
api_key | apiKey | $SPICRAWL_API_KEY | Spicrawl API key. |
base_url | baseUrl | $SPICRAWL_BASE_URL, else https://api.spicrawl.com | API base URL. |
scrape_options | scrapeOptions | {} | Extra POST /v1/scrape fields for every call. response_format is always markdown. |
max_chars | maxChars | 30000 | The page is cut to this many characters before it reaches the model, followed by [truncated: showing N of M characters]. |
timeout | timeout | 120 seconds / 120000 milliseconds | Wait for each response. |
max_retries | maxRetries | 2 | Retries of errors the API marks retryable. |
A page that comes back empty returns a message telling the model to call again with js_render set to true. The second call costs 3 credits.
Errors
A blocked target is an error, not content. When the site answers 4xx or 5xx (an access-denied page, a 404), the packages raise SpicrawlError with code set to TARGET_STATUS. The API itself returns such a page with HTTP 200 and an X-Target-Status header (see Response headers); the packages check it so a block page never reaches your index or your model.
Every SpicrawlError carries these fields:
| Python | TypeScript | Meaning |
|---|---|---|
code | code | The API's error code, such as ERR::LIMIT::RATE_LIMITED or ERR::LIMIT::QUOTA_EXCEEDED, or one of TARGET_STATUS (the site answered 4xx/5xx) and CONNECTION (no response). All API codes are in Errors. |
status | status | The HTTP status of the Spicrawl response. |
retryable | retryable | Whether the API marked the error as safe to retry. |
retry_after | retryAfter | Seconds the API asked you to wait, if any. |
request_id | requestId | Quote it to Spicrawl support. |
from langchain_spicrawl import SpicrawlError, SpicrawlLoader
try:
docs = SpicrawlLoader("https://example.com/pricing").load()
except SpicrawlError as err:
print(err.code, err.status, err.request_id)- The loader raises on the first failure. With
continue_on_failure(continueOnFailure) it logs a warning and skips that URL, so the result can hold fewer documents than the URLs you passed. - The tool in Python returns the error text to the model as the tool output, for example
ERR::LIMIT::QUOTA_EXCEEDED: ..., so the agent can react instead of ending the run. In TypeScript the tool throwsSpicrawlError, andcreateAgentpasses the message back to the model. - Retries. Only errors the API marks
retryableare retried, up tomax_retriestimes, after theRetry-Afterwait, or a short backoff when there is none. A wait longer than 30 seconds is raised instead. A dropped connection is never retried, because the scrape may already have run and been billed.
Credits
| Request | Credits |
|---|---|
| Plain fetch (default) | 1 |
js_render | 3 |
| Any failure, including a blocked target | 0 |
A cache hit (cache_state of hit) is billed at the price of the fetch that stored it, not for free. credits_charged in each document's metadata is what that page cost, and can differ from the table when a request is served by a stronger rendering tier. See Credits for the full price table.
Add up a load:
total = sum(d.metadata.get("credits_charged", 0) for d in docs)The loader fetches one URL at a time. For thousands of URLs, use the batch API instead.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
A missing-API-key error when you create a loader or tool (No API key: pass api_key=... or set the SPICRAWL_API_KEY environment variable. from the loader) | The key is not set in the process that runs your code. | Export SPICRAWL_API_KEY, or pass api_key (apiKey). |
Cannot find module '@spicrawl/langchain' or a missing dist/ file after the TypeScript install | The clone was not built, or you installed the folder instead of the tarball. | Run npm ci && npm run build && npm pack in the clone, then install the .tgz. |
SpicrawlError with code TARGET_STATUS | The site answered 4xx or 5xx. This is a block or a missing page, not content. | The message names the site's status. A 404 or 410 means the URL is wrong; for a 403 or a challenge page see Anti-bot. |
ERR::LIMIT::QUOTA_EXCEEDED | The monthly credit allowance is used up. It is not retryable. | Wait for the reset date in the error, or raise the limit. See Credits. |
| The loader returns fewer documents than URLs | continue_on_failure skipped failed URLs. | Read the logged warnings, or turn the option off to see the error. |
The tool reports returned no readable content | The page is built by JavaScript. | Let the model call again with js_render true, or set js_render in scrape_options for every call (3 credits each). |
Source
- Python: github.com/OfficialSpicrawl/langchain-spicrawl (MIT)
- JavaScript and TypeScript: github.com/OfficialSpicrawl/langchain-spicrawl-js (MIT)
Related: Agent frameworks for the MCP route, Best practices for agents, Credits, Errors.
Dify
Use Spicrawl in Dify workflows and agents: install the Spicrawl plugin from its .difypkg file, authorize it with your API key, and call the Scrape, Extract and Batch scrape tools.
Agent frameworks
Give an agent built with the OpenAI Agents SDK, Vercel AI SDK, LangChain, CrewAI, Google ADK, Mastra or smolagents the 24 spicrawl_* tools by connecting to the hosted Spicrawl MCP server.