---
seo_title: Distill (Fase 1): extraer datos estructurados | EnConvert
meta_desc: Beta privada, Fase 1: extracción estructurada guiada por esquema desde cualquier URL. Primero una pasada CSS gratis, luego un respaldo LLM con tu forma JSON.
keywords: extraer datos estructurados de una web api, web scraping con json schema api, api de scraping con llm, extracción de datos con selectores css api, alternativa a firecrawl extract, scraping de datos de productos api, convertir una web a json api, extracción de datos web estructurados
---

# API para Extraer Datos Estructurados de un Sitio Web

<div class="alert alert-warning">
<strong>Beta privada.</strong> Distill ya se puede llamar hoy con tu clave de API habitual, en cualquier plan incluido Founding, y descuenta de tu cuota mensual de ops igual que cualquier otra llamada. No está anunciado ni disponible de forma general: las formas de la solicitud y de la respuesta pueden cambiar sin avisar, y no hay ningún compromiso de estabilidad ni de soporte, así que todavía no montes nada crítico encima. La hoja de ruta está en <a href="/es/docs/coming-soon">Próximamente</a>, y cada versión se anuncia en <a href="/es/changelog">el changelog</a>.
</div>

`POST /v2/distill` es una API para extraer datos estructurados de sitios
web: extrae campos de una o más URLs para que coincidan con un esquema
que tú proporcionas, ya sea un objeto JSON-Schema o un mapa plano
`{field: description}`. Ejecuta un motor de dos pasadas: primero una
pasada CSS gratuita (`JsonCssExtractionStrategy` de Crawl4AI, controlada
por tus selectores), y luego una pasada limitada asistida por LLM
para los campos que la pasada CSS dejó vacíos. La respuesta `data`
está garantizada a devolverse exactamente
en la forma que pediste. Será la respuesta de EnConvert a `/extract` de
Firecrawl.

Aquí tienes la llamada útil más pequeña. Envía una URL y un esquema plano
`{field: description}`, y recibe de vuelta los campos extraídos:

```bash
curl -X POST https://api.enconvert.com/v2/distill \
  -H "X-API-Key: sk_your_private_key" \
  -H "Content-Type: application/json" \
  -d '{
    "urls": ["https://example.com/product/widget"],
    "schema": {
      "name": "the product name",
      "price": "the listed price",
      "in_stock": "whether it is in stock"
    }
  }'
```

La respuesta trae un resultado por URL, los `data` extraídos, y qué nivel
los produjo:

```json
{
    "operation_id": "dst_3f9a2c1b8e7d4a6f90b1c2d3e4f5a6b7",
    "total": 1,
    "completed": 1,
    "failed": 0,
    "results": [
        {
            "url": "https://example.com/product/widget",
            "url_final": "https://example.com/product/widget",
            "status": "completed",
            "data": {
                "name": "Widget Pro",
                "price": "$49.00",
                "in_stock": "yes"
            },
            "extraction_tier": "llm",
            "fields_from_css": 0,
            "fields_from_llm": 3,
            "render_quality": 0.91,
            "tokens": {"input": 4120, "output": 38},
            "cost_cents": 0.45,
            "warnings": []
        }
    ],
    "total_cost_cents": 0.45,
    "warnings": []
}
```

---

## Endpoints

| Método | Ruta | Propósito |
|--------|------|---------|
| `POST` | `/v2/distill` | Ejecuta distill sobre una lista explícita de URLs, o usa discover para descubrir primero las URLs de un sitio y aplica distill a cada una, contra el mismo esquema. |

**Content-Type:** `application/json`.

A diferencia de [perceive](/es/docs/endpoints/perceive.md), distill es un único
endpoint síncrono: no hay una ruta separada de reobtención GET ni un
camino asíncrono por lotes. Cada URL se renderiza secuencialmente a
través del singleton compartido de Chrome headless y el conjunto
completo de resultados vuelve en una sola respuesta.

---

## Autenticación

Autentícate con una clave privada en el encabezado `X-API-Key` para
llamadas servidor a servidor. Este es el camino que usan los ejemplos de
abajo.

```http
X-API-Key: sk_your_private_key
```

Las claves públicas con un token bearer JWT también funcionan, usando el
mismo flujo que cualquier otro endpoint: genera un token con tu clave
`pk_`, y luego envíalo como `Authorization: Bearer <token>`. El flujo
completo, incluyendo el bloqueo de dominio y la renovación de tokens,
está en [la guía de autenticación](/es/docs/authentication.md).

Cada clave de API lleva una lista de endpoints permitidos. Si
`/v2/distill` no está en la lista de la clave, la solicitud se rechaza
con `403`.

---

## Cómo funciona distill

Una solicitud aplica distill a una lista de URLs contra un esquema. El
flujo es el mismo para cada URL:

1. **Resuelve la lista de URLs.** Con `urls`, la lista es exactamente lo
   que enviaste (deduplicada, con el orden preservado). Con
   `discover_from`, distill ejecuta [discover](/es/docs/coming-soon/discover.md)
   primero sobre la URL semilla (analizando el sitemap, rastreando, o
   ambas cosas) y luego aplica distill a las URLs descubiertas hasta
   `max_pages`.
2. **Renderiza.** Cada URL se renderiza una vez en Chrome headless a
   través del mismo pipeline de captura que impulsa
   [perceive](/es/docs/endpoints/perceive.md). El renderizado se filtra contra
   SSRF y, si `respect_robots=true`, se verifica contra el `robots.txt`
   del sitio. No se sube ningún artefacto al almacenamiento, porque
   distill solo necesita el DOM renderizado.
3. **Pasada 1: CSS (gratis).** Si proporcionaste un `css_schema`, el
   extractor CSS se ejecuta sobre el HTML renderizado y llena cada campo
   direccionable por selector a costo cero de LLM. Esta pasada está
   limitada a 10 segundos; si se agota el tiempo, la URL cae a la pasada
   LLM con una advertencia.
4. **Pasada 2: LLM (limitada, solo cuando hace falta).** Distill
   recopila los campos del esquema que la pasada CSS dejó faltantes o
   vacíos y escala *solo esos campos* a una pasada asistida por LLM,
   bajo topes de presupuesto estrictos por llamada y por período. Si
   tu plan no tiene nivel LLM, la página fue marcada como bloqueada,
   o se alcanza un tope de presupuesto, la pasada LLM se omite y los
   campos faltantes vuelven como `null` con una advertencia.
5. **Normaliza.** El resultado combinado se remodela para que coincida
   exactamente con las claves de tu esquema: los escalares faltantes
   se vuelven `null`, los arrays faltantes se vuelven `[]`, y cualquier
   clave extra se descarta. La garantía de forma se mantiene sin
   importar lo que haya producido CSS o el LLM.

Se factura una op por URL, y solo después de que esa URL se
completa. Los fallos de renderizado (rechazo por SSRF, bloqueo de
robots, un renderizado fallido) producen una fila de resultado `failed`
y no cuestan ops.

---

## Parámetros de la solicitud

Debes proporcionar **exactamente uno** de `urls` o `discover_from`, más
un `schema`. Enviar ambos, o ninguno, produce un `422`.

### Origen: URLs explícitas

| Parámetro | Tipo | Predeterminado | Descripción |
|-----------|------|---------|-------------|
| `urls` | `string[]` | -- | URLs explícitas para aplicar distill. Cada una debe comenzar con `http://` o `https://` y tener como máximo 2,048 caracteres. Máximo 50 URLs por solicitud (`MAX_DISTILL_URLS`). Mutuamente excluyente con `discover_from`. |

### Origen: descubrir y luego aplicar distill

| Parámetro | Tipo | Predeterminado | Descripción |
|-----------|------|---------|-------------|
| `discover_from` | `object` | -- | Descubre primero las URLs de un sitio, y luego aplica distill a cada una. Mutuamente excluyente con `urls`. Requiere el flag de plan `discover_enabled` (de lo contrario `402`). |
| `discover_from.url` | `string` | -- | URL semilla. Debe comenzar con `http://` o `https://`. Máximo 2,048 caracteres. |
| `discover_from.mode` | `string` | `hybrid` | `sitemap`, `crawl`, o `hybrid`. Los mismos modos que [el endpoint discover](/es/docs/coming-soon/discover.md). |
| `discover_from.max_pages` | `integer` | `10` | Tope de URLs descubiertas y procesadas con distill, de 1 a 50. Cada una es un renderizado completo, así que está acotado por `MAX_DISTILL_URLS`. |

### Esquema o prompt

Proporciona **o bien** un `schema` (la forma de salida que quieres) **o
bien** un `prompt` (una descripción en lenguaje natural de qué extraer).
Se requiere exactamente uno; si envías ambos, gana `schema`.

| Parámetro | Tipo | Predeterminado | Descripción |
|-----------|------|---------|-------------|
| `schema` | `object` | `null` | La forma de salida, enviada bajo la clave JSON `schema`. Puede ser un objeto JSON-Schema (`{"type": "object", "properties": {...}}`) o un mapa plano y flexible `{field: description}`. Máximo 200 propiedades de nivel superior. La `data` de la respuesta está garantizada a coincidir con esta forma. Un esquema estructuralmente inválido produce un `422`. |
| `prompt` | `string` | `null` | Una descripción en lenguaje natural de qué extraer. Cuando se envía sin un `schema`, distill sintetiza a partir de ella el esquema de extracción (un solo modelo) y luego ejecuta el motor normal de dos pasadas. Máximo 2,000 caracteres. |

El esquema es el contrato. Si envías un objeto JSON-Schema, distill lee
sus `properties`; si envías un mapa plano, cada clave nombra un campo y
cada valor es la descripción que se le pasa al LLM. De cualquier forma,
`data` vuelve con exactamente las claves de nivel superior del esquema.

**Modo solo-prompt.** Si envías un `prompt` en lugar de un `schema`,
distill primero sintetiza un esquema de campos a partir de tu prompt, y
luego extrae contra él. Los campos sintetizados se devuelven como eco en
la respuesta como `synthesized_schema`. El modo solo-prompt usa el nivel
de extracción LLM y requiere un plan que lo incluya; sin uno, distill
devuelve una advertencia clara en lugar de adivinar.

### Esquema CSS (opcional)

Proporciona un `css_schema` para responder campos gratis antes de
cualquier llamada al LLM. Sin él, todos los campos caen directo a la
pasada LLM.

| Parámetro | Tipo | Predeterminado | Descripción |
|-----------|------|---------|-------------|
| `css_schema.baseSelector` | `string` | -- | Selector CSS para el contenedor repetido; se extrae un registro por coincidencia. 1–1,024 caracteres. Obligatorio cuando `css_schema` está presente. |
| `css_schema.fields` | `CssField[]` | -- | 1–128 definiciones de campo leídas de cada contenedor. Obligatorio. |
| `css_schema.name` | `string` | `"distill"` | Etiqueta opcional para el esquema. Máximo 128 caracteres. |
| `css_schema.target_field` | `string` | inferido | Qué propiedad de salida de nivel superior llenan los registros de CSS: una propiedad de tipo array recibe la lista completa de registros, una propiedad escalar/objeto recibe el primer registro. Cuando se omite, distill lo infiere si el esquema tiene exactamente una propiedad de tipo array. Máximo 128 caracteres. |

Cada entrada en `fields` es un `CssField`:

| Campo | Tipo | Predeterminado | Descripción |
|-------|------|---------|-------------|
| `name` | `string` | -- | Clave de salida para este campo. 1–128 caracteres. Obligatorio. |
| `type` | `string` | -- | Uno de `text`, `attribute`, `html`, `regex`, `nested`, `list`, `nested_list`. Obligatorio. |
| `selector` | `string` | `null` | Sub-selector CSS. Opcional para tipos hoja (`text`/`attribute`/`html`/`regex`); obligatorio para `nested`/`list`/`nested_list`. Máximo 1,024 caracteres. |
| `attribute` | `string` | `null` | Nombre del atributo a leer. Obligatorio cuando `type` es `attribute`. Máximo 128 caracteres. |
| `pattern` | `string` | `null` | Patrón regex. Obligatorio cuando `type` es `regex`. Se compila en el edge; un patrón con grupos anidados re-cuantificados (una forma ReDoS como `(a+)+`) se rechaza con `422`. Máximo 1,024 caracteres. |
| `default` | any | `null` | Valor cuando el selector no coincide con nada. |
| `transform` | `string` | `null` | Uno de `lowercase`, `uppercase`, `strip`. |
| `fields` | `CssField[]` | `null` | Campos hijos, para `nested`/`list`/`nested_list`. Máximo 64 hijos; profundidad total de anidación máximo 5. |

Vale la pena señalar: el tipo de campo `computed` de Crawl4AI
deliberadamente no se acepta. Su forma de expresión ejecuta `eval` sobre
la entrada del llamador, y su forma invocable no puede cruzar un límite
JSON, así que distill enumera solo los siete tipos seguros de arriba.

### Controles de renderizado

Distill solo renderiza el DOM, así que expone un pequeño subconjunto de
las [opciones de renderizado de perceive](/es/docs/endpoints/perceive.md).

| Parámetro | Tipo | Predeterminado | Descripción |
|-----------|------|---------|-------------|
| `wait_for` | `string` | `null` | Espera después de la navegación un selector CSS o una expresión JS (`"js:window.dataReady === true"`). Máximo 1,024 caracteres. |
| `wait_timeout_ms` | `integer` | `30000` | Cuánto puede esperar `wait_for`, en milisegundos. 0–60,000. |
| `headers` | `object` | `null` | Encabezados de solicitud personalizados para el renderizado. |
| `cookies` | `array` | `null` | Cookies a inyectar antes de la navegación. |
| `respect_robots` | `boolean` | `false` | Cuando es `true`, una URL no permitida por el `robots.txt` del sitio se rechaza y el resultado de esa URL se marca como `failed`. |

---

## Respuesta

`POST /v2/distill` devuelve un `DistillResponse`:

| Campo | Tipo | Descripción |
|-------|------|-------------|
| `operation_id` | `string` | ID opaco (`dst_...`). Cítalo al contactar a soporte. |
| `total` | `integer` | Número de URLs procesadas (filas en `results`). |
| `completed` | `integer` | URLs que se renderizaron y produjeron un objeto `data`. |
| `failed` | `integer` | URLs cuyo renderizado fue rechazado o falló. |
| `results` | `object[]` | Un `DistillItemResult` por URL, descrito abajo. |
| `total_cost_cents` | `number` | Suma del costo de LLM por URL en toda la solicitud, en centavos. |
| `synthesized_schema` | `object` | Presente solo en el modo solo-prompt: el esquema que se sintetizó a partir de tu `prompt` y se usó para la extracción. |
| `warnings` | `string[]` | Notas a nivel de solicitud (p. ej. cuota de ops agotada a mitad de la lista, fallos de rastreo de discover). |

Cada entrada en `results` es un `DistillItemResult`:

| Campo | Tipo | Descripción |
|-------|------|-------------|
| `url` | `string` | La URL que enviaste (o que se descubrió). |
| `url_final` | `string` | La URL después de redirecciones. Se omite en una fila `failed`. |
| `status` | `string` | `completed` o `failed`. |
| `data` | `object` | Los datos extraídos, normalizados a exactamente las claves de tu esquema. `null` en una fila `failed`. |
| `extraction_tier` | `string` | `css` (solo CSS), `llm` (solo LLM), `mixed` (ambos contribuyeron), o `none` (no se encontró nada). |
| `fields_from_css` | `integer` | Cantidad de campos que llenó la pasada CSS. |
| `fields_from_llm` | `integer` | Cantidad de campos que llenó la pasada LLM. |
| `render_quality` | `number` | 0.0–1.0. Puntuaciones bajas señalan desafíos anti-bot o muros de inicio de sesión. |
| `tokens` | `object` | Tokens de LLM usados `{input, output}`. Cero a menos que se haya ejecutado la pasada LLM. |
| `cost_cents` | `number` | Costo de LLM en centavos para esta URL. Cero a menos que se haya ejecutado la pasada LLM. |
| `error` | `string` | Se establece solo cuando `status` es `failed`. Un mensaje genérico, porque el detalle interno del renderizado se queda del lado del servidor. |
| `warnings` | `string[]` | Notas por URL: un timeout de CSS, una pasada LLM omitida, un tope de presupuesto alcanzado. |

Para ser directos: la respuesta no lleva URLs de descarga firmadas ni
artefactos almacenados. Distill devuelve la `data` estructurada en línea
y nada más. Si además quieres el Markdown de la página, el HTML, una
captura de pantalla, o un PDF, para eso está
[perceive](/es/docs/endpoints/perceive.md).

---

## El modelo de costo de dos pasadas

La pasada CSS es gratis. La pasada LLM cuesta dinero, así que distill la
dispara lo más acotada posible y la limita desde varios frentes.

**Escala solo los campos faltantes.** Después de la pasada CSS, distill
calcula qué campos del esquema siguen vacíos: un escalar que volvió
como `null`/`""`, un array que volvió vacío, o un array cuyos elementos
carecen de un sub-campo declarado. Solo esos nombres de campo entran en
un esquema reducido para la llamada al LLM, lo que mantiene el prompt y
el costo al mínimo.

**Omite la pasada LLM por completo cuando** se cumple cualquiera de
estas condiciones, devolviendo el resultado solo-CSS con una advertencia
en lugar de sobregastar:

- Tu plan no tiene nivel LLM (`llm_extraction_enabled` más un
  `agent_model_tier` distinto de `none`).
- El `render_quality` de la página la marcó como bloqueada por
  protección anti-bot.
- Se alcanza el presupuesto de LLM por solicitud para esta llamada, o se
  alcanza el tope de presupuesto por período.

**Los topes de presupuesto están en capas:**

| Tope | Valor | Alcance |
|-----|-------|-------|
| Por llamada | $0.05 (`PER_REQUEST_CAP_CENTS`) | Costo proyectado en el peor caso de una llamada al LLM. Por encima → se omite antes de cualquier I/O de red. |
| Por solicitud | $0.50 (`_REQUEST_LLM_BUDGET_CENTS`) | Gasto total de LLM en todas las URLs de una llamada a `/v2/distill`. Las URLs restantes vuelven solo-CSS. |
| Escalaciones por solicitud | 50 (`_MAX_LLM_ESCALATIONS`) | Como máximo una llamada al LLM por URL, con tope estricto. |
| Por período | Tu saldo mensual de créditos de IA: $5 / $15 / $40 otorgados al mes en Indie / Studio / Production, los créditos no usados se acumulan | `ch_usage_periods.llm_cost_cents` contra los créditos otorgados del período (`usage.reserve_llm_budget`). |

> **Nota.** El presupuesto por período se reserva de forma atómica antes
> de la llamada y se ajusta al costo real después, así que las llamadas
> concurrentes no pueden superar el tope en conjunto. Un proyecto sin
> una fila de período de uso activa falla de forma segura, porque el gasto
> que EnConvert no puede contabilizar es gasto que no realiza. Cuando se
> alcanza un tope, los campos afectados vuelven como `null` con una
> advertencia; la solicitud igual se completa con éxito.

Cuando la pasada LLM sí se ejecuta, `extraction_tier` reporta `llm` o
`mixed`, y `tokens` y `cost_cents` reportan lo que costó. Cuando no se
ejecuta, ambos son cero.

---

## Cómo se mapean los registros de CSS a tu esquema

El caso común es una página de listado: una fila repetida, y un esquema
de salida con una propiedad de tipo array para contener las filas. Dale
a distill un `css_schema` cuyo `baseSelector` coincida con la fila y
cuyos `fields` lean las columnas, y llenará el array gratis:

```bash
curl -X POST https://api.enconvert.com/v2/distill \
  -H "X-API-Key: sk_your_private_key" \
  -H "Content-Type: application/json" \
  -d '{
    "urls": ["https://example.com/products"],
    "schema": {
      "type": "object",
      "properties": {
        "products": {
          "type": "array",
          "items": {
            "type": "object",
            "properties": {
              "name": {"type": "string"},
              "price": {"type": "string"},
              "sku": {"type": "string"}
            }
          }
        }
      }
    },
    "css_schema": {
      "baseSelector": ".product-card",
      "target_field": "products",
      "fields": [
        {"name": "name", "type": "text", "selector": ".title"},
        {"name": "price", "type": "text", "selector": ".price"},
        {"name": "sku", "type": "attribute",
         "selector": ".product-card", "attribute": "data-sku"}
      ]
    }
  }'
```

Cómo aterrizan los registros en el esquema:

- `target_field` establecido en una propiedad de tipo **array** → esa
  propiedad recibe la lista completa de registros.
- `target_field` establecido en una propiedad **escalar/objeto** →
  recibe el primer registro.
- `target_field` omitido, el esquema tiene **exactamente una** propiedad
  de tipo array → distill lo infiere y llena esa propiedad.
- De lo contrario → el primer registro se trata como un único objeto
  plano y sus claves coincidentes se elevan al nivel superior.

Si CSS llena el array pero algunos elementos carecen de un sub-campo
declarado (digamos que `sku` falta en la mitad de las tarjetas),
distill escala `products` a la pasada LLM para llenar los vacíos. Ese es
el diferenciador de dos pasadas frente a un scraper plano: estructurado
donde se pueda, respaldado por modelo donde haga falta.

---

## Ejemplos de código

### curl: esquema plano, solo LLM

```bash
curl -X POST https://api.enconvert.com/v2/distill \
  -H "X-API-Key: sk_your_private_key" \
  -H "Content-Type: application/json" \
  -d '{
    "urls": ["https://example.com/article"],
    "schema": {
      "headline": "the article headline",
      "author": "the author name",
      "published": "the publish date"
    }
  }'
```

### curl: descubrir y luego aplicar distill

```bash
curl -X POST https://api.enconvert.com/v2/distill \
  -H "X-API-Key: sk_your_private_key" \
  -H "Content-Type: application/json" \
  -d '{
    "discover_from": {
      "url": "https://example.com/blog",
      "mode": "sitemap",
      "max_pages": 25
    },
    "schema": {
      "title": "the post title",
      "summary": "a one-line summary"
    }
  }'
```

### Python

```python
import requests

response = requests.post(
    "https://api.enconvert.com/v2/distill",
    headers={"X-API-Key": "sk_your_private_key"},
    json={
        "urls": ["https://example.com/product/widget"],
        "schema": {
            "name": "the product name",
            "price": "the listed price",
            "in_stock": "whether it is in stock",
        },
    },
)
response.raise_for_status()
result = response.json()

for item in result["results"]:
    if item["status"] == "completed":
        print(item["url"], "->", item["data"])
    else:
        print(item["url"], "FAILED:", item["error"])

print("total cost (cents):", result["total_cost_cents"])
```

### Node.js

```javascript
const res = await fetch("https://api.enconvert.com/v2/distill", {
    method: "POST",
    headers: {
        "Content-Type": "application/json",
        "X-API-Key": "sk_your_private_key"
    },
    body: JSON.stringify({
        urls: ["https://example.com/product/widget"],
        schema: {
            name: "the product name",
            price: "the listed price",
            in_stock: "whether it is in stock"
        }
    })
});

const result = await res.json();

for (const item of result.results) {
    if (item.status === "completed") {
        console.log(item.url, "->", item.data);
    } else {
        console.log(item.url, "FAILED:", item.error);
    }
}

console.log("total cost (cents):", result.total_cost_cents);
```

---

## Respuestas de error

| Estado | Condición |
|--------|-----------|
| `400 Bad Request` | Una URL (o la semilla de `discover_from`) resuelve a una dirección privada, de loopback, o link-local, no tiene hostname, o de otra forma falla el filtro SSRF. Se genera por URL durante el renderizado. |
| `401 Unauthorized` | Clave de API / token JWT faltante o inválido. |
| `402 Payment Required` | Distill no está en tu plan actual, tu cuota mensual de ops está agotada, o `discover_from` se envió sin el flag de plan `discover_enabled`. |
| `403 Forbidden` | `/v2/distill` no está entre los endpoints permitidos de la clave de API. |
| `422 Unprocessable Entity` | Falta `schema`, se enviaron ambos o ninguno de `urls`/`discover_from`, un esquema estructuralmente inválido, más de 200 propiedades de esquema, un `CssField` inválido (falta `attribute`/`pattern`/`fields` para su tipo, un regex no compilable o propenso a ReDoS), o anidación de campos CSS más profunda que 5. |
| `500 Internal Server Error` | La orquestación falló inesperadamente. El mensaje incluye el `operation_id` para citar a soporte. |

Algunas notas de estado que vale la pena tener claras: una sola URL cuyo
renderizado es rechazado por SSRF muestra un `400` solo cuando es la
semilla de una solicitud `discover_from`; para una lista explícita de
`urls`, un rechazo de renderizado por URL se convierte en una fila de
resultado `failed` en lugar de hacer fallar toda la solicitud. Un tope
de presupuesto de LLM nunca es un error, porque se degrada a un resultado
solo-CSS con una advertencia. La referencia completa de códigos de
estado está en [la guía de códigos de error](/es/docs/reference/errors.md).

---

## Límites

| Límite | Valor |
|-------|-------|
| URLs por solicitud (`urls`) | 50 (`MAX_DISTILL_URLS`) |
| `discover_from.max_pages` | 1–50 |
| Longitud de URL | 2,048 caracteres |
| Propiedades de nivel superior del esquema | 200 (`MAX_SCHEMA_PROPERTIES`) |
| `css_schema.fields` | 1–128 |
| Hijos de `CssField.fields` | 64 |
| Profundidad de anidación de campos CSS | 5 (`MAX_CSS_FIELD_DEPTH`) |
| Longitud de `wait_for` | 1,024 caracteres |
| `wait_timeout_ms` | 0–60,000 ms |
| Timeout de la pasada CSS | 10 segundos por URL |
| Tope por llamada de LLM | $0.05 |
| Tope por solicitud de LLM | $0.50 |
| Escalaciones de LLM por solicitud | 50 |
| Tope por período de LLM | Saldo mensual de créditos de IA ($5 / $15 / $40 según el nivel; los créditos no usados se acumulan) |
| Ops mensuales (compartidas entre todos los endpoints, 1 por URL completada) | 500 / 3.000 / 15.000 / 50.000 según el nivel; ver [precios](/es/pricing.md) |

---

## Preguntas frecuentes

### ¿Cómo extraigo datos estructurados de un sitio web con una API REST?

Envía `POST /v2/distill` con `urls` (hasta 50 por solicitud) y un
`schema`, ya sea un objeto JSON-Schema o un mapa plano
`{field: description}`. La `data` de la respuesta vuelve normalizada a
exactamente las claves de nivel superior de tu esquema.

### ¿Puedo extraer un sitio web hacia un esquema JSON sin escribir selectores CSS?

Sí. `css_schema` es opcional, y sin él todos los campos caen directo a
la pasada asistida por LLM. Proporcionar un `css_schema`
llena gratis los campos direccionables por selector y escala solo los
campos que la pasada CSS dejó vacíos.

### ¿Cuánto cuesta la pasada de extracción LLM, y cómo se limita?

Los topes de presupuesto están en capas: $0.05 por llamada al LLM, $0.50
por solicitud a `/v2/distill`, como máximo 50 escalaciones por
solicitud, y por período tu saldo mensual de créditos de IA ($5 / $15 /
$40 en Indie / Studio / Production; los créditos no usados se
acumulan). La extracción con LLM consume créditos, no ops. Alcanzar un tope nunca hace fallar la
solicitud: los campos afectados vuelven como `null` con una
advertencia.

### ¿Por qué algunos campos aparecen como null en mi respuesta de distill?

Los escalares faltantes se normalizan a `null` (y los arrays faltantes a
`[]`) para preservar la garantía de forma. La pasada LLM se omite (con
una advertencia) cuando tu plan no tiene nivel LLM, el `render_quality`
de la página la marcó como bloqueada, o se alcanzó un tope de
presupuesto.

### ¿Puedo rastrear un sitio completo y extraer el mismo esquema de cada página?

Sí. Envía `discover_from` con una `url` semilla, un `mode` (`sitemap`,
`crawl`, o `hybrid`), y `max_pages` (1–50) en lugar de `urls`. Requiere
el flag de plan `discover_enabled`; sin él la solicitud se rechaza con
`402`.
