# Extract structured JSON from web pages

> Turn web pages and PDFs into JSON with /v1/extract: send a JSON Schema and a prompt, get each page's data back. Classification, summaries, pricing and failures.

[`/v1/extract`](https://serpkite.com/docs/endpoints/extract) reads up to 20 URLs (HTML pages or PDFs) per call. Add `output_schema` and each page also comes back as JSON that matches your [JSON Schema](https://json-schema.org), written by Claude from the page's content. No per-site parser, no HTML to maintain.

## Extract with a schema

Describe the fields you want. An optional `prompt` (up to 4,096 characters) says what to pick and how to normalise it.

cURL:

```bash
curl https://api.serpkite.com/v1/extract \
  -H "Authorization: Bearer $SERPKITE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"urls":["https://www.example.com/pricing"],"output_schema":{"type":"object","properties":{"plans":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"price_per_month":{"type":["number","null"]},"seats":{"type":["integer","null"]}}}},"free_trial":{"type":"boolean"}}},"prompt":"Monthly prices in USD; null when a plan has no list price."}'
```

TypeScript:

```ts
import { SerpKite } from "serpkite";

const sk = new SerpKite(); // reads SERPKITE_API_KEY
const res = await sk.extract({ urls: ["https://www.example.com/pricing"], output_schema: {"type":"object","properties":{"plans":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"price_per_month":{"type":["number","null"]},"seats":{"type":["integer","null"]}}}},"free_trial":{"type":"boolean"}}}, prompt: "Monthly prices in USD; null when a plan has no list price." });
console.log(res.results.map((r) => r.output), res.failed.length);
```

Each entry in `results` keeps its usual fields (`url`, `title`, `markdown`…) and gains `output`:

```json
{
  "url": "https://www.example.com/pricing",
  "title": "Pricing · Example",
  "output": {
    "plans": [
      { "name": "Starter", "price_per_month": 19, "seats": 1 },
      { "name": "Team", "price_per_month": 79, "seats": 5 },
      { "name": "Enterprise", "price_per_month": null, "seats": null }
    ],
    "free_trial": true
  }
}
```

Values come only from the page. Make fields nullable (`"type": ["number", "null"]`) and say "null when missing" in the prompt, so a field the page doesn't have comes back as `null` instead of a guess.

## Classification schemas

A schema whose fields are all booleans or enums is a classification ("is this a pricing page?", "which plan tier is this?"). Those go to OpenAI's Decisions API instead of Claude, which always answers with values the schema allows.

```json
{
  "urls": ["https://www.example.com/blog/launch", "https://www.example.com/pricing"],
  "output_schema": {
    "type": "object",
    "properties": {
      "page_type": { "enum": ["pricing", "docs", "blog", "login", "other"] },
      "mentions_free_tier": { "type": "boolean" }
    }
  }
}
```

## Summaries

`summary: true` adds a 3–6 sentence summary of each page in `results[].summary`, focused on `query` when you send one. It can be combined with `output_schema` and with `highlights`.

## Pricing and failures

| What | Credits per page |
| --- | --- |
| Reading the page | 1 (0.5 from cache) |
| `output_schema` | +4 |
| `summary: true` | +4 |

- A URL that can't be read is listed in `failed` with a reason and costs nothing. So is a short page that turns out to be a bot check, a login wall, a soft 404 or an empty app shell.
- An extraction or summary that fails is reported in `results[].output_error` or `summary_error` and not charged; you pay only for the page.
- With `summary` or `output_schema`, `timeout` defaults to 75 seconds and the last 25 are kept for the model. `meta.llm` reports the provider, model and tokens.

## Extract from a whole site

Combine extraction with the [site endpoints](https://serpkite.com/docs/guides/site-ingestion): list the URLs with [`/v1/map`](https://serpkite.com/docs/endpoints/map) (ranked by a search phrase), then send the ones you need to `/v1/extract` with your schema, 20 at a time.

```python
from serpkite import SerpKite

sk = SerpKite()
urls = [u.url for u in sk.map("https://www.example.com/", search="pricing plans", limit=20).results]
```

Then post those URLs with your `output_schema` as above. The remote [MCP server](https://serpkite.com/docs/mcp)'s `extract` tool takes `output_schema`, `prompt` and `summary` too.

## Privacy

Page text and your prompt are sent to the model provider (Anthropic, or OpenAI for classification schemas) under API terms that exclude training. SerpKite never logs them, and extractions are not cached. See [Privacy and data retention](https://serpkite.com/docs/privacy-and-data-retention#ai-features).

## Related

- [Extract reference](https://serpkite.com/docs/endpoints/extract): Every parameter, field and failure code.
- [Site ingestion](https://serpkite.com/docs/guides/site-ingestion): Map, extract and crawl a site.
- [Cited answers](https://serpkite.com/docs/guides/cited-answers): Answers with numbered sources from live results.
- [AI pricing](https://serpkite.com/docs/credits-and-billing#ai-features): What summaries and extractions cost.