API para rastrear sitios web para RAG#

POST /v2/ingest rastrea un sitio web para RAG: convierte un sitio (o una lista explícita de URLs) en fragmentos listos para RAG y produce un único archivo JSONL que se carga directamente en LangChain JSONLoader, LlamaIndex SimpleDirectoryReader o una importación masiva a una BD vectorial. El endpoint siempre es asíncrono: POST responde 202 con un job_id, tú consultas GET /v2/ingest/{job_id} o registras un webhook_url, y un trabajo completado te devuelve un output_url prefirmado para el JSONL. EnConvert realiza el descubrimiento, el renderizado con Chrome headless, la fragmentación consciente de encabezados y el ensamblado del JSONL en un solo trabajo.

Los archivos subidos se ingieren a través del mismo pipeline mediante POST /v2/ingest/files: PDF, DOCX, PPTX, XLSX, CSV, HTML, EPUB y más se convierten a Markdown, se fragmentan y se ensamblan en el mismo JSONL. Una sola integración cubre la ingesta RAG tanto de web como de archivos.

Esta es la llamada útil más pequeña. Rastrea un sitio y fragmenta cada página que descubre:

curl -X POST https://api.enconvert.com/v2/ingest \
  -H "X-API-Key: sk_your_private_key" \
  -H "Content-Type: application/json" \
  -d '{
    "mode": "crawl",
    "url": "https://example.com/docs"
  }'

La respuesta es el registro del trabajo, devuelto con 202 Accepted. Observa que el estado es queued y que output_url está ausente hasta que el trabajo se completa:

{
    "job_id": "ing_3f9a2c1b8e7d4a6f90b1c2d3e4f5a6b7",
    "status": "queued",
    "mode": "crawl",
    "pages_discovered": 0,
    "pages_processed": 0,
    "pages_failed": 0,
    "total_chunks": 0,
    "webhook_delivered": false,
    "created_at": "2026-06-24T09:14:02.118Z"
}

La ingesta es siempre asíncrona. Cada página se renderiza en un navegador real, lo que tarda entre 10 y 30 segundos por URL, muy por encima de la ventana de solicitud de 300 segundos para cualquier trabajo no trivial. Por eso POST responde 202 con un job_id, y un worker local en el droplet procesa el trabajo fuera de banda. Consultas GET /v2/ingest/{job_id} para ver el progreso, o registras un webhook_url para que te avisen cuando termine.


Endpoints#

Método Ruta Propósito
POST /v2/ingest Crea un trabajo de ingesta web (lista de URLs, sitemap o rastreo). Responde 202 con un job_id.
POST /v2/ingest/files Crea un trabajo de ingesta de archivos a partir de documentos subidos (multipart). Mismo pipeline de trabajo + JSONL.
GET /v2/ingest Lista de los trabajos de este proyecto, del más reciente al más antiguo, con paginación skip/limit.
GET /v2/ingest/{job_id} Estado del ciclo de vida de un trabajo, con un output_url recién firmado una vez completado.
DELETE /v2/ingest/{job_id} Cancela un trabajo. El worker detecta el estado cancelado y se detiene entre páginas.
POST /v2/ingest/{job_id}/retry-webhook Vuelve a firmar y a enviar (POST) el webhook de finalización de un trabajo completado.
GET /v2/ingest/webhook-secret Revela el secreto de firma de webhooks del proyecto (canal del panel).
POST /v2/ingest/webhook-secret/rotate Rota el secreto de firma. Las firmas antiguas dejan de verificarse de inmediato.

Content-Type: application/json en cada POST.


Autenticación#

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

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, incluidos el bloqueo por dominio y la renovación del token, está en la guía de autenticación.

Cada clave de API lleva una lista de endpoints permitidos. Si /v2/ingest no está en la lista de la clave, la solicitud se rechaza con 403. Una clave limitada a /v2/ingest sigue alcanzando los trabajos que creó: GET y DELETE /v2/ingest/{job_id} y POST /v2/ingest/{job_id}/retry-webhook siempre están permitidos para un job_id (la forma ing_… se compara explícitamente). El endpoint de listado estático y las dos rutas de gestión de webhook-secret no heredan esa excepción; requieren un token más amplio o con alcance de panel.


Cómo funciona la ingesta#

Un trabajo pasa por cinco fases, todas duraderas y a prueba de reinicios. Si el proceso del worker se reinicia a mitad del trabajo, este se vuelve a encolar en el arranque y se reanuda desde la página en la que se detuvo. Las páginas ya completadas conservan su salida preparada y nunca se vuelven a renderizar ni a facturar.

  1. Encolar. POST valida la solicitud, ejecuta una comprobación rápida de ops units=1 (el plan tiene la ingesta habilitada y margen en la cuota mensual de ops), inserta la fila del trabajo y responde 202. No se persiste nada si la verificación de ops falla: un 402 no deja ninguna fila atrás.
  2. Descubrir. En los modos sitemap y crawl, el worker ejecuta el mismo pase de descubrimiento que el endpoint de descubrimiento, limitado a max_pages, y filtra la URL semilla contra SSRF. En el modo urls, la lista explícita se deduplica en orden; no se ejecuta ningún descubrimiento. El tamaño del descubrimiento previo al límite se informa como pages_found; cuando el sitio tiene más URLs de las que max_pages permitió, discovery_truncated es true y una entrada en warnings indica ambos números, de modo que pages_discovered (el recuento encolado) nunca se confunde con el tamaño del sitio.
  3. Renderizar y fragmentar. Cada URL se renderiza a través del singleton compartido de Chrome headless, el mismo pipeline de renderizado detrás de el endpoint de percepción. Después, el HTML renderizado se convierte a fit-Markdown y se divide con el fragmentador consciente de encabezados. Los renderizados se ejecutan de forma secuencial, una página a la vez.
  4. Preparar. Los fragmentos de cada página se escriben en un objeto JSONL por página en el almacenamiento, con una clave determinista basada en (project, job, url). Esto es lo que hace barato un reinicio: un trabajo reanudado reutiliza las páginas preparadas en lugar de volver a renderizarlas.
  5. Ensamblar. Una vez terminadas todas las páginas, los objetos por página se concatenan en el v2-ingest/{job_id}.jsonl final, los objetos de preparación se eliminan, el trabajo pasa a completed y se dispara el webhook de finalización firmado si se estableció un webhook_url.

La cuota de ops se vuelve a comprobar por página dentro del worker, no solo en el momento del envío; cada página completada factura una op. Un trabajo crawl cuyo número de páginas se desconoce de antemano se detiene limpiamente en tu límite mensual: las páginas ya renderizadas se facturan y se conservan, y las páginas restantes se marcan como skipped en lugar de gastar de más.

Los renderizados de ingesta no usan credenciales por diseño. A diferencia de /v2/perceive, no acepta auth, cookies ni headers personalizados. No se persiste nada secreto para la reanudación duradera, por lo que el estado del trabajo en disco nunca lleva credenciales.


Ingesta de archivos#

POST /v2/ingest rastrea la web; POST /v2/ingest/files ingiere archivos subidos a través del mismo pipeline. Ambos crean el mismo trabajo, ejecutan el mismo fragmentador consciente de encabezados y producen el mismo entregable JSONL único, así que una sola integración cubre la ingesta RAG de web y de archivos.

Envía los documentos como multipart/form-data en el campo files. Cada archivo se convierte a Markdown mediante el conversor anything-to-markdown, y luego se fragmenta y ensambla exactamente igual que una página rastreada. Se acepta cada formato de entrada admitido: PDF, DOCX, PPTX, XLSX, CSV, HTML, EPUB, OpenDocument y texto plano/Markdown.

curl -X POST https://api.enconvert.com/v2/ingest/files \
  -H "X-API-Key: sk_your_private_key" \
  -F "[email protected]" \
  -F "[email protected]" \
  -F "[email protected]" \
  -F "max_words=700" \
  -F "webhook_url=https://your-app.example.com/hooks/ingest"

La respuesta es el mismo IngestJobResponse que el endpoint de rastreo, con mode establecido en files:

{
    "job_id": "ing_7c1d8e2f4a5b6c7d8e9f0a1b2c3d4e5f",
    "status": "queued",
    "mode": "files",
    "pages_discovered": 0,
    "pages_processed": 0,
    "pages_failed": 0,
    "total_chunks": 0,
    "webhook_delivered": false,
    "created_at": "2026-07-14T10:15:30.220Z"
}

Cada archivo subido cuenta como una «página»: factura una op, incrementa pages_processed a medida que se completa, y se etiqueta con su nombre de archivo en el metadata.source_url del JSONL. Consultas GET /v2/ingest/{job_id}, cancelas con DELETE y recibes el webhook de finalización firmado exactamente igual que en un trabajo de rastreo. Los archivos se almacenan solo hasta que se ensambla el JSONL, y luego se eliminan.

Parámetros de la solicitud de archivos#

Se envían como campos de formulario multipart (no como cuerpo JSON):

Campo Tipo Predeterminado Descripción
files file[] -- Uno o más documentos a ingerir. 1–200 archivos por solicitud; cada uno se comprueba en tamaño contra el límite de subida de tu plan.
max_words integer 512 Límite flexible de palabras por fragmento. 32–4,000. Los bloques de código y las tablas con barras verticales se mantienen atómicos.
sentence_overlap integer 1 Frases repetidas entre fragmentos de prosa consecutivos de la misma sección. 0–10.
webhook_url string null Callback de finalización firmado con HMAC, con la misma política de firma y reintentos que los webhooks de finalización de abajo.

Un tipo de archivo no admitido, un archivo vacío o una imagen (no se realiza OCR) se rechaza en el envío con un 400; un archivo que supere el límite de tamaño por archivo de tu plan devuelve un 413.

import requests

BASE = "https://api.enconvert.com"
HEADERS = {"X-API-Key": "sk_your_private_key"}

# Submit several files (always 202).
with open("handbook.pdf", "rb") as a, open("pricing.xlsx", "rb") as b:
    job = requests.post(
        f"{BASE}/v2/ingest/files",
        headers=HEADERS,
        files=[("files", ("handbook.pdf", a)), ("files", ("pricing.xlsx", b))],
        data={"max_words": 700},
    ).json()

# Poll GET /v2/ingest/{job_id} exactly as for a crawl job, then download output_url.
print(job["job_id"], job["status"], job["mode"])  # -> ing_...  queued  files

Parámetros de la solicitud#

Origen y modo#

Parámetro Tipo Predeterminado Descripción
mode string "urls" urls, sitemap o crawl. Selecciona cómo se construye el conjunto de URLs.
url string null URL semilla para el modo sitemap/crawl. Debe empezar por http:// o https://. Máximo 2,048 caracteres. Obligatoria para esos modos; rechazada en el modo urls.
urls string[] null URLs explícitas a ingerir en el modo urls. No vacía, máximo 1,000 entradas, cada una http(s) y ≤ 2,048 caracteres. Obligatoria para el modo urls; rechazada en el modo sitemap/crawl.

url y urls son mutuamente excluyentes: envía exactamente un origen. El modo urls requiere urls; sitemap y crawl requieren una url semilla. Enviar la incorrecta para el modo devuelve un 422.

Descubrimiento (modos sitemap / crawl)#

Estos se reenvían al pase de descubrimiento y se ignoran en el modo urls.

Parámetro Tipo Predeterminado Descripción
max_pages integer 50 Límite de URLs descubiertas y ingeridas. 1–1,000.
max_depth integer 2 Profundidad de enlaces de rastreo desde la semilla. 1–5.
same_domain_only boolean true Restringe el descubrimiento al dominio de la semilla.
include_patterns string[] [] Patrones regex que una URL debe cumplir para conservarse. Máximo 50. Cada uno se compila en el envío; un patrón inválido devuelve un 422.
exclude_patterns string[] [] Patrones regex que descartan una URL coincidente. Máximo 50.
respect_robots boolean false Cuando es true, se omite una URL no permitida por el robots.txt del sitio.

Renderizado#

Parámetro Tipo Predeterminado Descripción
wait_for string null Espera tras la navegación a un selector CSS o expresión JS antes de capturar. Máximo 1,024 caracteres.
wait_timeout_ms integer 30000 Cuánto puede esperar wait_for, en milisegundos. 0–60,000.

Nota. La ingesta no acepta auth, cookies ni headers. Si una página necesita credenciales para renderizarse, la ingesta es la herramienta equivocada. Usa el endpoint de percepción, que ofrece toda la superficie de solicitud autenticada, para esa única página.

Fragmentación (objeto chunk)#

Parámetro Tipo Predeterminado Restricciones Descripción
max_words integer 512 32–4,000 Límite flexible de palabras por fragmento. Consciente de encabezados. Los bloques de código y las tablas con barras verticales se mantienen atómicos y pueden superarlo.
sentence_overlap integer 1 0–10 Frases repetidas entre fragmentos de prosa consecutivos de la misma sección. 0 desactiva el solapamiento. El solapamiento nunca cruza un límite de encabezado.

El fragmentador divide en los encabezados #, ## y ###, de modo que cada fragmento pertenece exactamente a una sección y lleva su ruta de encabezados completa. Los encabezados más profundos (##########) permanecen en línea como contenido. Los bloques de código con delimitadores y las tablas Markdown nunca se dividen, incluso cuando un solo bloque supera max_words; los elementos de lista se dividen entre elementos, nunca a mitad de un elemento.

Webhook#

Parámetro Tipo Predeterminado Descripción
webhook_url string null Endpoint que recibe el callback de finalización firmado con HMAC. Máximo 2,048 caracteres. Se comprueba el esquema en el envío; se filtra contra SSRF en el momento de la entrega, no en el del envío.

Respuesta#

POST, GET /v2/ingest/{job_id} y DELETE devuelven todos el mismo objeto IngestJobResponse.

Campo Tipo Descripción
job_id string ID opaco (ing_…). Úsalo con los endpoints GET/DELETE y menciónalo al soporte.
status string queued, discovering, processing, completed, failed o canceled.
mode string El modo que enviaste: urls, sitemap, crawl o files.
pages_discovered integer Elementos que el trabajo realmente encoló: URLs (la lista explícita, o el resultado del descubrimiento limitado a max_pages), o archivos subidos. pages_processed + pages_failed suman este valor una vez que el trabajo alcanza un estado terminal.
pages_found integer URLs únicas elegibles que el descubrimiento produjo antes del límite de max_pages. Para trabajos sitemap es el recuento único real del sitio; para trabajos crawl es una cota inferior (el rastreo deja de solicitar páginas al alcanzar el límite). Ausente en trabajos urls y files.
discovery_truncated boolean true cuando el descubrimiento encontró más URLs únicas de las que max_pages permitió encolar. Una entrada en warnings detalla los números; aumenta max_pages para ingerir más del sitio.
pages_processed integer URLs cuyo renderizado → fragmentación → preparación se completó.
pages_failed integer URLs que no se pudieron renderizar o se omitieron (p. ej. cuota de ops agotada).
total_chunks integer Total de fragmentos escritos en todas las páginas completadas. Coincide con el número de líneas del JSONL.
output_url string URL de descarga prefirmada del JSONL final. Presente solo cuando status es completed; expira tras 15 minutos.
error_message string Se establece cuando status es failed (p. ej. descubrimiento rechazado, todas las páginas fallaron).
webhook_url string El destino del webhook de finalización registrado para este trabajo, si lo hay.
webhook_delivered boolean true una vez que el webhook de finalización firmado obtuvo un 2xx.
created_at string Cuándo se creó el trabajo (UTC).
completed_at string Cuándo el trabajo alcanzó un estado terminal (UTC).
warnings string[] Notas no fatales, p. ej. truncamiento del descubrimiento: "discovery found 719 unique URLs; the job was capped at max_pages=50, so 50 pages were enqueued. Raise max_pages to ingest more of the site."

Nota. POST y el GET/DELETE por trabajo usan response_model_exclude_none, por lo que los campos que aún son null (como output_url antes de completarse) se omiten del JSON en lugar de enviarse como null.

La forma del registro JSONL#

El archivo final es JSON delimitado por saltos de línea. Cada línea es un fragmento:

{"id":"9f2b8c1ad4e5-0000","content":"Pricing is usage-based...","metadata":{"source_url":"https://example.com/pricing","title":"Pricing","headings_path":["Pricing","Plans"],"section":"Plans","word_count":118,"chunk_index":0}}
Campo Tipo Descripción
id string Determinista por (source_url, chunk_index): <md5(url)[:12]>-<index:04d>. Una nueva ejecución produce ids idénticos.
content string El texto recuperable del fragmento. Se corresponde con Document.page_content en LangChain.
metadata.source_url string La página de la que provino el fragmento.
metadata.title string El <title> de la página, con recurso al primer <h1>, limitado a 512 caracteres.
metadata.headings_path string[] La ruta h1 → h2 → h3 bajo la que se sitúa el fragmento.
metadata.section string El texto del encabezado más interno (la última entrada de headings_path).
metadata.word_count integer Número de palabras de content delimitadas por espacios en blanco.
metadata.chunk_index integer El índice del fragmento dentro de su página.

El archivo es UTF-8, escrito con ensure_ascii=false, por lo que el unicode se mantiene legible. Como content es una cadena de nivel superior y metadata es un objeto hermano, el mismo archivo se carga en LangChain JSONLoader(content_key="content", json_lines=True), LlamaIndex SimpleDirectoryReader y cualquier importación a una BD vectorial orientada a líneas sin reestructurarlo.


Ciclo de vida del trabajo y sondeo#

Un trabajo pasa por estos estados:

queued → discovering → processing → completed | failed | canceled
Estado Significado
queued Aceptado y a la espera del worker.
discovering Ejecutando el pase de descubrimiento de sitemap/crawl (solo sitemap/crawl).
processing Renderizando y fragmentando páginas. pages_processed y total_chunks aumentan en vivo.
completed El JSONL final está ensamblado; output_url está firmado y listo.
failed El descubrimiento fue rechazado, o todas las páginas fallaron o se omitieron. error_message lo explica.
canceled Un DELETE alcanzó el trabajo antes de que terminara.

Consulta el estado con el GET por trabajo. Es de solo lectura: no consume ops y vuelve a firmar el output_url a partir de la clave del objeto almacenado en cada llamada:

curl https://api.enconvert.com/v2/ingest/ing_3f9a2c1b8e7d4a6f90b1c2d3e4f5a6b7 \
  -H "X-API-Key: sk_your_private_key"

Un job_id desconocido, o uno que pertenece a otro proyecto, devuelve 404. La existencia nunca se filtra entre proyectos.

Listado de trabajos#

GET /v2/ingest devuelve los trabajos de este proyecto del más reciente al más antiguo, con los parámetros de consulta skip y limit. limit es 20 por defecto y tiene un tope de 100. La respuesta lleva una bandera has_more en lugar de un recuento total:

curl "https://api.enconvert.com/v2/ingest?skip=0&limit=20" \
  -H "X-API-Key: sk_your_private_key"
{
    "jobs": [
        {
            "job_id": "ing_3f9a...",
            "status": "completed",
            "mode": "crawl",
            "pages_discovered": 42,
            "pages_found": 42,
            "discovery_truncated": false,
            "pages_processed": 41,
            "pages_failed": 1,
            "total_chunks": 1187,
            "output_url": "https://spaces.example.com/...signed...",
            "webhook_configured": true,
            "webhook_delivered": true,
            "created_at": "2026-06-24T09:14:02.118Z",
            "completed_at": "2026-06-24T09:31:55.402Z"
        }
    ],
    "skip": 0,
    "limit": 20,
    "has_more": false
}

Las filas del listado reducen webhook_url a un booleano webhook_configured, así que el listado nunca devuelve el endpoint sin procesar en la tabla.

Cancelación de un trabajo#

DELETE /v2/ingest/{job_id} establece el estado del trabajo en canceled. El worker lee ese estado entre páginas y se detiene sin ensamblar la salida. La cancelación es idempotente y a prueba de carreras: si el ensamblado ya se confirmó, el DELETE no coincide con nada y el trabajo se devuelve sin cambios como completed. Un trabajo terminado nunca se revierte a canceled.

curl -X DELETE \
  https://api.enconvert.com/v2/ingest/ing_3f9a2c1b8e7d4a6f90b1c2d3e4f5a6b7 \
  -H "X-API-Key: sk_your_private_key"

Webhooks de finalización#

Establece webhook_url en el POST y EnConvert envía un POST firmado con HMAC cuando el trabajo se completa. La carga útil es un JSON compacto y ordenado por claves:

{"job_id":"ing_3f9a...","output_url":"https://spaces.example.com/...signed...","pages_processed":41,"status":"completed","total_chunks":1187}

La entrega se reintenta hasta tres veces tras el primer intento, con retardos de espera de 1, 4 y 16 segundos, es decir, cuatro POST en el peor caso. Cada intento se vuelve a firmar con una marca de tiempo nueva, de modo que una cadena de reintentos lenta nunca sobrepasa la ventana de frescura del consumidor. Una respuesta 2xx es un éxito. Un endpoint caído se registra como una no entrega y genera una alerta en el panel, pero nunca hunde un trabajo que por lo demás está completado.

El webhook_url se filtra contra SSRF en el momento de la entrega, no en el del envío. Una URL que se resuelve en una dirección privada, de loopback o de metadatos se almacena de forma inerte y solo se rechaza cuando EnConvert intenta enviarle un POST.

Verificación de la firma#

Cada entrega lleva dos cabeceras:

Cabecera Valor
X-Enconvert-Signature sha256=<hex>, el HMAC-SHA256 de <timestamp>.<raw body>.
X-Enconvert-Timestamp La marca de tiempo en segundos unix vinculada a la firma.

La entrada de firma es la marca de tiempo, un . literal y luego el cuerpo de solicitud sin procesar. Vincular la marca de tiempo al MAC significa que un consumidor que rechaza marcas de tiempo caducadas obtiene protección contra reproducción gratis. La ventana de frescura por defecto es de 300 segundos. Verifica en tu manejador:

import hashlib
import hmac
import time

SECRET = "whsec_your_signing_secret"   # from GET /v2/ingest/webhook-secret
TOLERANCE_SECONDS = 300


def verify(raw_body: bytes, signature_header: str, timestamp_header: str) -> bool:
    if not signature_header or not timestamp_header:
        return False
    try:
        ts = int(timestamp_header)
    except ValueError:
        return False
    if abs(time.time() - ts) > TOLERANCE_SECONDS:
        return False  # replayed or badly skewed clock

    provided = signature_header.removeprefix("sha256=")
    expected = hmac.new(
        SECRET.encode("utf-8"),
        f"{ts}.".encode("utf-8") + raw_body,
        hashlib.sha256,
    ).hexdigest()
    return hmac.compare_digest(expected, provided)

Gestión del secreto de firma#

GET /v2/ingest/webhook-secret revela el secreto del proyecto (creándolo en la primera llamada) junto con los nombres de las cabeceras y la tolerancia que necesita tu consumidor. Es sensible y solo se expone a través del canal autenticado del panel:

{
    "secret": "whsec_...",
    "signature_header": "X-Enconvert-Signature",
    "timestamp_header": "X-Enconvert-Timestamp",
    "signature_scheme": "sha256",
    "replay_tolerance_seconds": 300,
    "rotated": false
}

POST /v2/ingest/webhook-secret/rotate emite un nuevo secreto y establece rotated en true. Toda firma calculada con el secreto anterior deja de verificarse en el momento en que se confirma la rotación. Rota tras una fuga sospechada y luego actualiza tu consumidor.

Reenvío de un webhook#

Si tu endpoint estaba caído cuando el trabajo terminó, POST /v2/ingest/{job_id}/retry-webhook vuelve a firmar y a enviar (POST) con la misma política de reintentos:

curl -X POST \
  https://api.enconvert.com/v2/ingest/ing_3f9a.../retry-webhook \
  -H "X-API-Key: sk_your_private_key"
{
    "job_id": "ing_3f9a...",
    "delivered": true,
    "attempts": 1,
    "status_code": 200,
    "detail": "Delivered (HTTP 200)."
}

Devuelve 404 para un job_id desconocido o ajeno, 400 cuando no hay ningún webhook_url configurado (o la URL almacenada ahora se resuelve en una dirección privada/interna), y 409 cuando el trabajo no ha alcanzado completed.


Ejemplos de código#

curl: lista explícita de URLs#

curl -X POST https://api.enconvert.com/v2/ingest \
  -H "X-API-Key: sk_your_private_key" \
  -H "Content-Type: application/json" \
  -d '{
    "mode": "urls",
    "urls": [
      "https://example.com/docs/intro",
      "https://example.com/docs/quickstart",
      "https://example.com/docs/api"
    ]
  }'

curl: rastreo con fragmentación y un webhook#

curl -X POST https://api.enconvert.com/v2/ingest \
  -H "X-API-Key: sk_your_private_key" \
  -H "Content-Type: application/json" \
  -d '{
    "mode": "crawl",
    "url": "https://example.com/docs",
    "max_pages": 200,
    "max_depth": 3,
    "include_patterns": ["/docs/"],
    "chunk": {"max_words": 700, "sentence_overlap": 2},
    "webhook_url": "https://your-app.example.com/hooks/ingest"
  }'

Python: enviar, sondear, descargar#

import time

import requests

BASE = "https://api.enconvert.com"
HEADERS = {"X-API-Key": "sk_your_private_key"}

# 1. Submit (always 202).
job = requests.post(
    f"{BASE}/v2/ingest",
    headers=HEADERS,
    json={"mode": "crawl", "url": "https://example.com/docs", "max_pages": 100},
).json()
job_id = job["job_id"]

# 2. Poll until terminal.
while True:
    job = requests.get(f"{BASE}/v2/ingest/{job_id}", headers=HEADERS).json()
    if job["status"] in ("completed", "failed", "canceled"):
        break
    time.sleep(5)

# 3. Download the JSONL from its signed URL.
if job["status"] == "completed":
    jsonl = requests.get(job["output_url"]).text
    print(f"{job['total_chunks']} chunks across "
          f"{job['pages_processed']} pages")
    print(jsonl.splitlines()[0])

Node.js: enviar y sondear#

const BASE = "https://api.enconvert.com";
const HEADERS = {
    "Content-Type": "application/json",
    "X-API-Key": "sk_your_private_key"
};

// 1. Submit.
const submit = await fetch(`${BASE}/v2/ingest`, {
    method: "POST",
    headers: HEADERS,
    body: JSON.stringify({
        mode: "crawl",
        url: "https://example.com/docs",
        max_pages: 100
    })
});
let job = await submit.json();

// 2. Poll until terminal.
while (!["completed", "failed", "canceled"].includes(job.status)) {
    await new Promise((r) => setTimeout(r, 5000));
    const poll = await fetch(`${BASE}/v2/ingest/${job.job_id}`, {
        headers: { "X-API-Key": HEADERS["X-API-Key"] }
    });
    job = await poll.json();
}

// 3. Download the JSONL.
if (job.status === "completed") {
    const jsonl = await fetch(job.output_url).then((r) => r.text());
    console.log(`${job.total_chunks} chunks`);
    console.log(jsonl.split("\n")[0]);
}

Respuestas de error#

Estado Condición
202 Accepted El trabajo se creó y se encoló. Este es el resultado normal de POST.
401 Unauthorized Clave de API / token JWT ausente o inválido.
402 Payment Required La ingesta no está en tu plan actual, o tu cuota mensual de ops está agotada.
403 Forbidden /v2/ingest no está en los endpoints permitidos de la clave de API.
404 Not Found job_id desconocido, o uno propiedad de otro proyecto.
409 Conflict retry-webhook llamado en un trabajo que no ha alcanzado completed.
400 Bad Request retry-webhook llamado sin ningún webhook_url configurado, o su URL almacenada ahora se resuelve en una dirección privada/interna.
422 Unprocessable Entity El origen no coincide con el modo (urls sin urls, o una url semilla en el modo urls); un parámetro está fuera de rango; o un regex de include_patterns/exclude_patterns no compila.
500 Internal Server Error El trabajo no se pudo crear. El mensaje incluye el job_id que mencionar al soporte.

Un fallo de renderizado a nivel de página no hace fallar la solicitud ni el trabajo. Incrementa pages_failed, coloca el error de la página en su propia fila y el trabajo continúa. Un trabajo solo fails cuando el descubrimiento es rechazado o todas las páginas fallan o se omiten. La referencia completa de códigos de estado está en la guía de códigos de error.


Límites#

Límite Valor
URLs por solicitud en modo urls 1,000
Longitud de url / de cada entrada de urls 2,048 caracteres
max_pages (límite de descubrimiento) 1–1,000
max_depth 1–5
include_patterns / exclude_patterns 50 cada uno
Longitud de wait_for 1,024 caracteres
wait_timeout_ms 0–60,000 ms
chunk.max_words 32–4,000 (predeterminado 512)
chunk.sentence_overlap 0–10 (predeterminado 1)
Longitud de webhook_url 2,048 caracteres
Techo de páginas por trabajo (MAX_PAGES_PER_JOB) 1,000
Archivos por solicitud a /v2/ingest/files 1–200
Tamaño de subida por archivo Depende del plan (Founding: 5 MB)
limit del listado GET /v2/ingest 1–100 (predeterminado 20)
Expiración del output_url firmado 15 minutos
Intentos de entrega del webhook 4 (inicial + 3 reintentos)
Tolerancia de reproducción del webhook 300 segundos
Ops mensuales (compartidas entre todos los endpoints, 1 por página) 500 / 3.000 / 15.000 / 50.000 según el nivel; consulta precios

Preguntas frecuentes#

¿Cómo rastreo un sitio web para RAG con una API?#

Envía POST /v2/ingest con mode: "crawl" y una url semilla. La llamada responde 202 con un job_id; el worker descubre páginas, renderiza cada una en Chrome headless, fragmenta el Markdown de forma consciente de encabezados y ensambla un archivo JSONL que descargas desde el output_url firmado.

¿Cómo ingiero archivos (PDF, documentos de Word) para RAG?#

Envía POST /v2/ingest/files como multipart/form-data con uno o más files. Cada documento se convierte a Markdown, se fragmenta de forma consciente de encabezados y se ensambla en el mismo JSONL único que un trabajo de rastreo, así que un solo pipeline cubre web y archivos. Se admiten archivos PDF, DOCX, PPTX, XLSX, CSV, HTML, EPUB, OpenDocument y de texto plano/Markdown (hasta 200 por solicitud); la lista completa está en la página anything-to-markdown.

¿La salida JSONL se carga directamente en LangChain y LlamaIndex?#

Sí. Cada línea lleva una cadena content de nivel superior con un objeto metadata hermano, por lo que el mismo archivo se carga en LangChain JSONLoader(content_key="content", json_lines=True), LlamaIndex SimpleDirectoryReader y cualquier importación a una BD vectorial orientada a líneas sin reestructurarlo.

¿Cómo divide el fragmentador las páginas en fragmentos para RAG?#

Divide en los encabezados #, ## y ### con un límite flexible max_words (predeterminado 512, rango 32–4,000) y un sentence_overlap opcional. Los bloques de código con delimitadores y las tablas Markdown nunca se dividen, y cada fragmento lleva su headings_path completo.

¿Cómo me notifican cuando termina un trabajo de ingesta?#

Establece webhook_url en el POST y EnConvert envía un callback firmado con HMAC (cabeceras X-Enconvert-Signature y X-Enconvert-Timestamp) con hasta tres reintentos tras el primer intento. Si tu endpoint estaba caído, POST /v2/ingest/{job_id}/retry-webhook lo vuelve a firmar y a entregar.

¿Por qué falta output_url en mi respuesta de ingesta?#

output_url solo está presente una vez que status es completed. La respuesta del POST es un trabajo queued con el campo omitido. Consulta GET /v2/ingest/{job_id}, que no consume ops y vuelve a firmar la URL en cada llamada; cada URL firmada expira tras 15 minutos.