Distill

POST /v2/distill extrae datos estructurados de una o varias URL para que coincidan con un esquema que tú proporcionas. Funciona con un motor de dos pasadas: primero una pasada CSS gratuita (la JsonCssExtractionStrategy de Crawl4AI guiada por tus selectores) y luego —solo para los campos que la pasada CSS dejó vacíos— una pasada LLM acotada sobre claude-haiku-4-5 de Anthropic. El data de la respuesta está garantizado que vuelve con la forma exacta que pediste. Es la respuesta de EnConvert a Firecrawl /extract.

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:

curl -X POST https://api.enconvert.com/v2/distill \
  -H "X-API-Key: sk_live_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 lleva un resultado por URL, el data extraído y qué nivel lo produjo:

{
    "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": []
}

Conviene señalarlo desde el principio. /v2/distill es un endpoint V2 con su propio contador de cuota, distill_operations, donde una operación equivale a una URL destilada. Los planes de conversión V1 no incluyen ninguna operación de distill: necesitas un plan que incluya V2 para llamarlo. Distill solo factura las URL que se completan, así que un fallo de renderizado de EnConvert nunca te cuesta una unidad. Consulta la visión general de inteligencia web V2 para ver cómo se relacionan los contadores, y la página de precios para las asignaciones por nivel.


Endpoints

Method Path Propósito
POST /v2/distill Destila una lista de URL explícita, o descubre primero las URL de un sitio y destila cada una, contra un único esquema.

Content-Type: application/json.

A diferencia de perceive, distill es un único endpoint síncrono: no hay un GET de refetch separado ni una ruta de lote asíncrono. Cada URL se renderiza secuencialmente a través del singleton compartido de Chrome headless y todo el conjunto de resultados vuelve en una sola respuesta.


Autenticación

Autentícate con una clave privada en la cabecera X-API-Key para llamadas servidor a servidor. Esta es la ruta que usan los ejemplos de abajo.

X-API-Key: sk_live_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 token, está en la guía de autenticación.

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


Cómo funciona distill

Una petición destila una lista de URL contra un único esquema. El flujo es el mismo para cada URL:

  1. Resuelve la lista de URL. Con urls, la lista es exactamente la que enviaste (deduplicada, conservando el orden). Con discover_from, distill ejecuta primero discover sobre la URL semilla —analizando el sitemap, rastreando, o ambos— y luego destila las URL 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. El renderizado se examina contra SSRF y, si respect_robots=true, se comprueba contra el robots.txt del sitio. No se suben artefactos al almacenamiento: distill solo necesita el DOM renderizado.
  3. Pasada 1 — CSS (gratuita). Si proporcionaste un css_schema, el extractor CSS se ejecuta sobre el HTML renderizado y rellena cada campo direccionable por selector con coste LLM cero. Esta pasada está acotada a 10 segundos; al agotarse el tiempo, la URL cae a la pasada LLM con una advertencia.
  4. Pasada 2 — LLM (acotada, solo cuando es necesaria). Distill recopila los campos del esquema que la pasada CSS dejó faltantes o vacíos y escala solo esos campos a claude-haiku-4-5, bajo topes de presupuesto estrictos por llamada y por periodo. 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 reestructura exactamente a 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 produjeron CSS o el LLM.

El contador de cuota se incrementa en uno solo después de que una URL se completa. Los fallos de renderizado (rechazo por SSRF, bloqueo por robots, un renderizado caído) producen una fila de resultado failed y no cuestan cuota.


Parámetros de la petición

Debes proporcionar exactamente uno entre urls o discover_from, más un schema. Enviar ambos, o ninguno, es un 422.

Origen — URL explícitas

Parameter Type Default Descripción
urls string[] URL explícitas a destilar. Cada una debe empezar por http:// o https:// y tener como máximo 2.048 caracteres. Máximo 50 URL por petición (MAX_DISTILL_URLS). Mutuamente excluyente con discover_from.

Origen — descubrir y luego destilar

Parameter Type Default Descripción
discover_from object Descubre primero las URL de un sitio y luego destila cada una. Mutuamente excluyente con urls. Requiere el flag de plan discover_enabled (de lo contrario 402).
discover_from.url string URL semilla. Debe empezar por http:// o https://. Máximo 2.048 caracteres.
discover_from.mode string hybrid sitemap, crawl, o hybrid. Los mismos modos que el endpoint discover.
discover_from.max_pages integer 10 Tope de URL descubiertas y destiladas. 1–50: cada una es un renderizado completo, así que está acotado por MAX_DISTILL_URLS.

Esquema (obligatorio)

Parameter Type Default Descripción
schema object La forma de salida, enviada bajo la clave JSON schema. Puede ser un objeto JSON-Schema ({"type": "object", "properties": {...}}) o un mapa plano y permisivo {field: description}. Máximo 200 propiedades de nivel superior. El data de la respuesta está garantizado que coincide con esta forma. Un esquema estructuralmente inválido es un 422. Obligatorio.

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 entrega al LLM. En cualquier caso, data vuelve con exactamente las claves de nivel superior del esquema.

Esquema CSS (opcional)

Proporciona un css_schema para responder campos gratis antes de cualquier llamada al LLM. Sin él, cada campo cae directamente a la pasada LLM.

Parameter Type Default 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 rellenan los registros CSS: una propiedad de tipo array recibe la lista completa de registros, una propiedad escalar/objeto recibe el primer registro. Cuando se omite, distill la infiere si el esquema tiene exactamente una propiedad de tipo array. Máximo 128 caracteres.

Cada entrada en fields es un CssField:

Field Type Default 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 Subselector CSS. Opcional para los 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 anidamiento máxima 5.

Conviene señalarlo: el tipo de campo computed de Crawl4AI no se acepta deliberadamente. Su forma de expresión ejecuta eval sobre la entrada del llamante, 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.

Parameter Type Default Descripción
wait_for string null Espera tras la navegación a un selector CSS o una expresión JS ("js:window.dataReady === true"). Máximo 1.024 caracteres.
wait_timeout_ms integer 30000 Cuánto tiempo puede esperar wait_for, en milisegundos. 0–60.000.
headers object null Cabeceras de petición personalizadas 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:

Field Type Descripción
operation_id string ID opaco (dst_...). Cítalo al soporte.
total integer Número de URL procesadas (filas en results).
completed integer URL que se renderizaron y produjeron un objeto data.
failed integer URL cuyo renderizado fue rechazado o se cayó.
results object[] Un DistillItemResult por URL: ver abajo.
total_cost_cents number Suma del coste LLM por URL en toda la petición, en céntimos.
warnings string[] Notas a nivel de petición (p. ej. cuota agotada a mitad de la lista, fallos del rastreo de discover).

Cada entrada en results es un DistillItemResult:

Field Type Descripción
url string La URL que enviaste (o descubierta).
url_final string La URL tras las redirecciones. Se omite en una fila failed.
status string completed o failed.
data object Los datos extraídos, normalizados exactamente a 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 Recuento de campos que rellenó la pasada CSS.
fields_from_llm integer Recuento de campos que rellenó la pasada LLM.
render_quality number 0.0–1.0. Las puntuaciones bajas señalan desafíos anti-bot o muros de inicio de sesión.
tokens object Tokens LLM usados {input, output}. Cero salvo que se ejecutara la pasada LLM.
cost_cents number Coste LLM en céntimos para esta URL. Cero salvo que se ejecutara la pasada LLM.
error string Establecido solo cuando status es failed. Un mensaje genérico: el detalle interno del renderizado 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 claros: la respuesta no lleva URL de descarga firmadas ni artefactos almacenados. Distill devuelve el data estructurado en línea y nada más: si también quieres el Markdown de la página, el HTML, una captura de pantalla o un PDF, para eso está perceive.


El modelo de coste de dos pasadas

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

Escala solo los campos faltantes. Tras 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 subcampo declarado. Solo esos nombres de campo entran en un esquema reducido para la llamada LLM, lo que mantiene el prompt y el coste al mínimo.

Omite la pasada LLM por completo cuando se cumple cualquiera de estos, devolviendo el resultado solo-CSS con una advertencia en lugar de gastar de más:

  • 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 LLM por petición para esta llamada, o se alcanza el tope de presupuesto por periodo.

Los topes de presupuesto están estratificados:

Cap Value Alcance
Por llamada $0.05 (PER_REQUEST_CAP_CENTS) Coste proyectado en el peor caso de una llamada LLM. Si se supera → omitida antes de cualquier I/O de red.
Por petición $0.50 (_REQUEST_LLM_BUDGET_CENTS) Gasto LLM total en todas las URL de una llamada a /v2/distill. Las URL restantes devuelven solo-CSS.
Escalaciones por petición 50 (_MAX_LLM_ESCALATIONS) Como máximo una llamada LLM por URL, con tope estricto.
Por periodo $5 por defecto, $20 en Pro y superiores ch_usage_periods.llm_cost_cents para tu periodo de facturación (usage.reserve_llm_budget).

Nota. El presupuesto por periodo se reserva atómicamente antes de la llamada y se ajusta al coste real después, de modo que las llamadas concurrentes no pueden, en conjunto, sobrepasar el tope. Un proyecto sin una fila de periodo de uso activa falla de forma cerrada: 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 petición aun así tiene éxito.

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


Mapear registros 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 alojar las filas. Dale a distill un css_schema cuyo baseSelector coincida con la fila y cuyos fields lean las columnas, y rellenará el array gratis:

curl -X POST https://api.enconvert.com/v2/distill \
  -H "X-API-Key: sk_live_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 apuntando a una propiedad de tipo array → esa propiedad recibe la lista completa de registros.
  • target_field apuntando a una propiedad escalar/objeto → recibe el primer registro.
  • target_field omitido, el esquema tiene exactamente una propiedad de tipo array → distill la infiere y rellena esa propiedad.
  • En otro caso → el primer registro se trata como un único objeto plano y sus claves coincidentes se elevan al nivel superior.

Si CSS rellena el array pero algunos elementos carecen de un subcampo declarado —pongamos que sku falta en la mitad de las tarjetas— distill escala products a la pasada LLM para rellenar los huecos. Ese es el diferenciador de las dos pasadas frente a un scraper sencillo: estructurado donde puede serlo, respaldado por el modelo donde tiene que serlo.


Ejemplos de código

curl — esquema plano, solo-LLM

curl -X POST https://api.enconvert.com/v2/distill \
  -H "X-API-Key: sk_live_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 destilar

curl -X POST https://api.enconvert.com/v2/distill \
  -H "X-API-Key: sk_live_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

import requests

response = requests.post(
    "https://api.enconvert.com/v2/distill",
    headers={"X-API-Key": "sk_live_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

const res = await fetch("https://api.enconvert.com/v2/distill", {
    method: "POST",
    headers: {
        "Content-Type": "application/json",
        "X-API-Key": "sk_live_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);

Restricciones del plan

Las operaciones de distill se miden en su propio contador distill_operations, separado de las conversiones V1 y del contador de perceive. Una operación equivale a una URL destilada, y solo se cuentan las URL que se completan. Los planes de conversión V1 no incluyen ninguna cuota de distill: necesitas un plan que incluya V2.

La pasada LLM dirigida por esquema (claude-haiku-4-5) es una restricción de plan adicional: solo se ejecuta cuando tu plan lleva llm_extraction_enabled y un nivel de modelo de agente distinto de none. Sin ello, distill devuelve resultados solo-CSS y anota los campos omitidos en warnings. El presupuesto LLM por periodo es de $5 en el nivel por defecto y de $20 en Pro y superiores.

Usar discover_from requiere además el flag de plan discover_enabled: de lo contrario la petición se rechaza con 402, de modo que un plan solo de distill no puede llegar a discover gratis enrutando por aquí.

Las asignaciones de distill_operations por nivel y los planes que incluyen el nivel LLM están en la página de precios.


Respuestas de error

Status 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 otro modo no pasa el examen SSRF. Se lanza por URL durante el renderizado.
401 Unauthorized Clave API / token JWT ausente o inválido.
402 Payment Required Distill no está en tu plan actual, tu cuota mensual de distill_operations está agotada, o se envió discover_from sin el flag de plan discover_enabled.
403 Forbidden /v2/distill no está en los endpoints permitidos de la clave API.
422 Unprocessable Entity Falta schema, ambos o ninguno de urls/discover_from, un esquema estructuralmente inválido, más de 200 propiedades de esquema, un CssField inválido (le falta attribute/pattern/fields para su tipo, un regex no compilable o propenso a ReDoS), o anidamiento de campos CSS de más de 5 de profundidad.
500 Internal Server Error La orquestación falló de forma inesperada. El mensaje incluye el operation_id para citarlo al soporte.

Unas pocas notas sobre estados que conviene tener claras: una sola URL cuyo renderizado es rechazado por SSRF expone un 400 solo cuando es la semilla de una petición discover_from; para una lista urls explícita, un rechazo de renderizado por URL se convierte en una fila de resultado failed en lugar de hacer fallar toda la petición. Un tope de presupuesto LLM nunca es un error: 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.


Límites

Límite Value
URL por petición (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 anidamiento 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 LLM por llamada $0.05
Tope LLM por petición $0.50
Escalaciones LLM por petición 50
Tope LLM por periodo $5 por defecto / $20 Pro+
distill_operations mensuales Depende del plan: ver precios