API de crawl de site web pour le RAG#
POST /v2/ingest crawle un site web pour le RAG : il transforme un site (ou
une liste explicite d'URL) en chunks prêts pour le RAG et produit un seul
fichier JSONL qui se charge directement dans LangChain JSONLoader, LlamaIndex
SimpleDirectoryReader, ou un import en masse vers une base vectorielle.
L'endpoint est toujours asynchrone : POST répond 202 avec un job_id, vous
interrogez GET /v2/ingest/{job_id} ou enregistrez un webhook_url, et un job
terminé renvoie une output_url pré-signée pour le JSONL. EnConvert effectue la
découverte, le rendu en Chrome headless, le découpage tenant compte des titres,
et l'assemblage du JSONL en un seul job.
Les fichiers téléversés sont ingérés par exactement le même pipeline via
POST /v2/ingest/files. PDF, DOCX, PPTX, XLSX, CSV,
HTML, EPUB et bien d'autres sont convertis en Markdown, découpés en chunks et
assemblés dans le même JSONL. Une seule intégration couvre l'ingestion RAG à la
fois du web et des fichiers.
Voici l'appel utile le plus simple. Crawlez un site et découpez en chunks chaque page qu'il découvre :
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 réponse est l'enregistrement du job, renvoyé avec 202 Accepted. Notez que le
statut est queued et que output_url est absent tant que le job n'est pas
terminé :
{
"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'ingestion est toujours asynchrone. Chaque page est rendue dans un vrai
navigateur, ce qui prend 10–30 secondes par URL, bien au-delà de la fenêtre de
requête de 300 secondes pour tout job non trivial. Ainsi, POST répond 202
avec un job_id, et un worker local à la droplet traite le job en arrière-plan.
Vous interrogez GET /v2/ingest/{job_id} pour suivre la progression, ou
enregistrez un webhook_url pour être averti à la fin.
Endpoints#
| Method | Path | Purpose |
|---|---|---|
POST |
/v2/ingest |
Crée un job d'ingestion web (liste d'URL, sitemap ou crawl). Répond 202 avec un job_id. |
POST |
/v2/ingest/files |
Crée un job d'ingestion de fichiers à partir de documents téléversés (multipart). Même pipeline job + JSONL. |
GET |
/v2/ingest |
Liste des jobs de ce projet, du plus récent au plus ancien, avec pagination skip/limit. |
GET |
/v2/ingest/{job_id} |
Statut du cycle de vie d'un job, avec une output_url fraîchement signée une fois terminé. |
DELETE |
/v2/ingest/{job_id} |
Annule un job. Le worker voit le statut annulé et s'arrête entre deux pages. |
POST |
/v2/ingest/{job_id}/retry-webhook |
Re-signe et re-POST le webhook de fin pour un job terminé. |
GET |
/v2/ingest/webhook-secret |
Révèle le secret de signature webhook du projet (canal tableau de bord). |
POST |
/v2/ingest/webhook-secret/rotate |
Fait tourner le secret de signature. Les anciennes signatures cessent immédiatement d'être valides. |
Content-Type : application/json sur chaque POST.
Authentification#
Authentifiez-vous avec une clé privée dans l'en-tête X-API-Key pour les appels
serveur à serveur. C'est la méthode utilisée par les exemples ci-dessous.
X-API-Key: sk_your_private_key
Les clés publiques avec un jeton bearer JWT fonctionnent aussi, selon le même
flux que tous les autres endpoints : générez un jeton avec votre clé pk_, puis
envoyez-le sous forme Authorization: Bearer <token>. Le flux complet, y compris
le verrouillage de domaine et le rafraîchissement du jeton, est décrit dans le
guide d'authentification.
Chaque clé API porte une liste blanche des endpoints autorisés. Si /v2/ingest
n'est pas sur la liste de la clé, la requête est rejetée avec 403. Une clé
limitée à /v2/ingest atteint quand même les jobs qu'elle a créés : GET et
DELETE /v2/ingest/{job_id} ainsi que POST /v2/ingest/{job_id}/retry-webhook
sont toujours autorisés pour un job_id (la forme ing_… est reconnue
explicitement). L'endpoint de liste statique et les deux routes de gestion
webhook-secret n'héritent pas de cette dérogation ; ils exigent un jeton
plus large ou limité au tableau de bord.
Fonctionnement de l'ingestion#
Un job passe par cinq phases, toutes durables et résistantes au redémarrage. Si le processus worker redémarre en cours de job, le job est réinséré dans la file au démarrage et reprend à la page où il s'était arrêté. Les pages déjà terminées conservent leur sortie stockée et ne sont jamais re-rendues ni refacturées.
- Mise en file.
POSTvalide la requête, effectue une vérification rapide d'opsunits=1(le plan a l'ingestion activée et de la marge dans le quota mensuel d'ops), insère la ligne du job et répond202. Rien n'est persisté si la barrière d'ops échoue : un402ne laisse aucune ligne derrière lui. - Découverte. Pour les modes
sitemapetcrawl, le worker effectue la même passe de découverte que l'endpoint discover, plafonnée àmax_pages, et filtre l'URL de départ contre les SSRF. Pour le modeurls, la liste explicite est dédupliquée dans l'ordre ; aucune découverte n'est exécutée. La taille de la découverte avant plafonnement est rapportée danspages_found; lorsque le site compte plus d'URL quemax_pagesn'en autorise,discovery_truncatedvauttrueet une entrée dewarningsindique les deux nombres, afin quepages_discovered(le nombre mis en file) ne soit jamais confondu avec la taille du site. - Rendu et découpage. Chaque URL est rendue via le singleton Chrome headless partagé, le même pipeline de rendu que l'endpoint perceive. Le HTML rendu est ensuite converti en fit-Markdown et découpé par le chunker tenant compte des titres. Les rendus s'exécutent séquentiellement, une page à la fois.
- Stockage intermédiaire. Les chunks de chaque page sont écrits dans un objet
JSONL par page dans le stockage, clé déterministe par
(project, job, url). C'est ce qui rend un redémarrage peu coûteux : un job repris réutilise les pages stockées au lieu de les re-rendre. - Assemblage. Une fois chaque page terminée, les objets par page sont
concaténés dans le
v2-ingest/{job_id}.jsonlfinal, les objets intermédiaires sont supprimés, le job passe àcompleted, et le webhook de fin signé est déclenché si unwebhook_urla été défini.
Le quota d'ops est revérifié par page à l'intérieur du worker, pas
seulement au moment de la soumission ; chaque page aboutie facture une op. Un job crawl dont le nombre de pages est
inconnu au départ s'arrête proprement à votre plafond mensuel : les pages déjà
rendues sont facturées et conservées, et les pages restantes sont marquées
skipped plutôt que de dépasser le budget.
Les rendus d'ingestion sont sans identifiants par conception. Contrairement à
/v2/perceive, il n'accepte ni auth, ni cookies, ni headers personnalisés.
Aucun secret n'est persisté pour la reprise durable, de sorte que l'état du job
sur disque ne transporte jamais d'identifiants.
Ingestion de fichiers#
POST /v2/ingest crawle le web ; POST /v2/ingest/files ingère des fichiers
téléversés via exactement le même pipeline. Les deux créent le même job,
exécutent le même chunker tenant compte des titres et produisent le même livrable
JSONL unique, si bien qu'une seule intégration couvre l'ingestion RAG du web et
des fichiers.
Envoyez les documents en multipart/form-data dans le champ files. Chaque
fichier est converti en Markdown par le convertisseur anything-to-markdown,
puis découpé et assemblé exactement comme une page crawlée. Tous les
formats d'entrée pris en charge
sont acceptés : PDF, DOCX, PPTX, XLSX, CSV, HTML, EPUB, OpenDocument et texte
brut/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 réponse est le même IngestJobResponse que l'endpoint de crawl, avec mode
réglé sur 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"
}
Chaque fichier téléversé compte comme une « page » : il facture une op,
fait grimper pages_processed à mesure qu'il se termine, et est
identifié par son nom de fichier dans le metadata.source_url du JSONL. Vous
interrogez GET /v2/ingest/{job_id}, annulez avec DELETE et recevez le webhook
de fin signé exactement comme pour un job de crawl. Les fichiers ne sont stockés
que jusqu'à l'assemblage du JSONL, puis supprimés.
Paramètres de la requête de fichiers#
Envoyés en champs de formulaire multipart (pas un corps JSON) :
| Field | Type | Default | Description |
|---|---|---|---|
files |
file[] | -- | Un ou plusieurs documents à ingérer. 1–200 fichiers par requête ; chacun est vérifié en taille par rapport à la limite de téléversement de votre plan. |
max_words |
integer |
512 |
Plafond souple de mots par chunk. 32–4,000. Les blocs de code et les tableaux à barres verticales restent atomiques. |
sentence_overlap |
integer |
1 |
Phrases répétées entre deux chunks de prose consécutifs d'une même section. 0–10. |
webhook_url |
string |
null |
Callback de fin signé en HMAC, avec la même politique de signature et de réessai que les webhooks de fin ci-dessous. |
Un type de fichier non pris en charge, un fichier vide ou une image (l'OCR n'est
pas effectué) est rejeté à la soumission avec un 400 ; un fichier dépassant la
limite de taille par fichier de votre plan est 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
Paramètres de la requête#
Source et mode#
| Parameter | Type | Default | Description |
|---|---|---|---|
mode |
string |
"urls" |
urls, sitemap ou crawl. Détermine comment l'ensemble d'URL est construit. |
url |
string |
null |
URL de départ pour le mode sitemap/crawl. Doit commencer par http:// ou https://. Maximum 2,048 caractères. Requise pour ces modes ; rejetée en mode urls. |
urls |
string[] |
null |
URL explicites à ingérer en mode urls. Non vide, maximum 1,000 entrées, chacune en http(s) et ≤ 2,048 caractères. Requise pour le mode urls ; rejetée en mode sitemap/crawl. |
url et urls sont mutuellement exclusifs : envoyez exactement une source.
Le mode urls exige urls ; sitemap et crawl exigent une url de
départ. Envoyer le mauvais pour le mode donné produit un 422.
Découverte (modes sitemap / crawl)#
Ceux-ci sont transmis à la passe de découverte et ignorés en mode urls.
| Parameter | Type | Default | Description |
|---|---|---|---|
max_pages |
integer |
50 |
Plafond des URL découvertes et ingérées. 1–1,000. |
max_depth |
integer |
2 |
Profondeur de crawl des liens depuis le point de départ. 1–5. |
same_domain_only |
boolean |
true |
Restreint la découverte au domaine du point de départ. |
include_patterns |
string[] |
[] |
Motifs regex qu'une URL doit satisfaire pour être conservée. Maximum 50. Chacun est compilé à la soumission ; un motif invalide produit un 422. |
exclude_patterns |
string[] |
[] |
Motifs regex qui écartent une URL correspondante. Maximum 50. |
respect_robots |
boolean |
false |
Lorsque true, une URL interdite par le robots.txt du site est ignorée. |
Rendu#
| Parameter | Type | Default | Description |
|---|---|---|---|
wait_for |
string |
null |
Attend après la navigation un sélecteur CSS ou une expression JS avant la capture. Maximum 1,024 caractères. |
wait_timeout_ms |
integer |
30000 |
Durée maximale d'attente de wait_for, en millisecondes. 0–60,000. |
Note. L'ingestion n'accepte pas
auth,cookiesniheaders. Si une page a besoin d'identifiants pour s'afficher, l'ingestion n'est pas le bon outil. Utilisez l'endpoint perceive, qui offre toute la surface de requête authentifiée, pour cette page unique.
Découpage (objet chunk)#
| Parameter | Type | Default | Constraints | Description |
|---|---|---|---|---|
max_words |
integer |
512 |
32–4,000 | Plafond souple de mots par chunk. Tient compte des titres. Les blocs de code et les tableaux à barres verticales restent atomiques et peuvent le dépasser. |
sentence_overlap |
integer |
1 |
0–10 | Phrases répétées entre deux chunks de prose consécutifs d'une même section. 0 désactive le chevauchement. Le chevauchement ne franchit jamais une limite de titre. |
Le chunker découpe sur les titres #, ## et ###, donc chaque chunk appartient
à exactement une section et porte son chemin de titres complet. Les titres plus
profonds (####–######) restent en ligne comme contenu. Les blocs de code
délimités et les tableaux Markdown ne sont jamais découpés, même lorsqu'un seul
bloc dépasse max_words ; les éléments de liste se découpent entre éléments,
jamais au milieu d'un élément.
Webhook#
| Parameter | Type | Default | Description |
|---|---|---|---|
webhook_url |
string |
null |
Endpoint qui reçoit le callback de fin signé en HMAC. Maximum 2,048 caractères. Le schéma est vérifié à la soumission ; le filtrage SSRF a lieu au moment de la livraison, pas à la soumission. |
Réponse#
POST, GET /v2/ingest/{job_id} et DELETE renvoient tous le même objet
IngestJobResponse.
| Field | Type | Description |
|---|---|---|
job_id |
string |
ID opaque (ing_…). Utilisez-le avec les endpoints GET/DELETE et communiquez-le au support. |
status |
string |
queued, discovering, processing, completed, failed ou canceled. |
mode |
string |
Le mode que vous avez soumis : urls, sitemap, crawl ou files. |
pages_discovered |
integer |
Éléments effectivement mis en file par le job : URL (la liste explicite, ou le résultat de la découverte plafonné à max_pages), ou fichiers téléversés. pages_processed + pages_failed totalisent cette valeur une fois le job dans un statut terminal. |
pages_found |
integer |
URL éligibles uniques que la découverte a produites avant le plafond max_pages. Pour les jobs sitemap, c'est le vrai nombre d'URL uniques du site ; pour les jobs crawl, c'est une borne inférieure (le crawl cesse de récupérer des pages au plafond). Absent pour les jobs urls et files. |
discovery_truncated |
boolean |
true lorsque la découverte a trouvé plus d'URL uniques que max_pages n'a permis d'en mettre en file. Une entrée de warnings détaille les nombres ; augmentez max_pages pour ingérer davantage du site. |
pages_processed |
integer |
URL dont le rendu → découpage → stockage s'est terminé. |
pages_failed |
integer |
URL qui n'ont pas pu être rendues ou ont été ignorées (par ex. quota d'ops épuisé). |
total_chunks |
integer |
Nombre total de chunks écrits sur toutes les pages terminées. Correspond au nombre de lignes du JSONL. |
output_url |
string |
URL de téléchargement pré-signée pour le JSONL final. Présente uniquement lorsque status vaut completed ; expire après 15 minutes. |
error_message |
string |
Défini lorsque status vaut failed (par ex. découverte rejetée, toutes les pages en échec). |
webhook_url |
string |
La cible du webhook de fin enregistrée pour ce job, le cas échéant. |
webhook_delivered |
boolean |
true une fois que le webhook de fin signé a reçu un 2xx. |
created_at |
string |
Date de création du job (UTC). |
completed_at |
string |
Date à laquelle le job a atteint un statut terminal (UTC). |
warnings |
string[] |
Notes non fatales, par ex. troncature de la découverte : "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." |
Note.
POSTet lesGET/DELETEpar job utilisentresponse_model_exclude_none, de sorte que les champs encorenull(commeoutput_urlavant la fin) sont omis du JSON plutôt qu'envoyés commenull.
Forme de l'enregistrement JSONL#
Le fichier final est du JSON délimité par des sauts de ligne. Chaque ligne est 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}}
| Field | Type | Description |
|---|---|---|
id |
string |
Déterministe par (source_url, chunk_index) : <md5(url)[:12]>-<index:04d>. Une réexécution produit des ids identiques. |
content |
string |
Le texte du chunk récupérable. Correspond à Document.page_content dans LangChain. |
metadata.source_url |
string |
La page d'où provient le chunk. |
metadata.title |
string |
Le <title> de la page, à défaut le premier <h1>, plafonné à 512 caractères. |
metadata.headings_path |
string[] |
Le chemin h1 → h2 → h3 sous lequel se trouve le chunk. |
metadata.section |
string |
Le texte du titre le plus profond (la dernière entrée de headings_path). |
metadata.word_count |
integer |
Nombre de mots de content, délimités par des espaces. |
metadata.chunk_index |
integer |
L'index du chunk au sein de sa page. |
Le fichier est en UTF-8, écrit avec ensure_ascii=false, de sorte que l'unicode
reste lisible. Comme content est une chaîne de premier niveau et metadata un
objet frère, le même fichier se charge via LangChain
JSONLoader(content_key="content", json_lines=True), LlamaIndex
SimpleDirectoryReader et tout import vers une base vectorielle orienté ligne,
sans remise en forme.
Cycle de vie du job et interrogation#
Un job passe par ces états :
queued → discovering → processing → completed | failed | canceled
| Status | Meaning |
|---|---|
queued |
Accepté et en attente du worker. |
discovering |
Exécution de la passe de découverte sitemap/crawl (sitemap/crawl uniquement). |
processing |
Rendu et découpage des pages. pages_processed et total_chunks augmentent en direct. |
completed |
Le JSONL final est assemblé ; output_url est signée et prête. |
failed |
La découverte a été rejetée, ou toutes les pages ont échoué ou été ignorées. error_message explique. |
canceled |
Un DELETE a atteint le job avant sa fin. |
Interrogez le statut avec le GET par job. C'est en lecture seule : cela ne
consomme aucune op et re-signe l'output_url à partir de la clé d'objet
stockée à chaque appel :
curl https://api.enconvert.com/v2/ingest/ing_3f9a2c1b8e7d4a6f90b1c2d3e4f5a6b7 \
-H "X-API-Key: sk_your_private_key"
Un job_id inconnu, ou appartenant à un autre projet, renvoie 404.
L'existence n'est jamais divulguée d'un projet à l'autre.
Lister les jobs#
GET /v2/ingest renvoie les jobs de ce projet du plus récent au plus ancien,
avec les paramètres de requête skip et limit. limit vaut 20 par défaut et
est plafonné à 100. La réponse porte un drapeau has_more au lieu d'un
décompte 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
}
Les lignes de la liste réduisent webhook_url à un booléen webhook_configured,
si bien que la liste ne renvoie jamais l'endpoint brut dans le tableau.
Annuler un job#
DELETE /v2/ingest/{job_id} passe le statut du job à canceled. Le worker lit
ce statut entre deux pages et s'arrête sans assembler de sortie. L'annulation est
idempotente et à l'épreuve des conditions de course : si l'assemblage a déjà été
validé, le DELETE ne correspond à rien et le job est renvoyé inchangé comme
completed. Un job terminé n'est jamais ramené de force à canceled.
curl -X DELETE \
https://api.enconvert.com/v2/ingest/ing_3f9a2c1b8e7d4a6f90b1c2d3e4f5a6b7 \
-H "X-API-Key: sk_your_private_key"
Webhooks de fin#
Définissez webhook_url sur le POST et EnConvert envoie un POST signé en HMAC
lorsque le job se termine. La charge utile est un JSON compact, à clés triées :
{"job_id":"ing_3f9a...","output_url":"https://spaces.example.com/...signed...","pages_processed":41,"status":"completed","total_chunks":1187}
La livraison réessaie jusqu'à trois fois après la première tentative, avec des délais de back-off de 1, 4 et 16 secondes, soit quatre POST dans le pire des cas. Chaque tentative est re-signée avec un timestamp frais, de sorte qu'une chaîne de réessais lente ne dérive jamais au-delà de la fenêtre de fraîcheur du consommateur. Une réponse 2xx est un succès. Un endpoint mort est enregistré comme non-livraison et déclenche une alerte sur le tableau de bord, mais il ne fait jamais échouer un job par ailleurs terminé.
Le webhook_url est filtré contre les SSRF au moment de la livraison, pas à
la soumission. Une URL qui se résout en adresse privée, de loopback ou de
métadonnées est stockée de façon inerte et n'est rejetée que lorsque EnConvert
tente d'y faire un POST.
Vérifier la signature#
Chaque livraison porte deux en-têtes :
| Header | Value |
|---|---|
X-Enconvert-Signature |
sha256=<hex>, le HMAC-SHA256 de <timestamp>.<raw body>. |
X-Enconvert-Timestamp |
Le timestamp en secondes unix lié à la signature. |
L'entrée de signature est le timestamp, un . littéral, puis le corps brut de la
requête. Lier le timestamp au MAC signifie qu'un consommateur qui rejette les
timestamps périmés obtient gratuitement une protection contre le rejeu. La
fenêtre de fraîcheur par défaut est de 300 secondes. Vérifiez dans votre
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)
Gérer le secret de signature#
GET /v2/ingest/webhook-secret révèle le secret du projet (en le créant au
premier appel) ainsi que les noms d'en-têtes et la tolérance dont votre
consommateur a besoin. Il est sensible et n'est exposé que via le canal
authentifié du tableau de bord :
{
"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 émet un nouveau secret et met rotated à
true. Toute signature calculée avec le secret précédent cesse d'être valide dès
que la rotation est validée. Faites tourner le secret après une fuite suspectée,
puis mettez à jour votre consommateur.
Re-livrer un webhook#
Si votre endpoint était hors service à la fin du job,
POST /v2/ingest/{job_id}/retry-webhook re-signe et re-POST avec la même
politique de réessai :
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)."
}
Il renvoie 404 pour un job_id inconnu ou étranger, 400 lorsqu'aucun
webhook_url n'est configuré (ou que l'URL stockée se résout désormais en adresse
privée/interne), et 409 lorsque le job n'a pas atteint completed.
Exemples de code#
curl : liste d'URL explicite#
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 avec découpage et 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 : soumettre, interroger, télécharger#
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 : soumettre et interroger#
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]);
}
Réponses d'erreur#
| Status | Condition |
|---|---|
202 Accepted |
Le job a été créé et mis en file. C'est le résultat normal d'un POST. |
401 Unauthorized |
Clé API / jeton JWT manquant ou invalide. |
402 Payment Required |
L'ingestion n'est pas dans votre plan actuel, ou votre quota mensuel d'ops est épuisé. |
403 Forbidden |
/v2/ingest n'est pas dans les endpoints autorisés de la clé API. |
404 Not Found |
job_id inconnu, ou appartenant à un autre projet. |
409 Conflict |
retry-webhook appelé sur un job qui n'a pas atteint completed. |
400 Bad Request |
retry-webhook appelé sans webhook_url configuré, ou son URL stockée se résout désormais en adresse privée/interne. |
422 Unprocessable Entity |
La source ne correspond pas au mode (urls sans urls, ou une url de départ en mode urls) ; un paramètre est hors plage ; ou une regex include_patterns/exclude_patterns ne compile pas. |
500 Internal Server Error |
Le job n'a pas pu être créé. Le message inclut le job_id à communiquer au support. |
Un échec de rendu au niveau d'une page ne fait pas échouer la requête ni le
job. Il incrémente pages_failed, place l'erreur de la page dans sa propre
ligne, et le job continue. Un job n'échoue (fails) que lorsque la découverte
est rejetée ou que toutes les pages échouent ou sont ignorées. La référence
complète des codes de statut se trouve dans le guide des codes
d'erreur.
Limites#
| Limit | Value |
|---|---|
URL par requête en mode urls |
1,000 |
Longueur de url / de chaque entrée urls |
2,048 characters |
max_pages (plafond de découverte) |
1–1,000 |
max_depth |
1–5 |
include_patterns / exclude_patterns |
50 each |
Longueur de wait_for |
1,024 characters |
wait_timeout_ms |
0–60,000 ms |
chunk.max_words |
32–4,000 (default 512) |
chunk.sentence_overlap |
0–10 (default 1) |
Longueur de webhook_url |
2,048 characters |
Plafond de pages par job (MAX_PAGES_PER_JOB) |
1,000 |
Fichiers par requête /v2/ingest/files |
1–200 |
| Taille de téléversement par fichier | Selon le plan (Founding : 5 MB) |
limit de la liste GET /v2/ingest |
1–100 (default 20) |
Expiration de l'output_url signée |
15 minutes |
| Tentatives de livraison du webhook | 4 (initial + 3 retries) |
| Tolérance de rejeu du webhook | 300 seconds |
| Ops mensuelles (partagées entre tous les endpoints, 1 par page) | 500 / 3 000 / 15 000 / 50 000 selon le palier ; voir les tarifs |
Questions fréquentes#
Comment crawler un site web pour le RAG avec une API ?#
Envoyez POST /v2/ingest avec mode: "crawl" et une url de départ. L'appel répond 202 avec un job_id ; le worker découvre les pages, rend chacune en Chrome headless, découpe le Markdown en tenant compte des titres, et assemble un fichier JSONL que vous téléchargez depuis l'output_url signée.
Comment ingérer des fichiers (PDF, documents Word) pour le RAG ?#
Envoyez POST /v2/ingest/files en multipart/form-data avec un ou plusieurs files. Chaque document est converti en Markdown, découpé en tenant compte des titres, et assemblé dans le même JSONL unique qu'un job de crawl, si bien qu'un seul pipeline couvre le web et les fichiers. Les fichiers PDF, DOCX, PPTX, XLSX, CSV, HTML, EPUB, OpenDocument et texte brut/Markdown sont pris en charge (jusqu'à 200 par requête) ; la liste complète est sur la page anything-to-markdown.
La sortie JSONL se charge-t-elle directement dans LangChain et LlamaIndex ?#
Oui. Chaque ligne porte une chaîne content de premier niveau avec un objet metadata frère, de sorte que le même fichier se charge via LangChain JSONLoader(content_key="content", json_lines=True), LlamaIndex SimpleDirectoryReader et tout import vers une base vectorielle orienté ligne, sans remise en forme.
Comment le chunker découpe-t-il les pages en chunks RAG ?#
Il découpe sur les titres #, ## et ### avec un plafond souple max_words (défaut 512, plage 32–4,000) et un sentence_overlap optionnel. Les blocs de code délimités et les tableaux Markdown ne sont jamais découpés, et chaque chunk porte son headings_path complet.
Comment être notifié quand un job d'ingestion se termine ?#
Définissez webhook_url sur le POST et EnConvert envoie un callback signé en HMAC (en-têtes X-Enconvert-Signature et X-Enconvert-Timestamp) avec jusqu'à trois réessais après la première tentative. Si votre endpoint était hors service, POST /v2/ingest/{job_id}/retry-webhook le re-signe et le re-livre.
Pourquoi output_url manque-t-il dans ma réponse d'ingestion ?#
output_url n'est présent que lorsque status vaut completed. La réponse au POST est un job queued avec le champ omis. Interrogez GET /v2/ingest/{job_id}, qui ne consomme aucune op et re-signe l'URL à chaque appel ; chaque URL signée expire après 15 minutes.