---
seo_title: API de Scraping Web : URL en Markdown, Capture & PDF | EnConvert
meta_desc: POST /v2/perceive restitue une page JavaScript dans Chrome headless et renvoie Markdown, captures d'écran, PDF et données structurées en un seul appel API.
keywords: api web scraping markdown capture d'écran, convertir page javascript en markdown api, api pour lire une page web pour llm, api url vers markdown, api capture d'écran site web, api html vers markdown, extraire données structurées page web api, api web scraping par lot
---

# API de Scraping Web pour Markdown, Captures d'écran et Données structurées

`POST /v2/perceive` est l'API de web scraping d'EnConvert : elle effectue le rendu
d'une URL une seule fois dans un vrai navigateur headless (JavaScript exécuté, contenu
en chargement différé chargé) et renvoie chaque sortie que vous demandez à partir de ce
rendu unique : Markdown propre (par défaut le contenu principal seul, débarrassé de
l'habillage du site), HTML nettoyé ou brut, une capture d'écran, un PDF, l'inventaire des
liens et des images, et des données structurées (métadonnées de la page, JSON-LD, titres,
tableaux). Les sorties fichiers reviennent sous forme d'URL de téléchargement pré-signées
et de courte durée, le bloc structuré en ligne, et les lots de plus de 10 URL s'exécutent
de manière asynchrone via un `job_id` interrogeable. Une seule requête remplace toute une
pile d'appels séparés : url-to-markdown, url-to-screenshot, url-to-pdf, plus votre propre
scraping.

Voici le plus petit appel utile. Envoyez une URL, récupérez du Markdown propre et les
métadonnées structurées de la page en retour :

```bash
curl -X POST https://api.enconvert.com/v2/perceive \
  -H "X-API-Key: sk_your_private_key" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/pricing",
    "outputs": ["markdown", "structured"]
  }'
```

La réponse contient une URL de téléchargement pré-signée pour le fichier Markdown et le
bloc structuré en ligne :

```json
{
    "operation_id": "per_3f9a2c1b8e7d4a6f90b1c2d3e4f5a6b7",
    "status": "completed",
    "url": "https://example.com/pricing",
    "url_final": "https://example.com/pricing",
    "content_hash": "9f2b8c1a...d4e5",
    "render_quality": 0.93,
    "cache_hit": false,
    "outputs": {
        "markdown": {
            "url": "https://spaces.example.com/...signed...",
            "object_key": "env/files/4127/v2-perceive/per_3f9a..._markdown.md",
            "size_bytes": 8421,
            "content_type": "text/markdown; charset=utf-8",
            "expires_in": 900
        }
    },
    "structured": {
        "metadata": {
            "title": "Pricing",
            "description": "Simple, usage-based pricing."
        },
        "structured_data": [
            {"@type": "Product", "name": "Studio", "offers": {"price": "99"}}
        ]
    },
    "extraction_tier": "heuristic",
    "tokens": {"input": 0, "output": 0},
    "cost_cents": 0.0,
    "duration_ms": 6230,
    "warnings": []
}
```

---

## Points de terminaison

| Méthode | Chemin | Objectif |
|--------|------|---------|
| `POST` | `/v2/perceive` | Perçoit une seule URL et renvoie les sorties demandées. |
| `GET` | `/v2/perceive/{operation_id}` | Récupère à nouveau une opération passée avec des URL de téléchargement fraîchement signées. |
| `POST` | `/v2/perceive/batch` | Perçoit jusqu'à 1,000 URL partageant un même jeu d'options. |
| `GET` | `/v2/perceive/batch/{job_id}` | Interroge le statut et les résultats par URL d'un lot. |
| `DELETE` | `/v2/perceive/batch/{job_id}` | Annule un lot en cours d'exécution. |

**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 dans les exemples ci-dessous.

```http
X-API-Key: sk_your_private_key
```

Les clés publiques avec un token JWT bearer fonctionnent également, en suivant le même
flux que tout autre point de terminaison : générez un token avec votre clé `pk_`, puis
envoyez-le sous la forme `Authorization: Bearer <token>`. Le flux complet, y compris le
verrouillage de domaine et le rafraîchissement de token, se trouve dans [le guide
d'authentification](/fr/docs/authentication.md).

Chaque clé API porte une liste blanche de points de terminaison autorisés. Si
`/v2/perceive` ne figure pas dans la liste de la clé, la requête est rejetée avec `403`.

---

## Comment fonctionne perceive

Une requête déclenche un rendu de navigateur via un singleton Chrome headless partagé,
puis matérialise chaque sortie à partir de ce rendu. Vous ne payez jamais deux fois pour
la même page au sein d'un seul appel.

1. **Rendu.** La page est récupérée via un repli multi-moteurs automatique : d'abord
   une empreinte TLS rapide de vrai navigateur, avec escalade vers Chrome headless
   lorsque la page est bloquée ou nécessite JavaScript, puis une fois de plus vers un
   rendu renforcé en mode furtif lorsqu'une page semble toujours bloquée par une
   protection anti-bot, de sorte que davantage de pages du monde réel reviennent avec
   un contenu exploitable. Dans le navigateur, les bannières de cookies sont fermées, la
   page est défilée pour déclencher le contenu en chargement différé, les en-têtes
   collants sont gérés, et les images ont le temps de se charger. C'est le même pipeline
   de capture qui alimente [le point de terminaison
   url-to-pdf](/fr/docs/endpoints/convert/web-pages/url-to-pdf.md).
2. **Matérialisation.** À partir du DOM rendu, perceive construit tout ce que vous avez
   listé dans `outputs` : Markdown, HTML nettoyé/brut, liens, images, une capture
   d'écran, un PDF. Le DOM est d'abord normalisé pour que le Markdown reflète ce
   que voit un lecteur : les clôtures de code conservent leur langage, les liens
   de carte leur structure, et le mobilier d'interface est retiré sous
   `only_main_content`. Voir [Qualité du Markdown](#qualite-du-markdown).
3. **Extraction.** Si vous avez demandé la sortie `structured`, perceive effectue une
   passe heuristique pour les métadonnées de la page, le JSON-LD, les titres et les
   tableaux. Si vous envoyez également un `schema` et que votre plan inclut le palier
   LLM, une extraction assistée par LLM complète le schéma lorsque la passe heuristique
   ne suffit pas.
4. **Score.** Un score de qualité de rendu (0.0–1.0) distingue un vrai rendu d'un
   rendu échoué. Un score inférieur à 0.40 signifie un rendu échoué : une page
   anti-bot, un mur de connexion, une page d'erreur HTTP, un soft 404 ou une coquille
   vide. Consultez `deductions` pour connaître la raison et `status_code` pour le
   statut du serveur d'origine.

Les sorties binaires et textuelles (Markdown, HTML, captures d'écran, PDF, le JSON des
liens et des images) sont téléversées vers le stockage et renvoyées sous forme d'**URL
pré-signées** qui expirent après 15 minutes. Le bloc `structured` est renvoyé en ligne
dans le JSON. Récupérez à nouveau n'importe quelle opération avec
`GET /v2/perceive/{operation_id}` pour obtenir un nouveau jeu d'URL signées.

---

## Paramètres de la requête

La validation est stricte : une clé de requête inconnue du schéma est rejetée avec
`422` en nommant le champ concerné. Les clés inconnues ne sont jamais ignorées
silencieusement. Chaque corps `422` contient en outre un tableau `errors` de premier
niveau avec des messages lisibles par un humain, à côté de la liste `detail` lisible
par machine.

### Principaux

| Paramètre | Type | Valeur par défaut | Description |
|-----------|------|---------|-------------|
| `url` | `string` | -- | La page à percevoir. Doit commencer par `http://` ou `https://`. 2,048 caractères max. Obligatoire. |
| `outputs` | `string[]` | `["markdown", "structured"]` | Les sorties à produire. Voir [Sorties](#outputs). |
| `extract` | `string[]` | `[]` | Les champs structurés à extraire lorsque `structured` figure dans `outputs`. Voir [Extraction structurée](#extraction-structuree). |
| `schema` | `object` | `null` | Un schéma JSON décrivant les champs que vous souhaitez extraire. Déclenche le palier d'extraction LLM sur les plans qui l'incluent. |
| `only_main_content` | `boolean` | `true` | Retire l'habillage du site (navigation, en-tête, pied de page, barres latérales, bannières de cookies, nœuds masqués) ainsi que le mobilier d'interface (boutons, barres d'onglets, widgets « Cette page vous a-t-elle été utile ? », libellés réservés aux lecteurs d'écran, fils d'Ariane) de la sortie `markdown` et de l'extraction `main_content`, derrière un garde-fou de fidélité : si le nettoyage supprimait trop de contenu réel, la page complète est renvoyée à la place et un avertissement est ajouté. Les URL d'images sont rendues sous forme de leur texte alt (la liste complète des images reste disponible via `outputs: ["images"]`). Définissez `false` pour la page complète, sans rien retirer. Voir [Qualité du Markdown](#qualite-du-markdown). |
| `truncate_data_arrays` | `boolean` | non défini | Réduit les longues suites de littéraux numériques (vecteurs d'embeddings bruts, dumps de tenseurs affichés dans les cellules de sortie de notebooks) à un échantillon initial suivi d'un décompte, par ex. `... [truncated 1520 of 1536 values]`. Non défini, l'option suit `only_main_content` : active quand la page est nettoyée, inactive quand vous avez demandé la page telle quelle. Définissez `true` ou `false` pour la contrôler explicitement. |
| `allow_degraded` | `boolean` | `false` | Renvoie le rendu même lorsqu'il s'agit d'un défi anti-bot ou d'une page de blocage sans contenu de page. Par défaut, un tel rendu échoue avec `502` au lieu de livrer le texte de l'interstitiel comme s'il s'agissait de la page. |
| `direct_download` | `boolean` | `false` | Renvoie les octets de l'artefact directement dans le corps de la réponse HTTP au lieu d'une enveloppe JSON. Exige exactement une sortie produisant un artefact. Requêtes à URL unique uniquement, car le point de terminaison de lot le rejette avec `422`. Voir [Téléchargement direct](#telechargement-direct). |
| `cache_mode` | `string` | `"enabled"` | `enabled`, `bypass` ou `refresh`. Voir [Mise en cache](#mise-en-cache). |

### Sorties {: #outputs }

`outputs` accepte n'importe quelle combinaison des noms suivants :

| Sortie | Renvoyée sous forme de | Ce que vous obtenez |
|--------|-------------|--------------|
| `markdown` | URL signée | Markdown propre de la page. Avec `only_main_content` (par défaut `true`), l'habillage du site tel que la navigation, l'en-tête, le pied de page, les barres latérales, les bannières de cookies et les nœuds masqués est retiré derrière un garde-fou de fidélité, et les URL d'images sont rendues sous forme de leur texte alt. Les blocs de code conservent leur langage sur la clôture (` ```python `) dans les deux modes. Définissez `only_main_content: false` pour la page complète. Voir [Qualité du Markdown](#qualite-du-markdown). |
| `html_cleaned` | URL signée | Le HTML rendu, débarrassé des scripts, des styles et du code répétitif. |
| `html_raw` | URL signée | Le HTML rendu complet, tel que produit par le navigateur. |
| `screenshot` | URL signée | Un PNG de la fenêtre d'affichage à la taille demandée (ou par défaut). |
| `screenshot_full_page` | URL signée | Un PNG pleine page capturant toute la hauteur de défilement. |
| `pdf` | URL signée | Un PDF de la page. Accepte l'ensemble des options `pdf_options` (voir ci-dessous). |
| `links` | URL signée | Un tableau JSON de tous les liens trouvés, avec URL absolues et texte d'ancrage. |
| `images` | URL signée | Un tableau JSON de toutes les images, avec `src` absolu et texte `alt`. |
| `structured` | JSON en ligne | Données structurées extraites de la page (le champ de réponse `structured`). |

### Qualité du Markdown

Avant la conversion de la page, le DOM rendu est normalisé afin que le
Markdown reflète ce que voit un lecteur plutôt que la façon dont la
page a été construite. Cela s'exécute à chaque rendu, si bien que le
résultat ne dépend pas de la stratégie d'extraction qui l'emporte pour
une page donnée.

Toujours appliqué, dans les deux modes de `only_main_content` :

- **Les clôtures de code conservent leur langage.** Le langage est lu
  depuis la convention utilisée par le site (`class="language-python"`,
  `data-lang`, un attribut `language` nu ou un wrapper de coloration
  syntaxique) puis normalisé, de sorte que ` ```python ` arrive au lieu
  d'une clôture nue.
- **Les liens de carte restent lisibles.** Un lien qui enveloppe un
  titre et une description devient un titre lié suivi de sa
  description, au lieu d'un seul lien agglutiné comme
  `[DatabaseSupabase provides a full Postgres database...]`. L'URL de
  destination est préservée.
- **Les titres tiennent sur une seule ligne.** Un titre dont le texte
  se trouve dans un élément imbriqué n'émet plus un `##` nu avec le
  texte échoué en dessous.
- **Les éléments adjacents ne se concatènent plus.** Les mises en page
  qui espacent leurs éléments en CSS plutôt qu'avec des blancs
  produisaient `YesNo` et `EvaluationDeploymentProduction` ; ils se
  lisent désormais comme des mots séparés.
- **Les caractères invisibles sont supprimés** : espaces de largeur
  nulle utilisés comme libellés d'ancre, traits d'union conditionnels
  et glyphes de la zone à usage privé des polices d'icônes, qui
  arrivent sous forme de jetons non imprimables.
- **Les éléments vides sont écartés** : éléments `<i>` ne contenant
  qu'une icône et rendus en `__` égarés, et liens dont le libellé est
  vide.

De plus, avec `only_main_content: true` :

- **Les contrôles d'interface sont supprimés** : boutons, barres
  d'onglets, indications de raccourcis clavier, actions « Copy page » /
  « On this page » et widgets de notation « Cette page vous a-t-elle
  été utile ? Oui/Non ». Un contrôle porteur de contenu réel (une
  question de FAQ, le corps d'une carte cliquable) est conservé.
- **Le texte réservé aux lecteurs d'écran est supprimé** : liens
  d'évitement et libellés « Section titled ... » que de nombreux thèmes
  de documentation attachent à chaque titre.
- **Le non-contenu déclaré par le site est respecté** : blocs marqués
  `data-nosnippet`, `data-pagefind-ignore` ou `data-noindex`, sauf
  s'ils contiennent des titres ou du code.
- **Les blocs dupliqués sont fusionnés** : les designs responsives qui
  livrent une copie desktop et une copie mobile de la même barre, et
  les carrousels qui pré-rendent chaque image, n'apparaissent qu'une
  fois.
- **Les fils d'Ariane et les surtitres placés au-dessus du titre de la
  page sont écartés.**

Le contenu différé est délibérément *conservé* : un panneau d'onglet
inactif à l'intérieur de la région de contenu abrite un véritable
exemple de code (l'exemple Python dans un onglet, celui en JavaScript
dans un autre), de sorte que les deux atteignent le Markdown, et pas
seulement l'onglet qui se trouvait sélectionné au moment du rendu.

### Rendu et attente

| Paramètre | Type | Valeur par défaut | Description |
|-----------|------|---------|-------------|
| `viewport` | `object` | `1920 x 1080` | `{"width": <int>, "height": <int>}`. Largeur 320–3840, hauteur 240–2160. |
| `mobile` | `boolean` | `false` | Effectue le rendu avec une fenêtre d'affichage mobile (390 x 844), sauf si `viewport` est défini explicitement. |
| `wait_for` | `string` | `null` | Attend après la navigation un sélecteur CSS (`".price"` ou `"css:.price"`) ou une expression JS (`"js:window.dataReady === true"`). |
| `wait_timeout_ms` | `integer` | `30000` | Durée maximale d'attente pour `wait_for`, en millisecondes. 0–60,000. Un dépassement de délai devient un avertissement ; la page est capturée telle quelle. |
| `js_code` | `string` | `null` | JavaScript à exécuter sur la page après la navigation. 20,000 caractères max. Une erreur devient un avertissement, pas un échec. |
| `block_resources` | `string[]` | `[]` | Types de ressources à interrompre avant leur chargement. Parmi `image`, `media`, `font`, `stylesheet`, `script`, `xhr`, `fetch`, `websocket`, `manifest`, `other`. Utile pour des rendus texte plus rapides. |
| `respect_robots` | `boolean` | `false` | Lorsque défini sur `true`, une URL interdite par le `robots.txt` du site est rejetée avec `403`. |
| `pdf_options` | `object` | `null` | Format de page, marges, en-têtes, pieds de page, échelle et orientation pour la sortie `pdf`. Même objet que pour [url-to-pdf](/fr/docs/endpoints/convert/web-pages/url-to-pdf.md). Sans `pdf_options`, perceive produit une seule page continue, identique octet pour octet à url-to-pdf en V1. |

### Requêtes authentifiées et personnalisées

| Paramètre | Type | Valeur par défaut | Description |
|-----------|------|---------|-------------|
| `auth` | `object` | `null` | Authentification HTTP Basic pour la page cible : `{"username": "...", "password": "..."}`. |
| `cookies` | `array` | `null` | Cookies à injecter avant la navigation. 50 max. Chacun nécessite `name`, `value`, et soit `domain`, soit `url`. |
| `headers` | `object` | `null` | En-têtes de requête personnalisés. 20 max. Noms bloqués : `host`, `content-length`, `transfer-encoding`, `connection`, `upgrade`, `te`, `trailer`. |

<div class="alert alert-warning">
<strong>Réservé, pas encore actif.</strong> <code>proxy_url</code> (Production+),
<code>geolocation</code> et <code>action_chain</code> sont acceptés par le
schéma de requête, mais renvoient <code>422</code> pour le moment. Ils arriveront
dans une prochaine version ; les envoyer dès maintenant vous indique précisément
quel réglage n'est pas prêt au lieu de l'ignorer silencieusement.
</div>

---

## Extraction structurée

Lorsque `structured` figure dans `outputs`, la liste `extract` contrôle les champs que
perceive extrait. Si vous ne demandez rien, elle utilise par défaut `metadata` et
`structured_data`.

| Valeur `extract` | Champ dans `structured` | Statut |
|-----------------|-----------------------|--------|
| `metadata` | `metadata` | Actif |
| `structured_data` | `structured_data` (JSON-LD) | Actif |
| `headings` | `headings` | Actif |
| `tables` | `tables` | Actif |
| `main_content` | `main_content` (texte, plafonné à 50,000 caractères) | Actif |
| `all` | s'étend à tous les champs actifs ci-dessus | Actif |
| `prices` | -- | Pas encore actif : renvoie un avertissement, omis |
| `contacts` | -- | Pas encore actif : renvoie un avertissement, omis |
| `technologies` | -- | Pas encore actif : renvoie un avertissement, omis |

Pour être clair : `prices`, `contacts` et `technologies` sont des noms réservés. En
demander un aujourd'hui ne provoque pas d'erreur. Le nom atterrit dans le tableau
`warnings` et est retiré de `structured`.

### Extraction pilotée par schéma

Envoyez un `schema` pour extraire des champs spécifiques dans `structured.extracted` :

```json
{
    "url": "https://example.com/product/widget",
    "outputs": ["markdown", "structured"],
    "schema": {
        "type": "object",
        "properties": {
            "name": {"type": "string"},
            "price": {"type": "number"},
            "in_stock": {"type": "boolean"}
        }
    }
}
```

Le palier d'extraction LLM complète le schéma, et il se
déclenche uniquement lorsque **toutes** ces conditions sont réunies : vous avez envoyé
un `schema`, votre plan inclut le palier LLM (Indie et au-dessus), la page n'a pas été
notée comme bloquée, et la passe heuristique a laissé des champs du schéma vides. Quand
il s'exécute, `extraction_tier` vaut `"llm"`, et `tokens` ainsi que `cost_cents`
indiquent ce que cette extraction a coûté ; sinon, `extraction_tier` vaut `"heuristic"`
et les deux valent zéro.

> **Note.** L'extraction par schéma est strictement plafonnée pour protéger votre
> facture : une seule extraction est plafonnée par requête, et la dépense du projet
> puise dans votre solde mensuel de crédits IA ($5 / $15 / $40 par mois sur
> Indie / Studio / Production ; les crédits inutilisés sont reportés).
> L'extraction LLM consomme des crédits, pas des ops. Si un plafond est atteint
> ou que le solde est épuisé, perceive renvoie le
> résultat heuristique avec une note dans `warnings` plutôt que de dépasser le budget.
> Sur un plan sans le palier LLM, vous n'obtenez que des données `structured`
> heuristiques.

---

## Réponse {: #response }

`POST /v2/perceive` et `GET /v2/perceive/{operation_id}` renvoient tous deux le même
objet.

| Champ | Type | Description |
|-------|------|-------------|
| `operation_id` | `string` | ID opaque (`per_...`). Utilisez-le avec le point de terminaison GET et citez-le au support. |
| `status` | `string` | `queued`, `processing`, `completed` ou `failed`. |
| `url` | `string` | L'URL que vous avez envoyée. |
| `url_final` | `string` | L'URL après redirections. |
| `content_hash` | `string` | SHA-256 de la page rendue. Pilote le cache d'1 heure. |
| `render_quality` | `number` | 0.0–1.0. Un score inférieur à 0.40 signifie un rendu échoué : une page anti-bot, un mur de connexion, une page d'erreur HTTP, un soft 404 ou une coquille vide. Consultez `deductions` pour connaître la raison et `status_code` pour le statut du serveur d'origine. |
| `status_code` | `integer` | Statut HTTP de la réponse finale du document principal (p. ex. `200`, `404`). `null` lorsqu'il est inconnu. |
| `deductions` | `object` | Déductions nommées de qualité de rendu qui se sont appliquées, p. ex. `{"http_error": 0.7}`, `{"soft_404": 0.65}`, `{"login_wall": 0.65}`. Vide pour un rendu propre. |
| `options_echo` | `object` | Écho des options de requête que le serveur a appliquées. Les secrets sont réduits à des booléens (`auth_provided`, `cookies_provided`, `headers_provided`, `js_code_provided`, `schema_provided`, `pdf_options_provided`). Les options simples (`outputs`, `only_main_content`, `truncate_data_arrays`, `allow_degraded`, `extract`, `cache_mode`, `mobile`, `respect_robots`, `direct_download`, `wait_for`, `wait_timeout_ms`, `viewport`, `block_resources`) sont renvoyées telles qu'appliquées. `truncate_data_arrays` est renvoyée sous forme de booléen **résolu**, de sorte que même laissée non définie vous savez dans quel sens elle a été tranchée. |
| `cache_hit` | `boolean` | `true` lorsque le résultat provient du cache plutôt que d'un rendu frais. |
| `outputs` | `object` | Correspondance entre le nom de la sortie et `{url, object_key, size_bytes, content_type, expires_in}`. Les URL signées expirent au bout de 900 secondes. |
| `structured` | `object` | Données structurées en ligne, présentes lorsque `structured` a été demandé. |
| `extraction_tier` | `string` | `heuristic`, `css` ou `llm`. |
| `tokens` | `object` | Tokens LLM `{input, output}` utilisés. Zéro sauf si le palier LLM s'est exécuté. |
| `cost_cents` | `number` | Coût LLM en centimes pour cette opération. Zéro sauf si le palier LLM s'est exécuté. |
| `duration_ms` | `integer` | Temps de rendu de bout en bout. |
| `error` | `string` | Défini uniquement lorsque `status` vaut `failed`. |
| `warnings` | `string[]` | Notes non fatales : un délai `wait_for` dépassé, une extraction ignorée, un signalement de page bloquée, un repli de `only_main_content` vers la page complète, une note indiquant que de longs tableaux de données numériques ont été tronqués. |

---

## Récupérer une opération {: #retrieve-an-operation }

Les URL signées expirent après 15 minutes. Pour télécharger une sortie plus tard,
récupérez à nouveau l'opération : perceive re-signe chaque URL à partir des clés
d'objet stockées. Aucun nouveau rendu n'a lieu, ce qui ne consomme donc aucune op.

```bash
curl https://api.enconvert.com/v2/perceive/per_3f9a2c1b8e7d4a6f90b1c2d3e4f5a6b7 \
  -H "X-API-Key: sk_your_private_key"
```

Un ID d'opération inconnu, ou appartenant à un autre projet, renvoie `404`.
L'existence n'est jamais divulguée entre projets.

---

## Téléchargement direct

Par défaut, chaque sortie fichier revient sous forme d'URL pré-signée que vous
récupérez dans une seconde requête. Définissez `direct_download: true` sur le POST
pour sauter l'enveloppe : le corps de la réponse HTTP **est** les octets de
l'artefact, sans JSON, sans URL signée et sans second téléchargement. La requête
doit produire exactement une sortie générant un artefact (`outputs: ["markdown"]`,
`outputs: ["pdf"]`, …), sinon elle est rejetée avec `400`. Les métadonnées qui
auraient figuré dans le JSON voyagent à la place dans les en-têtes de réponse :
`Content-Disposition`, `X-Operation-Id`, `X-Object-Key`, `X-Cache-Hit`,
`X-Render-Quality`, `X-Source-Status-Code`, `X-Content-Hash` et `X-Warnings-Count`.

```bash
curl -X POST https://api.enconvert.com/v2/perceive \
  -H "X-API-Key: sk_your_private_key" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/blog/post",
    "outputs": ["markdown"],
    "direct_download": true
  }' \
  -o post.md
```

Les points de terminaison GET diffusent les artefacts stockés de la même manière :

- `GET /v2/perceive/{operation_id}?direct_download=true&output=markdown` diffuse un
  artefact d'une opération passée. `output` est obligatoire lorsque l'opération a
  produit plus d'un artefact. Un artefact au-delà de la fenêtre de rétention de votre
  plan répond `410`.
- `GET /v2/perceive/batch/{job_id}?direct_download=true` diffuse le ZIP du lot pour
  les lots en `output_mode: "zip"` dont l'archive est prête, et répond `400` sinon.

`direct_download` est réservé aux URL uniques : `POST /v2/perceive/batch` le rejette
avec `422`. Définissez `output_mode` sur `"zip"` et téléchargez l'archive à la place.
Voir [Perception par lot](#batch-perception).

---

## Perception par lot {: #batch-perception }

`POST /v2/perceive/batch` perçoit une liste d'URL partageant un même bloc `options`.
Chaque URL est rendue via le même pipeline qu'un appel unique et produit sa propre
ligne d'opération.

```bash
curl -X POST https://api.enconvert.com/v2/perceive/batch \
  -H "X-API-Key: sk_your_private_key" \
  -H "Content-Type: application/json" \
  -d '{
    "urls": [
      "https://example.com/a",
      "https://example.com/b",
      "https://example.com/c"
    ],
    "options": {"outputs": ["markdown"]},
    "output_mode": "manifest"
  }'
```

Les lots de 10 URL ou moins s'exécutent en ligne et répondent `200` avec chaque
résultat renseigné. Les lots plus importants répondent `202` avec un `job_id` ; les URL
sont traitées une à une et vous interrogez les résultats :

```bash
curl https://api.enconvert.com/v2/perceive/batch/{job_id} \
  -H "X-API-Key: sk_your_private_key"
```

La réponse du lot rapporte la progression agrégée et porte un résultat perceive
complet par URL une fois le rendu effectué :

```json
{
    "job_id": "bat_8c1a...",
    "status": "partial",
    "output_mode": "manifest",
    "total": 3,
    "completed": 2,
    "failed": 1,
    "pending": 0,
    "items": [
        {"operation_id": "per_...", "status": "completed", "url": "https://example.com/a"},
        {"operation_id": "per_...", "status": "completed", "url": "https://example.com/b"},
        {"operation_id": "per_...", "status": "failed", "url": "https://example.com/c"}
    ]
}
```

`status` vaut `queued`, `processing`, `completed`, `failed`, `partial` (certaines URL
ont réussi, d'autres ont échoué) ou `canceled`. Définissez `output_mode` sur `zip` pour
regrouper chaque artefact dans un seul ZIP, renvoyé sous le champ `zip` une fois le lot
terminé.

### Durabilité et reprise automatique

Les lots résistent aux redémarrages. Si le service redémarre alors qu'un lot est en
cours, le lot **reprend automatiquement** et n'effectue un nouveau rendu que pour les
URL qui n'étaient pas terminées, donc les URL déjà terminées conservent leurs artefacts.
Vous n'avez jamais besoin de soumettre à nouveau un lot à cause d'un redémarrage.

### Annuler un lot

`DELETE /v2/perceive/batch/{job_id}` annule un lot en cours d'exécution. Le worker
s'arrête entre deux URL, de sorte que les URL déjà rendues conservent leurs résultats
et que les autres ne sont pas démarrées. L'appel est idempotent, donc annuler un lot
déjà terminé renvoie simplement son état actuel, et le `status` du lot devient
`canceled`.

```bash
curl -X DELETE https://api.enconvert.com/v2/perceive/batch/{job_id} \
  -H "X-API-Key: sk_your_private_key"
```

---

## Mise en cache

`cache_mode` contrôle la manière dont perceive traite son cache de résultats d'1
heure, indexé par votre projet, l'URL, et les options de requête affectant le rendu.

| `cache_mode` | Comportement |
|--------------|-----------|
| `enabled` (par défaut) | Renvoie un résultat mis en cache lorsqu'une requête identique a été rendue au cours de la dernière heure. `cache_hit` vaut `true`, `cost_cents` vaut `0`. |
| `bypass` | Ignore le cache et effectue un rendu frais. |
| `refresh` | Effectue un rendu frais et remplace l'entrée en cache. |

À noter : un cache hit facture tout de même une op sur votre quota mensuel
d'ops. Le quota mesure les opérations plutôt que les rendus de navigateur, donc
le cache vous fait gagner du temps de rendu, pas des ops.

---

## Exemples de code

### curl : Markdown seul

```bash
curl -X POST https://api.enconvert.com/v2/perceive \
  -H "X-API-Key: sk_your_private_key" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/blog/post",
    "outputs": ["markdown"]
  }'
```

### curl : Markdown et données structurées

```bash
curl -X POST https://api.enconvert.com/v2/perceive \
  -H "X-API-Key: sk_your_private_key" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/pricing",
    "outputs": ["markdown", "structured"],
    "extract": ["metadata", "structured_data", "tables"]
  }'
```

### curl : Toutes les sorties et PDF

```bash
curl -X POST https://api.enconvert.com/v2/perceive \
  -H "X-API-Key: sk_your_private_key" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/report",
    "outputs": ["markdown", "html_cleaned", "screenshot_full_page", "pdf"],
    "pdf_options": {"format": "A4", "print_background": true}
  }'
```

### Python

```python
import requests

response = requests.post(
    "https://api.enconvert.com/v2/perceive",
    headers={"X-API-Key": "sk_your_private_key"},
    json={
        "url": "https://example.com/pricing",
        "outputs": ["markdown", "structured"],
        "extract": ["metadata", "tables"],
    },
)
response.raise_for_status()
data = response.json()

# Download the Markdown artifact from its signed URL
markdown_url = data["outputs"]["markdown"]["url"]
markdown_text = requests.get(markdown_url).text

print(data["structured"])
print(markdown_text)
```

### Node.js

```javascript
const res = await fetch("https://api.enconvert.com/v2/perceive", {
    method: "POST",
    headers: {
        "Content-Type": "application/json",
        "X-API-Key": "sk_your_private_key"
    },
    body: JSON.stringify({
        url: "https://example.com/pricing",
        outputs: ["markdown", "structured"],
        extract: ["metadata", "tables"]
    })
});

const data = await res.json();

// Download the Markdown artifact from its signed URL
const markdownText = await fetch(data.outputs.markdown.url).then(r => r.text());

console.log(data.structured);
console.log(markdownText);
```

Si vous appelez EnConvert depuis Claude, Cursor, ou un autre client MCP, la même
fonctionnalité est exposée sous forme d'outil `perceive_url`. Voir [la page du serveur
MCP](/fr/mcp.md).

---

## Réponses d'erreur

| Statut | Condition |
|--------|-----------|
| `400 Bad Request` | L'URL n'est pas en `http(s)`, contient des identifiants intégrés, ou se résout vers une adresse privée, loopback ou link-local (protection SSRF). |
| `400 Bad Request` | `auth` invalide (`username`/`password` manquant), `cookies` invalide (pas un tableau, plus de 50 entrées, champs manquants), ou `headers` invalide (pas un objet, plus de 20 entrées, nom bloqué). |
| `401 Unauthorized` | Clé API / token JWT manquant ou invalide. |
| `402 Payment Required` | Perceive n'est pas inclus dans votre plan actuel, ou votre quota mensuel d'ops est épuisé. |
| `403 Forbidden` | `/v2/perceive` ne figure pas dans les points de terminaison autorisés de la clé API. |
| `403 Forbidden` | Le traitement par lot n'est pas disponible sur votre plan, ou la taille du lot dépasse la limite de votre plan. |
| `403 Forbidden` | `respect_robots=true` et le `robots.txt` du site interdit l'URL. |
| `404 Not Found` | `operation_id` ou `job_id` inconnu, ou appartenant à un autre projet. |
| `422 Unprocessable Entity` | Échec de la validation de la requête (valeur d'énumération invalide dans `outputs`/`extract`, `wait_timeout_ms` hors limites, viewport hors limites, une clé de requête inconnue). |
| `422 Unprocessable Entity` | `proxy_url`, `geolocation` ou `action_chain` a été envoyé. Ces trois options sont réservées pour une version ultérieure. |
| `500 Internal Server Error` | Le rendu a échoué. Le message inclut l'`operation_id` à citer au support. |
| `502 Bad Gateway` | Tous les moteurs ont été bloqués et l'origine a renvoyé un défi anti-bot sans contenu de page derrière lui. Réessayez plus tard, ou envoyez `allow_degraded: true` pour recevoir la page de défi telle quelle. |

Les clés de requête inconnues sont rejetées avec un `422` nommant le champ, sur
`/v2/perceive`, `/v2/perceive/batch`, `/v2/discover` et `/v2/lookup` sans distinction.
Elles ne sont jamais ignorées silencieusement. Chaque corps `422` contient un tableau
`errors` de premier niveau avec des messages lisibles par un humain, à côté de la liste
`detail` brute.

La référence complète des codes de statut se trouve dans [le guide des codes
d'erreur](/fr/docs/reference/errors.md).

---

## Limites

| Limite | Valeur |
|-------|-------|
| Longueur de l'URL | 2,048 caractères |
| `wait_timeout_ms` | 0–60,000 ms |
| Longueur de `js_code` | 20,000 caractères |
| Largeur du viewport | 320–3,840 px |
| Hauteur du viewport | 240–2,160 px |
| Cookies par requête | 50 |
| En-têtes personnalisés par requête | 20 |
| Extraction `main_content` | 50,000 caractères |
| URL par lot et par requête | 1,000 (plafond du schéma) |
| Seuil de traitement en ligne | 10 URL (les lots plus importants s'exécutent de manière asynchrone) |
| TTL du cache de résultats | 1 heure |
| Expiration des URL signées | 15 minutes |
| Ops mensuelles (partagées entre tous les endpoints) | 500 / 3 000 / 15 000 / 50 000 selon le palier ; voir [la page tarifaire](/fr/pricing.md) |

---

## Questions fréquentes

### Comment convertir une page web en Markdown avec une API REST ?

Envoyez `POST /v2/perceive` avec `{"url": "...", "outputs": ["markdown"]}`. La page est rendue dans Chrome headless et la réponse contient une URL de téléchargement pré-signée pour le fichier Markdown. Par défaut, `only_main_content` retire l'habillage du site pour que vous receviez l'article, pas la navigation ; définissez `"only_main_content": false` pour la page complète, ou ajoutez `"direct_download": true` pour recevoir les octets du Markdown directement dans le corps de la réponse.

### Puis-je obtenir une capture d'écran et du Markdown à partir du même rendu ?

Oui. `outputs` accepte n'importe quelle combinaison, donc `["markdown", "screenshot"]` (ou `screenshot_full_page` pour toute la hauteur de défilement) produit les deux à partir d'un seul rendu de navigateur. Vous ne payez jamais deux fois pour la même page en un seul appel.

### /v2/perceive effectue-t-il le rendu de pages JavaScript ?

Oui. Chaque requête effectue un vrai rendu headless-Chrome : les bannières de cookies sont fermées, la page est défilée pour déclencher le contenu en chargement différé, et vous pouvez contrôler la page avant la capture avec `wait_for` (un sélecteur CSS ou une expression JS), `js_code`, et `block_resources`.

### Pourquoi mon URL de téléchargement signée a-t-elle cessé de fonctionner ?

Les URL signées expirent après 15 minutes (`expires_in: 900`). Récupérez à nouveau l'opération avec `GET /v2/perceive/{operation_id}` pour obtenir des URL fraîchement signées. Aucun nouveau rendu n'a lieu et aucune op n'est consommée.

### Un résultat mis en cache compte-t-il toujours dans mon quota ?

Oui. Un cache hit facture une op, car le quota mensuel mesure les opérations plutôt que les rendus de navigateur. Définissez `cache_mode` sur `bypass` pour ignorer le cache d'1 heure, ou sur `refresh` pour effectuer un rendu frais et remplacer l'entrée en cache.
