API per il crawl di siti web per il RAG#

POST /v2/ingest esegue il crawl di un sito web per il RAG: trasforma un sito (oppure un elenco esplicito di URL) in chunk pronti per il RAG ed emette un unico file JSONL che si carica direttamente in LangChain JSONLoader, LlamaIndex SimpleDirectoryReader o in un import massivo su vector DB. L'endpoint è sempre asincrono: POST risponde 202 con un job_id, tu interroghi GET /v2/ingest/{job_id} oppure registri un webhook_url, e un job completato restituisce un output_url pre-firmato per il JSONL. EnConvert si occupa della discovery, del rendering in headless Chrome, del chunking basato sui titoli e dell'assemblaggio del JSONL in un unico job.

Anche i file caricati vengono ingeriti attraverso la stessa identica pipeline tramite POST /v2/ingest/files: PDF, DOCX, PPTX, XLSX, CSV, HTML, EPUB e altri vengono convertiti in Markdown, suddivisi in chunk e assemblati nello stesso JSONL. Una sola integrazione copre l'ingestione RAG sia dal web sia dai file.

Ecco la chiamata utile più piccola. Fai il crawl di un sito e suddividi in chunk ogni pagina che scopre:

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 risposta è il record del job, restituito con 202 Accepted. Nota che lo stato è queued e che output_url è assente finché il job non si 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"
}

L'ingestione è sempre asincrona. Ogni pagina viene renderizzata in un browser reale, con tempi di 10–30 secondi per URL, ben oltre la finestra di richiesta di 300 secondi per qualunque job non banale. Perciò POST risponde 202 con un job_id, e un worker locale sul droplet smaltisce il job fuori banda. Tu interroghi GET /v2/ingest/{job_id} per lo stato di avanzamento, oppure registri un webhook_url per essere avvisato quando finisce.


Endpoint#

Metodo Path Scopo
POST /v2/ingest Crea un job di ingestione web (elenco di URL, sitemap o crawl). Risponde 202 con un job_id.
POST /v2/ingest/files Crea un job di ingestione file da documenti caricati (multipart). Stessa pipeline job + JSONL.
GET /v2/ingest Elenco dei job di questo progetto, dal più recente, con paginazione skip/limit.
GET /v2/ingest/{job_id} Stato del ciclo di vita di un singolo job, con un output_url firmato di fresco una volta completato.
DELETE /v2/ingest/{job_id} Annulla un job. Il worker vede lo stato annullato e si ferma tra una pagina e l'altra.
POST /v2/ingest/{job_id}/retry-webhook Rifirma e ri-invia in POST il webhook di completamento per un job completato.
GET /v2/ingest/webhook-secret Rivela il secret di firma dei webhook del progetto (canale dashboard).
POST /v2/ingest/webhook-secret/rotate Ruota il secret di firma. Le vecchie firme smettono di verificarsi immediatamente.

Content-Type: application/json su ogni POST.


Autenticazione#

Autenticati con una chiave privata nell'header X-API-Key per le chiamate server-to-server. È il metodo usato dagli esempi qui sotto.

X-API-Key: sk_your_private_key

Funzionano anche le chiavi pubbliche con un bearer token JWT, seguendo lo stesso flusso di ogni altro endpoint: genera un token con la tua chiave pk_, poi invialo come Authorization: Bearer <token>. Il flusso completo, incluso il domain locking e il refresh del token, è nella guida all'autenticazione.

Ogni chiave API porta con sé un'allowlist di endpoint consentiti. Se /v2/ingest non è nell'elenco della chiave, la richiesta viene rifiutata con 403. Una chiave limitata a /v2/ingest raggiunge comunque i job che ha creato: GET e DELETE /v2/ingest/{job_id} e POST /v2/ingest/{job_id}/retry-webhook sono sempre consentiti per un job_id (la forma ing_… viene riconosciuta esplicitamente). L'endpoint di elenco statico e le due route di gestione webhook-secret non ereditano questa deroga; richiedono un token più ampio o con scope dashboard.


Come funziona l'ingestione#

Un job attraversa cinque fasi, tutte durevoli e sicure al riavvio. Se il processo worker si riavvia a metà job, il job viene rimesso in coda all'avvio e riprende dalla pagina su cui si era fermato: le pagine già completate mantengono il loro output in staging e non vengono mai renderizzate né fatturate di nuovo.

  1. Queue. POST valida la richiesta, esegue un rapido controllo di ops units=1 (il piano ha l'ingestione abilitata e margine nella quota mensile di ops), inserisce la riga del job e risponde 202. Nulla viene persistito se il gate di ops fallisce: un 402 non lascia alcuna riga.
  2. Discover. Per le modalità sitemap e crawl il worker esegue lo stesso passaggio di discovery dell'endpoint discover, limitato a max_pages, e sottopone l'URL seed a screening SSRF. Per la modalità urls l'elenco esplicito viene deduplicato nell'ordine dato; non viene eseguita alcuna discovery. La dimensione della discovery prima del limite viene riportata come pages_found; quando il sito ha più URL di quanti max_pages abbia consentito, discovery_truncated è true e una voce in warnings riporta entrambi i numeri, così pages_discovered (il conteggio degli elementi in coda) non viene mai scambiato per la dimensione del sito.
  3. Render and chunk. Ogni URL viene renderizzato attraverso il singleton condiviso di headless Chrome, cioè la stessa pipeline di rendering dietro l'endpoint perceive. Poi l'HTML renderizzato viene convertito in fit-Markdown e suddiviso dal chunker basato sui titoli. I rendering vengono eseguiti in sequenza, una pagina alla volta.
  4. Stage. I chunk di ogni pagina vengono scritti in un oggetto JSONL per pagina nello storage, con chiave deterministica (project, job, url). È questo che rende economico un riavvio: un job ripreso riutilizza le pagine in staging invece di renderizzarle di nuovo.
  5. Assemble. Una volta terminata ogni pagina, gli oggetti per pagina vengono concatenati nel v2-ingest/{job_id}.jsonl finale, gli oggetti di staging vengono eliminati, il job passa a completed e, se era stato impostato un webhook_url, parte il webhook di completamento firmato.

La quota di ops viene ricontrollata per pagina all'interno del worker, non solo al momento dell'invio; ogni pagina completata addebita una op. Un job crawl il cui numero di pagine è ignoto in partenza si ferma in modo pulito al tuo tetto mensile: le pagine già renderizzate vengono fatturate e conservate, e le pagine restanti vengono marcate skipped anziché sforare la spesa.

I rendering di ingestione sono privi di credenziali per progettazione. A differenza di /v2/perceive, non accetta auth, cookies o headers personalizzati: nessun segreto viene persistito per la ripresa durevole, quindi lo stato del job su disco non trasporta mai credenziali.


Ingestione dei file#

POST /v2/ingest esegue il crawl del web; POST /v2/ingest/files ingerisce i file caricati attraverso la stessa identica pipeline. Entrambi creano lo stesso job, usano lo stesso chunker basato sui titoli e producono lo stesso unico deliverable JSONL, così una sola integrazione copre l'ingestione RAG dal web e dai file.

Invia i documenti come multipart/form-data nel campo files. Ogni file viene convertito in Markdown dal convertitore anything-to-markdown, poi suddiviso in chunk e assemblato esattamente come una pagina sottoposta a crawl. Viene accettato ogni formato di input supportato: PDF, DOCX, PPTX, XLSX, CSV, HTML, EPUB, OpenDocument e testo semplice/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 risposta è lo stesso IngestJobResponse dell'endpoint di crawl, con mode impostato su 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"
}

Ogni file caricato conta come una «pagina»: fattura una op, incrementa pages_processed man mano che si completa ed è etichettato con il suo nome file nel metadata.source_url del JSONL. Tu interroghi GET /v2/ingest/{job_id}, annulli con DELETE e ricevi il webhook di completamento firmato esattamente come per un job di crawl. I file vengono conservati solo finché il JSONL non è assemblato, poi vengono eliminati.

Parametri della richiesta file#

Inviati come campi form multipart (non un body JSON):

Campo Tipo Default Descrizione
files file[] -- Uno o più documenti da ingerire. 1–200 file per richiesta; la dimensione di ciascuno viene controllata rispetto al limite di upload del tuo piano.
max_words integer 512 Tetto morbido di parole per chunk. 32–4,000. I blocchi di codice e le tabelle pipe restano atomici.
sentence_overlap integer 1 Frasi ripetute tra chunk di prosa consecutivi della stessa sezione. 0–10.
webhook_url string null Callback di completamento firmato in HMAC, con la stessa firma e la stessa policy di retry dei webhook di completamento più sotto.

Un tipo di file non supportato, un file vuoto o un'immagine (l'OCR non viene eseguito) viene rifiutato all'invio con un 400; un file oltre il limite di dimensione per file del tuo piano dà 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

Parametri della richiesta#

Sorgente e modalità#

Parametro Tipo Default Descrizione
mode string "urls" urls, sitemap o crawl. Seleziona come viene costruito l'insieme di URL.
url string null URL seed per la modalità sitemap/crawl. Deve iniziare con http:// o https://. Massimo 2,048 caratteri. Obbligatorio per quelle modalità; rifiutato in modalità urls.
urls string[] null URL espliciti da ingerire in modalità urls. Non vuoto, massimo 1,000 voci, ciascuna http(s) e ≤ 2,048 caratteri. Obbligatorio per la modalità urls; rifiutato in modalità sitemap/crawl.

url e urls sono mutuamente esclusivi: indica esattamente una sorgente. La modalità urls richiede urls; sitemap e crawl richiedono un url seed. Inviare quello sbagliato per la modalità dà un 422.

Discovery (modalità sitemap / crawl)#

Questi vengono inoltrati al passaggio di discovery e ignorati in modalità urls.

Parametro Tipo Default Descrizione
max_pages integer 50 Tetto sugli URL individuati e ingeriti. 1–1,000.
max_depth integer 2 Profondità dei link nel crawl a partire dal seed. 1–5.
same_domain_only boolean true Limita la discovery al dominio del seed.
include_patterns string[] [] Pattern regex che un URL deve soddisfare per essere mantenuto. Massimo 50. Ciascuno viene compilato all'invio; un pattern errato dà un 422.
exclude_patterns string[] [] Pattern regex che scartano un URL corrispondente. Massimo 50.
respect_robots boolean false Quando è true, un URL non consentito dal robots.txt del sito viene saltato.

Rendering#

Parametro Tipo Default Descrizione
wait_for string null Dopo la navigazione, attende un selettore CSS o un'espressione JS prima di catturare. Massimo 1,024 caratteri.
wait_timeout_ms integer 30000 Per quanto tempo wait_for può attendere, in millisecondi. 0–60,000.

Nota. L'ingestione non accetta auth, cookies o headers. Se una pagina ha bisogno di credenziali per essere renderizzata, l'ingestione è lo strumento sbagliato: per quella singola pagina usa l'endpoint perceive, che offre l'intera superficie di richiesta autenticata.

Chunking (oggetto chunk)#

Parametro Tipo Default Vincoli Descrizione
max_words integer 512 32–4,000 Tetto morbido di parole per chunk. Basato sui titoli. I blocchi di codice e le tabelle pipe restano atomici e possono superarlo.
sentence_overlap integer 1 0–10 Frasi ripetute tra chunk di prosa consecutivi della stessa sezione. 0 disabilita la sovrapposizione. La sovrapposizione non attraversa mai un confine di titolo.

Il chunker suddivide sui titoli #, ## e ###: ogni chunk appartiene esattamente a una sezione e porta con sé il suo percorso completo dei titoli. I titoli più profondi (##########) restano inline come contenuto. I blocchi di codice recintati e le tabelle Markdown non vengono mai suddivisi, anche quando un singolo blocco supera max_words; gli elementi di elenco si suddividono tra un elemento e l'altro, mai a metà elemento.

Webhook#

Parametro Tipo Default Descrizione
webhook_url string null Endpoint che riceve il callback di completamento firmato in HMAC. Massimo 2,048 caratteri. Lo schema viene controllato all'invio; lo screening SSRF avviene al momento della consegna, non all'invio.

Risposta#

POST, GET /v2/ingest/{job_id} e DELETE restituiscono tutti lo stesso oggetto IngestJobResponse.

Campo Tipo Descrizione
job_id string ID opaco (ing_…). Usalo con gli endpoint GET/DELETE e citalo al supporto.
status string queued, discovering, processing, completed, failed o canceled.
mode string La modalità che hai inviato: urls, sitemap, crawl o files.
pages_discovered integer Elementi effettivamente messi in coda dal job: URL (l'elenco esplicito, o il risultato della discovery limitato a max_pages) o file caricati. pages_processed + pages_failed sommano a questo valore una volta che il job è terminale.
pages_found integer URL unici idonei che la discovery ha prodotto prima del limite max_pages. Per i job sitemap è il vero conteggio unico del sito; per i job crawl è un limite inferiore (il crawl smette di scaricare al raggiungimento del limite). Assente per i job urls e files.
discovery_truncated boolean true quando la discovery ha trovato più URL unici di quanti max_pages abbia permesso al job di mettere in coda. Una voce in warnings riporta entrambi i numeri; aumenta max_pages per ingerire una parte maggiore del sito.
pages_processed integer URL la cui sequenza render → chunk → stage è completata.
pages_failed integer URL che non sono riusciti a renderizzarsi o sono stati saltati (es. quota di ops esaurita).
total_chunks integer Chunk totali scritti su tutte le pagine completate. Corrisponde al numero di righe del JSONL.
output_url string URL di download pre-firmato per il JSONL finale. Presente solo quando status è completed; scade dopo 15 minuti.
error_message string Impostato quando status è failed (es. discovery rifiutata, tutte le pagine fallite).
webhook_url string La destinazione del webhook di completamento registrata per questo job, se presente.
webhook_delivered boolean true una volta che il webhook di completamento firmato ha ricevuto un 2xx.
created_at string Quando il job è stato creato (UTC).
completed_at string Quando il job ha raggiunto uno stato terminale (UTC).
warnings string[] Note non fatali, es. troncamento della discovery: "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 e i GET/DELETE per singolo job usano response_model_exclude_none, quindi i campi che sono ancora null (come output_url prima del completamento) vengono omessi dal JSON anziché inviati come null.

La forma del record JSONL#

Il file finale è JSON delimitato da newline. Ogni riga è un chunk:

{"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 Descrizione
id string Deterministico per (source_url, chunk_index): <md5(url)[:12]>-<index:04d>. Una riesecuzione produce id identici.
content string Il testo del chunk recuperabile. Corrisponde a Document.page_content in LangChain.
metadata.source_url string La pagina da cui proviene il chunk.
metadata.title string Il <title> della pagina, con fallback al primo <h1>, limitato a 512 caratteri.
metadata.headings_path string[] Il percorso h1 → h2 → h3 sotto cui si trova il chunk.
metadata.section string Il testo del titolo più interno (l'ultima voce di headings_path).
metadata.word_count integer Conteggio di parole di content delimitate da spazi.
metadata.chunk_index integer L'indice del chunk all'interno della sua pagina.

Il file è UTF-8, scritto con ensure_ascii=false, così l'unicode resta leggibile. Poiché content è una stringa di primo livello e metadata è un oggetto affiancato, lo stesso file si carica tramite LangChain JSONLoader(content_key="content", json_lines=True), LlamaIndex SimpleDirectoryReader e qualunque import vector-DB orientato alle righe senza rimodellare nulla.


Ciclo di vita del job e polling#

Un job attraversa questi stati:

queued → discovering → processing → completed | failed | canceled
Status Significato
queued Accettato e in attesa del worker.
discovering Esegue il passaggio di discovery sitemap/crawl (solo sitemap/crawl).
processing Rendering e chunking delle pagine. pages_processed e total_chunks crescono in tempo reale.
completed Il JSONL finale è assemblato; output_url è firmato e pronto.
failed La discovery è stata rifiutata, oppure ogni pagina è fallita o è stata saltata. error_message lo spiega.
canceled Un DELETE ha raggiunto il job prima che finisse.

Interroga lo stato con il GET per singolo job. È di sola lettura, non consuma ops e rifirma l'output_url dalla chiave dell'oggetto memorizzato a ogni chiamata:

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

Un job_id sconosciuto, o appartenente a un progetto diverso, restituisce 404. L'esistenza non viene mai rivelata tra progetti diversi.

Elenco dei job#

GET /v2/ingest restituisce i job di questo progetto dal più recente, con i query param skip e limit. limit vale per default 20 ed è limitato a 100. La risposta porta un flag has_more invece di un conteggio totale:

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
}

Le righe dell'elenco riducono webhook_url a un booleano webhook_configured: l'elenco non ripete mai l'endpoint grezzo nella tabella.

Annullamento di un job#

DELETE /v2/ingest/{job_id} imposta lo stato del job su canceled. Il worker legge quello stato tra una pagina e l'altra e si ferma senza assemblare l'output. L'annullamento è idempotente e a prova di race: se l'assemblaggio è già stato confermato, il DELETE non trova corrispondenze e il job viene restituito invariato come completed. Un job terminato non viene mai riportato a canceled.

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

Webhook di completamento#

Imposta webhook_url sul POST ed EnConvert invia un POST firmato in HMAC quando il job si completa. Il payload è JSON compatto, con chiavi ordinate:

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

La consegna ritenta fino a tre volte dopo il primo tentativo, con ritardi di back-off di 1, 4 e 16 secondi, cioè quattro POST nel caso peggiore. Ogni tentativo viene rifirmato con un timestamp aggiornato, così una catena di retry lenta non esce mai dalla finestra di freschezza del consumatore. Una risposta 2xx è un successo; un endpoint morto viene registrato come mancata consegna (e genera un alert nella dashboard), non affonda mai un job per il resto completato.

Il webhook_url viene sottoposto a screening SSRF al momento della consegna, non all'invio. Un URL che si risolve in un indirizzo privato, di loopback o di metadata viene memorizzato in modo inerte e rifiutato solo quando EnConvert prova a inviargli un POST.

Verifica della firma#

Ogni consegna porta due header:

Header Valore
X-Enconvert-Signature sha256=<hex>, cioè l'HMAC-SHA256 di <timestamp>.<raw body>.
X-Enconvert-Timestamp Il timestamp in secondi unix legato alla firma.

L'input di firma è il timestamp, un . letterale, poi il body grezzo della richiesta. Legare il timestamp al MAC significa che un consumatore che rifiuta timestamp scaduti ottiene gratis la protezione dai replay. La finestra di freschezza predefinita è di 300 secondi. Verifica nel tuo handler:

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)

Gestione del secret di firma#

GET /v2/ingest/webhook-secret rivela il secret del progetto (creandolo alla prima chiamata) insieme ai nomi degli header e alla tolleranza di cui il tuo consumatore ha bisogno. È sensibile ed esposto solo tramite il canale dashboard autenticato:

{
    "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 emette un nuovo secret e imposta rotated su true. Ogni firma calcolata con il secret precedente smette di verificarsi nel momento in cui la rotazione viene confermata. Ruota dopo un sospetto leak, poi aggiorna il tuo consumatore.

Riconsegna di un webhook#

Se il tuo endpoint era offline quando il job è terminato, POST /v2/ingest/{job_id}/retry-webhook rifirma e ri-invia in POST con la stessa policy di retry:

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)."
}

Restituisce 404 per un job_id sconosciuto o estraneo, 400 quando non è configurato alcun webhook_url (o l'URL memorizzato ora si risolve in un indirizzo privato/interno) e 409 quando il job non ha raggiunto completed.


Esempi di codice#

curl: elenco esplicito di URL#

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: crawl con chunking e 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: invio, polling, download#

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: invio e polling#

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]);
}

Risposte di errore#

Status Condizione
202 Accepted Il job è stato creato e messo in coda. È il normale esito del POST.
401 Unauthorized Chiave API / token JWT mancante o non valido.
402 Payment Required L'ingestione non è nel tuo piano attuale, oppure la tua quota mensile di ops è esaurita.
403 Forbidden /v2/ingest non è tra gli endpoint consentiti della chiave API.
404 Not Found job_id sconosciuto, o appartenente a un altro progetto.
409 Conflict retry-webhook chiamato su un job che non ha raggiunto completed.
400 Bad Request retry-webhook chiamato senza alcun webhook_url configurato, o il cui URL memorizzato ora si risolve in un indirizzo privato/interno.
422 Unprocessable Entity La sorgente non corrisponde alla modalità (urls senza urls, o un url seed in modalità urls); un parametro è fuori intervallo; oppure una regex include_patterns/exclude_patterns non compila.
500 Internal Server Error Il job non ha potuto essere creato. Il messaggio include il job_id da citare al supporto.

Un errore di rendering a livello di pagina non fa fallire la richiesta né il job: incrementa pages_failed, registra l'errore della pagina nella propria riga e il job continua. Un job fails soltanto quando la discovery viene rifiutata oppure ogni pagina fallisce o viene saltata. Il riferimento completo dei codici di stato è nella guida ai codici di errore.


Limiti#

Limite Valore
URL per richiesta in modalità urls 1,000
Lunghezza di url / di ogni voce urls 2,048 caratteri
max_pages (tetto della discovery) 1–1,000
max_depth 1–5
include_patterns / exclude_patterns 50 ciascuno
Lunghezza di wait_for 1,024 caratteri
wait_timeout_ms 0–60,000 ms
chunk.max_words 32–4,000 (default 512)
chunk.sentence_overlap 0–10 (default 1)
Lunghezza di webhook_url 2,048 caratteri
Tetto di pagine per job (MAX_PAGES_PER_JOB) 1,000
File per richiesta /v2/ingest/files 1–200
Dimensione di upload per file Dipende dal piano (Founding: 5 MB)
limit dell'elenco GET /v2/ingest 1–100 (default 20)
Scadenza dell'output_url firmato 15 minuti
Tentativi di consegna del webhook 4 (iniziale + 3 retry)
Tolleranza di replay del webhook 300 secondi
Ops mensili (condivise tra tutti gli endpoint, 1 per pagina) 500 / 3.000 / 15.000 / 50.000 per livello; vedi i prezzi

Domande frequenti#

Come faccio il crawl di un sito web per il RAG con un'API?#

Invia POST /v2/ingest con mode: "crawl" e un url seed. La chiamata risponde 202 con un job_id; il worker individua le pagine, renderizza ciascuna in headless Chrome, suddivide il Markdown in chunk basati sui titoli e assembla un unico file JSONL che scarichi dall'output_url firmato.

Come ingerisco file (PDF, documenti Word) per il RAG?#

Invia POST /v2/ingest/files come multipart/form-data con uno o più files. Ogni documento viene convertito in Markdown, suddiviso in chunk basati sui titoli e assemblato nello stesso unico JSONL di un job di crawl: una sola pipeline per web e file. Sono supportati i file PDF, DOCX, PPTX, XLSX, CSV, HTML, EPUB, OpenDocument e testo semplice/Markdown (fino a 200 per richiesta); l'elenco completo è sulla pagina anything-to-markdown.

L'output JSONL si carica direttamente in LangChain e LlamaIndex?#

Sì. Ogni riga porta una stringa content di primo livello con un oggetto metadata affiancato, così lo stesso file si carica tramite LangChain JSONLoader(content_key="content", json_lines=True), LlamaIndex SimpleDirectoryReader e qualunque import vector-DB orientato alle righe senza rimodellare nulla.

Come suddivide il chunker le pagine in chunk per il RAG?#

Suddivide sui titoli #, ## e ### con un tetto morbido max_words (default 512, intervallo 32–4,000) e un sentence_overlap opzionale. I blocchi di codice recintati e le tabelle Markdown non vengono mai suddivisi, e ogni chunk porta con sé il suo headings_path completo.

Come vengo avvisato quando un job di ingestione finisce?#

Imposta webhook_url sul POST ed EnConvert invia un callback firmato in HMAC (header X-Enconvert-Signature e X-Enconvert-Timestamp) con fino a tre retry dopo il primo tentativo. Se il tuo endpoint era offline, POST /v2/ingest/{job_id}/retry-webhook lo rifirma e lo riconsegna.

Perché output_url manca dalla mia risposta di ingestione?#

output_url è presente solo quando status è completed: la risposta del POST è un job queued con il campo omesso. Interroga GET /v2/ingest/{job_id}, che non consuma ops e rifirma l'URL a ogni chiamata; ogni URL firmato scade dopo 15 minuti.