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.
- Encolar.
POSTvalida la solicitud, ejecuta una comprobación rápida de opsunits=1(el plan tiene la ingesta habilitada y margen en la cuota mensual de ops), inserta la fila del trabajo y responde202. No se persiste nada si la verificación de ops falla: un402no deja ninguna fila atrás. - Descubrir. En los modos
sitemapycrawl, el worker ejecuta el mismo pase de descubrimiento que el endpoint de descubrimiento, limitado amax_pages, y filtra la URL semilla contra SSRF. En el modourls, 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 comopages_found; cuando el sitio tiene más URLs de las quemax_pagespermitió,discovery_truncatedestruey una entrada enwarningsindica ambos números, de modo quepages_discovered(el recuento encolado) nunca se confunde con el tamaño del sitio. - 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.
- 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. - Ensamblar. Una vez terminadas todas las páginas, los objetos por página
se concatenan en el
v2-ingest/{job_id}.jsonlfinal, los objetos de preparación se eliminan, el trabajo pasa acompletedy se dispara el webhook de finalización firmado si se estableció unwebhook_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,cookiesniheaders. 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.
POSTy elGET/DELETEpor trabajo usanresponse_model_exclude_none, por lo que los campos que aún sonnull(comooutput_urlantes de completarse) se omiten del JSON en lugar de enviarse comonull.
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.