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.

  1. Mise en file. POST valide la requête, effectue une vérification rapide d'ops units=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épond 202. Rien n'est persisté si la barrière d'ops échoue : un 402 ne laisse aucune ligne derrière lui.
  2. Découverte. Pour les modes sitemap et crawl, 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 mode urls, 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 dans pages_found ; lorsque le site compte plus d'URL que max_pages n'en autorise, discovery_truncated vaut true et une entrée de warnings indique les deux nombres, afin que pages_discovered (le nombre mis en file) ne soit jamais confondu avec la taille du site.
  3. 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.
  4. 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.
  5. Assemblage. Une fois chaque page terminée, les objets par page sont concaténés dans le v2-ingest/{job_id}.jsonl final, les objets intermédiaires sont supprimés, le job passe à completed, et le webhook de fin signé est déclenché si un webhook_url a é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, cookies ni headers. 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. POST et les GET/DELETE par job utilisent response_model_exclude_none, de sorte que les champs encore null (comme output_url avant la fin) sont omis du JSON plutôt qu'envoyés comme null.

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.