Skip to main content
The Spidra Python SDK searches the web, scrapes pages, batch-processes URLs, and crawls entire sites. It wraps the Spidra API so you’re not writing raw HTTP calls and polling loops yourself, handling job submission, status polling, retry logic, and error mapping to typed exceptions.

Installation

Requires Python 3.9 or higher.
Get your API key from app.spidra.io under API Keys. Store it as an environment variable. Never hardcode it.

Getting started

Create a client with your API key:
Name the instance spidra, client, or whatever fits your codebase. The method names stay the same.
If you’re inside an existing async context (FastAPI, asyncio, Jupyter notebook), use AsyncSpidra instead and await the calls. The method signatures are identical.

Searching

Search runs a real query and returns structured results: titles, links, descriptions, thumbnails. It’s the same data you’d get scraping a search engine yourself, minus the scraping. Unlike scrape/batch/crawl, a plain search usually resolves in a few seconds, often within the single search() call below with no polling at all.
By default only web results come back. Request more with sources:
If a source is temporarily unavailable, that source comes back as an empty list rather than failing the whole request.

Search parameters

Research and developer sources

research searches academic papers, and returns authors and doi alongside the usual fields:
developer searches GitHub issues and pull requests. Write the query the way you’d type it into GitHub’s own search, qualifiers and all:
include_domains, exclude_domains, and filetype only apply to web/news/images/videos. They’re not supported on research or developer.

Pagination and time range

Pass the previous response’s next_page_tokens back as page_tokens to get the next page of a specific source:
time_range narrows results to a recency window, useful for sources like news where older results matter less:

Domain filtering

Restrict web results to specific domains, or keep specific domains out. Pass one or the other, never both.

Scrape content from results

Add scrape_options to also fetch each web result’s actual page content in the same call. No second request, no separate job to poll.
This makes the search take as long as its slowest scraped page, not the usual few seconds. Real scraping isn’t instant, and Spidra would rather you wait once on one job than build your own polling loop around N separate scrape jobs. max_results caps how many top-ranked web results get scraped, omit it to scrape all of them, up to a cap of 10. A result that fails to scrape or falls outside the time budget is simply left without markdown, not treated as an error.
Need AI extraction, a schema, or a screenshot instead of plain markdown? Scrape that specific URL directly with spidra.scrape().

Submitting without waiting

If you’d rather not wait on the call at all, submit the search and check on it yourself whenever you’re ready. Unlike search(), this never tries to resolve inline, it always returns a job_id immediately:

Scraping

The scraper accepts up to three URLs per request and processes them in parallel. You can pass a URL string directly, or a ScrapeParams object for full control. The simplest call:
If you’d rather fire and move on, start_scrape() returns a job ID immediately. You can then call get_scrape() whenever you’re ready to check:
Job statuses move through: queued → waiting → active → completed (or failed).

Scrape parameters

Enforcing an exact output shape

Without a schema the AI extracts what it finds. With a schema, missing fields come back as None rather than guessed values, which matters when the output feeds a database or a typed pipeline downstream:
Use the Spidra JSON Schema Generator to build and preview your schema visually before pasting it here.
Define every field you want extracted. An untyped object with no properties (or an array of them) gives the AI nothing to fill in, so those members come back empty.
If your schema uses a keyword the API doesn’t support (e.g. anyOf, $ref), the response includes schema_warnings listing what was ignored. The rest of the schema still applies.

Enforcing shape with Pydantic

If you already model your data with Pydantic, skip the JSON Schema entirely. Pass the model itself (class or instance) and the SDK converts it for you:
The same works on batch_scrape() and crawl() (applied per page). Pydantic stays optional, install it with pip install spidra[pydantic] only if you use this. Both v2 and v1 models are supported.

Scraping geo-restricted content

Some sites serve different prices or content depending on where you’re browsing from. Set use_proxy=True and a proxy_country code to route through a residential IP in that country:
Supported country codes include us, gb, de, fr, jp, au, ca, br, in, nl, and 40+ more. Use "global" or "eu" for regional routing without pinning to a specific country.

Scraping pages behind a login

If the page requires a session, pass your cookies as a raw header string. The easiest way to get this is to log in through your browser, open devtools, and copy the Cookie header from any authenticated request:

Browser actions

Sometimes you need to interact with the page before extraction: dismiss a cookie banner, type into a search box, scroll to load lazy content. Pass an actions list inside the ScrapeUrl and they run in order before the AI sees the page:
For selector you can pass a CSS selector or XPath. If you’d rather describe the element in plain English, use value and Spidra will locate it with AI.

Controlling how long scrape() waits

By default scrape() polls every 3 seconds and waits until the job finishes, however long that takes. If you’d rather cap the wait, pass a timeout in seconds. When it fires, SpidraTimeoutError is raised and the job keeps running server-side, so you can check it later with get_scrape() or cancel it:
Transient hiccups mid-wait (a 502 blip, a dropped connection, a rate limit) don’t kill the wait; the SDK keeps polling unless several happen in a row. The same options work on batch_scrape() and crawl().

Batch scraping

When you have a list of URLs to process, batch is the right tool. You can submit up to 50 URLs in a single request and they all run in parallel.
Each item moves through pending → running → completed (or failed). If you don’t want to wait for the whole thing to finish, use start_batch_scrape() and get_batch_scrape() separately:

Retrying failures and cancelling

If some URLs fail, re-queue just the failed items instead of resubmitting the whole batch. Cancelling stops everything still pending and refunds the credits for it:
To look through past batches:

Bulk search: one template, many pages

Batch scraping needs you to list every URL. Bulk search is the shortcut: give one URL template and lists of values, for example every job title in every city, and the SDK builds the URLs, runs them in batches of 40, merges the results and removes duplicates. It is built on batch scraping, so there is no new endpoint. It works with any site, not only job boards: anything you can write as a URL with a few changing parts, such as product searches by keyword, listings by city, or one page per category. Each page costs 1 credit, plus AI tokens if you set a prompt or schema. Call estimate_bulk first to see the cost before you spend anything.
For any other site, describe the items with a schema and point items_path at the list in it:
Parameters Every other batch_scrape option, such as output, prompt, schema and use_proxy, works here too and applies to every page. The async client has the same methods: await client.bulk_scrape(...).
Bulk search only runs the pages you ask for, it does not follow “next page” links. Some sites, Indeed for one, show only the first page of a search to visitors who are not signed in. For those, depth comes from many narrow searches, which is what bulk search is for.

Crawling

Crawling is different from scraping. You give it a starting URL and it discovers and processes pages on its own, following links according to your instructions. Good for indexing a docs site, monitoring a competitor’s blog, or building a structured dataset from an entire section of a site.
crawl_instruction tells the crawler which links to follow. transform_instruction tells the AI what to extract from each page. By default the call waits until the crawl finishes. Pass timeout=<seconds> to bound the wait (the job keeps running server-side if it fires).

Raw content mode

Omit both transform_instruction and schema to get the raw page content without any AI processing. Each page’s data field contains the plain markdown of that page, no token credits are charged:
This is useful when you want to feed the content into your own AI pipeline.

Structured output with schema

When you need every page to return the same fields in the same format, use schema. The AI returns JSON matching it exactly for every page:
Use the Spidra JSON Schema Generator to build and preview your schema visually before pasting it here.

Scoped crawling with path filters

Use include_paths and exclude_paths to keep crawls focused on the content you actually need. Both accept glob-style patterns:

Crawl parameters

Each returned page has depth (clicks from base_url) and parent_url (the page it was found on).

Submitting without waiting

Just like scraping, you can fire-and-forget with start_crawl() and poll with get_crawl():

Cancelling a crawl

Cancel a queued or running job at any time. Pages already processed are kept:

Downloading the raw HTML and Markdown

Once a crawl completes, crawl_pages() returns signed download URLs for the raw HTML and Markdown of every page. These links expire after one hour:

Re-extracting with a different prompt

If you crawled a site and want to pull out different information, you don’t have to re-crawl. crawl_extract() runs a new AI pass over the already-crawled content and charges only transformation credits:

Browsing your crawl history

Look back through crawls you’ve already run, or check your total count for the account:

Get full job details

A flat snapshot of a crawl job’s config and counters, the same data crawl_history() returns per row, for one job.

Retry one page’s transformation

If a single page’s extraction failed, or you want to pull it again with a fresh AI pass, retry just that page instead of re-running the whole crawl. Charges credits for that page’s transformation only.

Download results as a zip

To get the raw files instead of working through the API, download everything a crawl produced as one zip:
include defaults to all three (html, markdown, data) if you omit it.

Why a page was skipped

The crawl report says how Spidra understood the instruction, how many pages it read to find the ones it returned (free), and every link it didn’t follow, with the reason and the page the link was on:
See GET /crawl/{jobId}/report for every reason.

Parsing documents

parse() turns a PDF, Word, Excel, PowerPoint or other document into Markdown page by page. Pass a file path, a link, bytes (with file_name), an open file, or file_id=. It waits for the result: short documents answer at once and longer ones are polled. 1 credit per page.
Options: mode ("auto", "fast", "ocr"), pages, max_pages, include_pages, page_markers, prompt and schema. A document that can’t be parsed raises SpidraJobFailedError. Also: estimate_parse() (pages and credits, no charge), parse_batch(), upload_for_parse() for files up to 500 MB, start_parse()/get_parse() and delete_parse_file(). See Parse.

Mapping a site

map() lists a site’s page URLs without scraping them, from its sitemap and the links on its pages. It answers in the same request for 1 credit, and is free when nothing is found.
Options: search, limit (default 500, up to 5,000), sitemap ("include", "only" or "skip"), include_subdomains, ignore_query_params, respect_robots and cookies. See POST /map.

Watching jobs (streaming results)

A 50-page crawl can take a while. Instead of waiting for the whole thing, watch_crawl() yields each page the moment it’s crawled, perfect for writing results to a database as they arrive or updating a progress bar:
Batches work the same way. watch_batch() yields each item as it finishes (completed or failed):
On AsyncSpidra these are async generators, same names, just async for. Every page/item is yielded exactly once, including ones that finished before you started watching, and page content is only re-fetched when the crawl actually makes progress, so watching stays cheap. The loop ends when the job completes or is cancelled, raises SpidraJobFailedError if it fails, and breaking out early never cancels the job, use cancel_crawl() for that.

Logs

Every scrape request your API key makes gets logged automatically. You can filter by status, URL, date range, or where it came from:
To fetch the full details of a single log entry including the AI extraction output:

Usage statistics

Check how many requests and credits your account has used over a given period:

Retries and reliability

You don’t have to write retry loops. Transient failures (network blips, 502/503/504 gateway errors) are retried automatically with exponential backoff, so a single hiccup never fails your call. Both knobs are configurable:
The retry policy is designed so it can never double-charge you: 4xx client errors are never retried, and job submissions are only retried when the server explicitly rejected them. Never on network errors or gateway timeouts, where the job may already have been queued. When the server sends a Retry-After hint, the SDK honors it instead of its own backoff.

Error handling

Every API error is mapped to a typed exception class, so you can catch exactly what you care about and let the rest bubble up:
All exceptions expose .status (the HTTP status code, or 0 for non-HTTP errors like job failures and timeouts) and .message. API errors also carry .code (a machine-readable identifier like SERVICE_BUSY) and .details (the raw error body).

Verifying webhooks

Crawl jobs can push crawl.page, crawl.completed, and crawl.failed events to your webhook_url. Spidra signs each delivery with HMAC-SHA256 in the X-Spidra-Signature header, and the SDK ships a helper so you never accept a forged event:
The helper uses only the standard library and compares in constant time. Always pass the raw request body. Re-serialising parsed JSON produces different bytes and fails verification.

Ruby

Official Ruby SDK, pure stdlib, no external dependencies. Works in Rails, Sinatra, and scripts.

Elixir

Official Elixir SDK with idiomatic pattern matching, OTP-ready, works with Phoenix and plain Mix projects.