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.
- Queue.
POSTvalida la richiesta, esegue un rapido controllo di opsunits=1(il piano ha l'ingestione abilitata e margine nella quota mensile di ops), inserisce la riga del job e risponde202. Nulla viene persistito se il gate di ops fallisce: un402non lascia alcuna riga. - Discover. Per le modalità
sitemapecrawlil worker esegue lo stesso passaggio di discovery dell'endpoint discover, limitato amax_pages, e sottopone l'URL seed a screening SSRF. Per la modalitàurlsl'elenco esplicito viene deduplicato nell'ordine dato; non viene eseguita alcuna discovery. La dimensione della discovery prima del limite viene riportata comepages_found; quando il sito ha più URL di quantimax_pagesabbia consentito,discovery_truncatedètruee una voce inwarningsriporta entrambi i numeri, cosìpages_discovered(il conteggio degli elementi in coda) non viene mai scambiato per la dimensione del sito. - 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.
- 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. - Assemble. Una volta terminata ogni pagina, gli oggetti per pagina
vengono concatenati nel
v2-ingest/{job_id}.jsonlfinale, gli oggetti di staging vengono eliminati, il job passa acompletede, se era stato impostato unwebhook_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,cookiesoheaders. 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.
POSTe iGET/DELETEper singolo job usanoresponse_model_exclude_none, quindi i campi che sono ancoranull(comeoutput_urlprima del completamento) vengono omessi dal JSON anziché inviati comenull.
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.