Schema extraction

Turn any page into structured JSON — a working request, the options that matter, the response, what it costs, and the errors to handle.

For AI agents: https://bytekit.com/llms.txt — every docs page is available as markdown by appending .md

POST /v1/schema fetches a URL and returns data from it as JSON that follows a JSON Schema you provide. Send a url and a schema describing the fields you want, such as a product name and price, and you get back an object with those fields filled in from the page. An optional prompt tells the extraction what to look for.

The call is synchronous: it returns 200 with the result in the response body. There is no async mode, and a field this endpoint does not know is rejected (see Errors).

Run an extraction

curl -X POST https://api.bytekit.com/v1/schema \
  -H "Authorization: Bearer sk_live_YOUR_KEY_HERE" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://books.toscrape.com/catalogue/a-light-in-the-attic_1000/index.html",
    "schema": {
      "type": "object",
      "properties": {
        "title": { "type": "string" },
        "price": { "type": "number" },
        "in_stock": { "type": "boolean" }
      }
    },
    "prompt": "Extract the book title, its price as a number, and whether it is in stock."
  }'
{
  "data": {
    "title": "A Light in the Attic",
    "price": 51.77,
    "in_stock": true
  },
  "model": "openai/gpt-oss-20b",
  "validated": true,
  "metrics": {
    "scrape_ms": 1006,
    "llm_total_ms": 257,
    "input_tokens": 977,
    "output_tokens": 91,
    "retries": 0,
    "total_ms": 1502
  },
  "scrape": {
    "url": "https://books.toscrape.com/catalogue/a-light-in-the-attic_1000/index.html",
    "status_code": 200,
    "content_length": 2303,
    "truncated": false
  },
  "warnings": []
}

This request was run for real and returned 200. The response is printed as it came back, reformatted for reading. The page is a public practice shop for scraping; the timings and token counts will differ on your calls.

Options

url and schema are required. Send any of the others alongside them.

OptionDefaultDescription
url—The page to extract from. An http or https URL
schema—A JSON Schema describing the output (see Writing the schema)
promptnoneA hint in plain language about what to extract, up to 2000 characters
modelopenai/gpt-oss-20bThe extraction model: openai/gpt-oss-20b or openai/gpt-oss-120b. Any other value is rejected with 400 unsupported_model
countryUSTwo-letter country code that sets where the page is fetched from, upper-cased for you. null is treated as US
languagenullA string or null. The API accepts it, but the extraction does not use it today
wait_untilloadload or domcontentloaded. networkidle is rejected (see Rendering options)
wait_for_selectornullMust stay unset or null (see Rendering options)
delay_ms0Must stay 0 (see Rendering options). A value outside 0–10000 is a 422 validation_error

Rendering options

/v1/schema reads the page the quick way, without waiting on a browser to render it. Options that ask for that waiting are therefore refused with 400 unsupported_option: wait_until set to networkidle, a wait_for_selector, or a delay_ms above 0. The error message lists the options this endpoint does not support; it does not say which of yours triggered it.

{
  "status": "failed",
  "error": {
    "code": "unsupported_option",
    "message": "Schema extraction does not support slow-path options (wait_for_selector, wait_until=networkidle, cookies, delay_ms, custom headers).",
    "http_status": 400,
    "failed_at": "2026-10-09T23:17:40.806Z"
  }
}

This response came from a real request with "wait_until": "networkidle".

Caller-supplied cookies and headers are no longer accepted: sending either returns 400 validation_error.

Writing the schema

The schema is a JSON Schema (draft 2020-12) object. To keep the output reliable, ByteKit accepts a deliberately small part of the language:

  • The root must be an object schema: "type": "object" with its properties.
  • These keywords are accepted: type, properties, required, additionalProperties, items, enum, $defs, $ref, description, title and default. A $ref can only point into your own #/$defs.
  • Other keywords are rejected with 422 schema_incompatible, and error.details.construct names the one found. They include pattern, format, minimum, maxLength, oneOf, anyOf, allOf, not and if/then/else.
  • The schema can be at most 50,000 characters once serialized, and nested properties can go at most 8 levels deep (the count restarts inside items, and $defs entries are not counted). A schema over either limit, or one that is not valid JSON Schema, returns 422 validation_error.
{
  "status": "failed",
  "error": {
    "code": "schema_incompatible",
    "message": "schema_incompatible: unsupported construct \"pattern\" — unsupported keyword \"pattern\" found at #/properties/title",
    "http_status": 422,
    "failed_at": "2026-10-09T23:17:43.923Z",
    "details": {
      "construct": "pattern"
    }
  }
}

This response came from a real request whose schema used pattern.

Every property you declare is asked for. What a missing value looks like depends on the property's type: a string, number, integer, boolean or object property the page does not contain usually comes back as null, and a list (array) property usually comes back as an empty array, []. Asking the book page above for an isbn it does not show returned "isbn": null. Asking it for a title and a list of literary awards, declared as an array of string items, returned "awards": []. Both responses had validated set to true.

A property limited by enum, such as {"type": "string", "enum": ["hardcover", "paperback"]}, comes back as null when the page holds none of the listed values. The type is optional: an enum with no type behaves the same way. Your enum does not need to list null. Asking the book page above for that format next to a title returned "format": null with validated set to true; asked for on its own, the same request returned 200 with validated set to false and an empty_extraction warning, which charges no credits. A model can also answer with one of the listed values that the page does not support, so treat an enum result as a guess when the page is silent. An integer property with "enum": [1, 2] returned 2 on a page that states no edition.

One kind of schema can instead fail with 503 provider_error when the page has no value to give: a schema whose only properties are lists of strings, such as {"type": "object", "properties": {"awards": {"type": "array", "items": {"type": "string"}}}}, when the page has no entries. The same schema returned "awards": [] on one call and 503 provider_error on others, so it does not fail every time. The title and awards request above, which asks for a title as well, returned [].

This failure can repeat on retry, and it keeps the page's bandwidth charge (see Cost), so change the schema instead of retrying the same request.

Lists work through items. This schema collects every book on a category page:

curl -X POST https://api.bytekit.com/v1/schema \
  -H "Authorization: Bearer sk_live_YOUR_KEY_HERE" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://books.toscrape.com/catalogue/category/books/poetry_23/index.html",
    "schema": {
      "type": "object",
      "properties": {
        "category": { "type": "string" },
        "books": {
          "type": "array",
          "items": {
            "type": "object",
            "properties": {
              "title": { "type": "string" },
              "price": { "type": "number" }
            }
          }
        }
      }
    },
    "prompt": "List every book on the page with its price as a number."
  }'

This request was run for real and returned 200 with "validated": true and 19 books in data.books. Its scrape.content_length was 4521. The first two books were: {"title": "A Light in the Attic", "price": 51.77} and {"title": "The Black Maria", "price": 52.15}.

Response

FieldDescription
dataThe extracted object, shaped by your schema. null when validated is false
modelThe model that produced the result
validatedtrue when the output matched your schema, unless it holds values and all of them are null. See below for when this is false
metricsTimings in milliseconds (scrape_ms, llm_total_ms, total_ms), the token counts (input_tokens, output_tokens) and retries, the number of extra attempts (0 to 2)
scrapeThe page that was read: its url, the status_code it answered with (or null), content_length (its size in bytes, which the bandwidth charge uses) and truncated
warningsCodes for results that are less complete than a clean run. An empty array on a clean run

A 200 does not always mean you got data. validated is false, with data: null, when the model refused, when its retries ran out before it produced output that matched your schema, or when the output holds values and every one of them is null. That last case is still true if your schema allows it: each null value has to sit at a property whose type is a list that includes "null", for example ["string", "null"]. Check validated before you use data.

{
  "data": null,
  "model": "openai/gpt-oss-20b",
  "validated": false,
  "metrics": {
    "scrape_ms": 1455,
    "llm_total_ms": 220,
    "input_tokens": 303,
    "output_tokens": 102,
    "retries": 0,
    "total_ms": 1711
  },
  "scrape": {
    "url": "https://example.com",
    "status_code": 200,
    "content_length": 334,
    "truncated": false
  },
  "warnings": ["empty_extraction"]
}

This response came from a real request that asked https://example.com, which has no price, for a price and a sku.

The warning codes are:

CodeMeaning
content_truncatedThe page was too long for the model and was cut short, so the result may be incomplete
extract_validation_failedThe model's output did not match your schema
exhausted_retriesThe retries ran out before the model produced output that matched your schema
model_refusalThe model declined to extract
empty_extractionThe output matched your schema, but it held values and all of them were null

Cost

A request is billed on two things:

  • A fixed credit charge. Today a request costs 10 credits, whatever the page or the schema, and they are charged only when the extraction validates.
  • The page's bandwidth. The page is billed at the bytes it took to download, which is the scrape.content_length in the response, with no multiplier. While the page loads the account is charged an estimate of 100 KiB; when the download finishes the estimate is replaced by the real size.

When a request fails, what you keep paying depends on how far it got:

What happenedCreditsBandwidth
Rejected or failed before the page was fetchedNot charged, or refundedNot charged, or refunded
The page was fetched, but the extraction failed (provider_error, provider_timeout, provider_rate_limited, content_too_large, or schema_incompatible raised after the fetch)RefundedKept: the page was downloaded
The page was fetched and the call returned 200 with validated: falseNot chargedKept: the page was downloaded
A successful extractionChargedCharged

Credit billing covers the two charges and a failure before the fetch, and Rate Limits covers a failed extraction after it. On real requests, credits_scrape on GET /v1/usage rose by 10 after a successful extraction and did not move after the validated: false call or a request for a host that does not exist.

The credits for this endpoint appear in credits_scrape on GET /v1/usage.

Not in the SDKs

/v1/schema is not available in the TypeScript SDK, the Python SDK or the CLI: none of them has a method for it. Call the HTTP endpoint directly, as in the examples above. Any HTTP client works with the same Authorization: Bearer header; see Authentication.

Errors

Errors use the unified envelope described in the Errors guide. These are the ones you are most likely to meet on this endpoint. The Errors guide does not list unsupported_option, unsupported_model, schema_incompatible, content_too_large, scrape_failed or the provider_* codes, so handle those here.

StatusCodeWhenRetry?
400invalid_urlurl is not a valid http or https URL. error.details names the field and the reasonNo — fix the URL
400unsupported_optionwait_until: "networkidle", a wait_for_selector or a delay_ms above 0 (see Rendering options)No — remove the option
400unsupported_modelmodel is not one of the two listed in OptionsNo — use a listed model
400validation_errorThe body includes cookies or headers, which are no longer acceptedNo — remove the field
401unauthorized, invalid_api_keyThe API key is missing, malformed, or not validNo — check your key
402quota_exceededThe credit allowance is used up, or the bandwidth balance cannot cover the 100 KiB estimate. The message says whichNo — add credits or balance, or upgrade
402spending_cap_reached, billing_entitlement_impaired, billing_entitlement_expiredThe spending cap or the subscription state blocks the request — see the Errors guideNo — resolve the billing state first
422validation_errorThe body is not valid JSON, a required field is missing, a field has the wrong type or range, or the body has a field this endpoint does not know (for example async or formats)No — fix the request
422schema_incompatibleThe schema uses something outside what is supportedNo — simplify the schema
422content_too_largeThe page was fetched, but it was too large or too slow to convert for extraction. Credits are refunded and the page's bandwidth is kept (see Cost)No — retrying the same request fetches and bills the page again
429rate_limitedToo many requests per secondYes — wait for the Retry-After header, then back off
429provider_rate_limitedThe extraction service is rate limiting your requests. A Retry-After header may be presentYes — wait for Retry-After when present, then back off
500internal_errorAn unexpected failure on ByteKit's side. If the request could not be recorded, it did not runYes — back off and retry
503scrape_failedThe page could not be fetched, or the extraction service could not be reached. Credits are refunded, and so is the bandwidth when the page was not fetchedYes — back off and retry; if the same URL keeps failing, check the page loads
503provider_timeout, provider_unavailableThe extraction service did not answer in time, or is not availableYes — back off and retry
503provider_errorThe extraction service returned an error. A schema with only list properties can cause it when the page has no value (see Writing the schema)Yes — back off and retry once or twice; if it repeats, change the schema

422 schema_incompatible normally comes from the check on your schema before the page is fetched, as in the example above. It can also come back after the fetch, when the model cannot work with the schema; the charges for that case are in the second row of the Cost table.

A 429 rate_limited response carries a Retry-After header, in seconds. Your own limit depends on your plan; see Rate Limits. The 500 and 503 bodies use the same envelope as the other errors in this table; read error.code to tell the 503 cases apart.

{
  "status": "failed",
  "error": {
    "code": "scrape_failed",
    "message": "Schema extraction failed: scrape_failed",
    "http_status": 503,
    "failed_at": "2026-10-09T23:18:17.698Z"
  }
}

This response came from a real request for a host that does not exist.

Next steps