---
seo_title: Tâches API sync ou async et interrogation | EnConvert
meta_desc: Comment EnConvert choisit entre un résultat en ligne et une tâche en file, plus le cycle de vie des tâches, le contrat d'interrogation et les statuts terminaux.
keywords: api sync ou async, paramètre async_mode, interroger le statut d'une tâche api, endpoint de statut d'un lot, récupération après timeout avec job_id, api de conversion 202 accepted, statut terminal d'une tâche, interroger le statut d'une conversion
---

# Tâches synchrones et asynchrones

La plupart des appels EnConvert vous remettent le résultat fini dans le corps de la réponse. Certains vous remettent un identifiant à la place et effectuent le travail en arrière-plan. Ce que vous obtenez dépend de l'endpoint que vous appelez et, sur quelques endpoints, de ce que vous mettez dans la requête.

---

## Ce qui détermine le mode

| Endpoint | Mode |
|----------|------|
| Toutes les conversions par téléversement de fichier (documents, formats de données, images) | Toujours sync. `async_mode` n'est jamais lu sur ces endpoints. |
| `url-to-pdf`, `url-to-screenshot`, `url-to-markdown` | Sync par défaut. Async lorsque vous définissez `async_mode: true`, ou lorsque `url` est un tableau. |
| `website-to-pdf`, `website-to-screenshot` | Toujours async. Les deux répondent `202` avec un `batch_id` et `output_format: "zip"`. |
| `POST /v2/perceive` | Toujours sync. Une URL unique est rendue pendant la requête et il n'existe aucun commutateur async. |
| `POST /v2/perceive/batch` | Sync jusqu'à 10 URLs incluses, async au-delà. |
| `POST /v2/ingest`, `POST /v2/ingest/files` | Toujours async. Les deux répondent `202` avec un `job_id`. |

Les clés publiques et dashboard sont limitées aux requêtes sync à URL unique sur les endpoints URL V1, quoi que dise le corps de la requête.

| | Mode sync | Mode async |
|---|---|---|
| **Déclencheur** | Par défaut pour une seule URL / un seul téléversement | URLs multiples, ou `async_mode: true` |
| **Réponse** | `200 OK` avec le résultat | `202 Accepted` avec `batch_id` |
| **Livraison du résultat** | Octets du fichier ou URL présignée dans la réponse | Interrogation, webhook ou e-mail |
| **Types de clés** | Clés privées et publiques | Clés privées uniquement |
| **Exigence de plan** | Tous les plans | Nécessite l'accès async (Indie et supérieur) |

<div class="alert alert-info">
<strong>Restriction par plan :</strong> Le mode async n'est pas disponible sur le plan free. Toute tentative de définir <code>async_mode: true</code> ou de soumettre plusieurs URLs sur un plan free renvoie <code>403 Forbidden</code>.
</div>

L'async et le traitement par lot appartiennent tous deux aux plans payants. Le plan Founding n'a ni l'un ni l'autre, ce qui explique qu'un premier test sur une clé free qui soumet trois URLs revienne en `403` plutôt qu'en `202`. Les chiffres par plan, y compris le plafond de taille de lot, se trouvent dans [Limites de débit et quotas](/fr/docs/reference/rate-limits.md).

---

## Demander l'async explicitement

Voici les champs de requête qui déterminent le mode ou qui vous donnent une prise sur la tâche produite. Tout le reste de la requête (options de rendu, options PDF, nommage de la sortie) est identique dans les deux modes.

| Paramètre | Type | Valeur par défaut | Description | Restriction par plan |
|-----------|------|---------|-------------|-------------|
| `async_mode` | `boolean` | `false` | Met le travail en file d'attente et répond `202` au lieu de maintenir la connexion ouverte. Lu uniquement par `url-to-pdf`, `url-to-screenshot` et `url-to-markdown`. | Nécessite l'accès async |
| `url` (tableau) | `string[]` | -- | Plus d'une URL force `async_mode` à `true`, que vous l'ayez défini ou non, et est vérifié par rapport à la limite de lot de votre plan. | Nécessite l'accès au traitement par lot |
| `job_id` | `string` | `null` | Un identifiant que vous générez, utilisé pour récupérer le résultat si la requête elle-même meurt. Envoyé dans le corps JSON sur les endpoints URL et comme champ de formulaire sur les endpoints de téléversement de fichier. Fonctionne avec tout type de clé. | -- |
| `callback_url` | `string` | `null` | URL de webhook destinée à recevoir un POST à la fin du traitement. | Nécessite l'accès webhook |
| `notification_email` | `string` | E-mail du propriétaire du projet | Adresse e-mail à notifier à la fin du traitement. Si omis, l'e-mail du propriétaire du projet est utilisé par défaut. | -- |
| `direct_download` | `boolean` | Dépend de l'endpoint | Ne peut pas être combiné avec `async_mode: true` ni avec plusieurs URLs. L'une ou l'autre combinaison renvoie `400`. Voir [URLs signées](/fr/docs/concepts/signed-urls.md). | -- |

Une soumission async minimale :

```bash
curl -X POST https://api.enconvert.com/v1/convert/url-to-pdf \
  -H "X-API-Key: sk_your_private_key" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://example.com/very-long-report", "async_mode": true}'
```

---

## Ce que renvoie chaque mode

### Sync

Une conversion V1 qui se termine pendant la requête répond `200` avec les métadonnées et un lien signé vers la sortie :

```json
{
    "presigned_url": "https://spaces.example.com/...signed...",
    "object_key": "env/files/4127/url-to-pdf/example_20260405_123456789.pdf",
    "filename": "example_20260405_123456789.pdf",
    "file_size": 184320,
    "conversion_time_seconds": 3.12
}
```

Les clés publiques et dashboard reçoivent les mêmes cinq champs plus `job_id`, ainsi que les mêmes valeurs reproduites dans les en-têtes de réponse `X-Object-Key`, `X-File-Size`, `X-Conversion-Time` et `X-Filename`.

`POST /v2/perceive` est lui aussi sync, mais son corps est le résultat perceive complet : `operation_id`, `status`, `render_quality`, une table `outputs` d'artefacts signés, et le bloc `structured` en ligne. Cette forme est documentée sur [la page perceive](/fr/docs/endpoints/perceive.md).

### Async

Une soumission V1 async ou par lot répond `202` et rien d'autre :

```json
{
    "status": "processing",
    "batch_id": "550e8400-e29b-41d4-a716-446655440000",
    "url_count": 3,
    "output_format": "individual"
}
```

Un lot perceive trop grand pour s'exécuter en ligne répond `202` avec un `job_id` :

```json
{
    "job_id": "bat_8c1a...",
    "status": "queued",
    "output_mode": "manifest",
    "total": 40,
    "completed": 0,
    "failed": 0,
    "pending": 40
}
```

Le corps complet du lot porte aussi `zip`, `items` et `warnings`. Une soumission ingest répond `202` avec un identifiant préfixé par `ing_` :

```json
{
    "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"
}
```

<div class="alert alert-warning">
<strong>La prise porte deux noms.</strong> La V1 renvoie <code>batch_id</code>. La V2 renvoie <code>job_id</code>. C'est la même idée (une chaîne opaque avec laquelle vous interrogez) mais ce sont des champs différents sur des endpoints différents, et rien ne fait la traduction entre les deux. Lisez le champ que l'endpoint appelé renvoie réellement.
</div>

Un dernier cas mérite d'être connu. Un lot perceive de 10 URLs ou moins s'exécute normalement en ligne et répond `200` avec chaque élément renseigné, mais si cette exécution en ligne dépasse sa fenêtre d'attente de 240 secondes, elle se dégrade en `202` avec `status: "processing"` et un avertissement vous invitant à interroger. Traitez donc `202` comme possible sur chaque appel par lot, pas uniquement sur les gros.

---

## Endpoints de statut et statuts terminaux

| Tâche | Interroger | Non terminal | Terminal |
|-----|------|--------------|----------|
| Conversion V1 async ou par lot | `GET /v1/convert/batch/{batch_id}` | `processing` | `completed`, `partial`, `failed` |
| Conversion V1 sync avec votre propre `job_id` | `GET /v1/convert/status/{job_id}` | `processing` | `success`, `failed` |
| Lot perceive V2 | `GET /v2/perceive/batch/{job_id}` | `queued`, `processing` | `completed`, `partial`, `failed`, `canceled` |
| Ingest V2 | `GET /v2/ingest/{job_id}` | `queued`, `discovering`, `processing` | `completed`, `failed`, `canceled` |

`partial` signifie que la tâche s'est terminée et que certaines unités ont échoué. C'est un statut terminal. Ne le traitez pas comme un signal de nouvelle tentative à lui seul : lisez les lignes élément par élément et ne réessayez que les échecs.

À l'intérieur d'un lot perceive V2, chaque élément porte son propre `status` : `queued`, `processing`, `completed` ou `failed`. Il n'y a ni `partial` ni `canceled` au niveau de l'élément, uniquement au niveau du lot.

La réponse de lot V1 mélange les casses : le `status` agrégé est en minuscules (`processing`, `completed`, `partial`, `failed`) tandis que le `status` de chaque élément est en casse de titre (`Success`, `In Progress`, `Failed`). Comparez exactement, ou normalisez avant de comparer.

Les deux types de tâches V2 peuvent être annulés : `DELETE /v2/perceive/batch/{job_id}` et `DELETE /v2/ingest/{job_id}`. Les deux sont idempotents, les deux arrêtent le worker entre deux unités, et le travail déjà terminé conserve ses artefacts.

---

## Le contrat d'interrogation

L'API ne vous dit pas à quelle fréquence interroger. Il n'y a pas d'en-tête `Retry-After` sur un `202`, ni d'intervalle recommandé dans le corps. Le contrat se limite à ceci : le `202` porte l'identifiant, vous faites un GET sur l'endpoint de statut correspondant, et vous vous arrêtez quand `status` atteint une valeur terminale.

Ce qu'il faut utiliser en pratique :

- **Cinq secondes** est une valeur par défaut raisonnable pour les lots V1 et pour les tâches ingest. Les deux passent l'essentiel de leur vie sur des rendus navigateur qui prennent environ 10 à 30 secondes par page : interroger plus vite ne vous achète guère que des requêtes supplémentaires.
- **Trois secondes** est ce qu'utilisent les SDK officiels pour la récupération après timeout sur une conversion unique, où la réponse est généralement à quelques secondes.
- Ajoutez une échéance. Les SDK attendent par défaut 30 minutes sur les lots de site entier et 5 minutes pour la récupération après timeout.
- Les lectures de statut sont des GET. Le limiteur de débit ne s'applique jamais qu'aux requêtes POST : l'interrogation ne compte donc pas dans votre limite par minute, et lire un statut ne facture aucune op.

Une boucle d'interrogation sur une tâche ingest :

```python
import time
import requests

HEADERS = {"X-API-Key": "sk_your_private_key"}
TERMINAL = {"completed", "failed", "canceled"}

job = requests.post(
    "https://api.enconvert.com/v2/ingest",
    headers=HEADERS,
    json={"mode": "sitemap", "url": "https://example.com", "max_pages": 200},
).json()

while True:
    status = requests.get(
        f"https://api.enconvert.com/v2/ingest/{job['job_id']}",
        headers=HEADERS,
    ).json()

    print(status["status"], status["pages_processed"], "pages")

    if status["status"] in TERMINAL:
        break

    time.sleep(5)

if status["status"] == "completed":
    print(status["output_url"])  # signed for 15 minutes
```

Chaque interrogation forge un nouveau jeu d'URLs de téléchargement signées sur les mêmes objets stockés : un lien qui a expiré pendant votre lecture est donc remplacé en interrogeant simplement à nouveau. C'est couvert dans [URLs signées](/fr/docs/concepts/signed-urls.md).

Si vous préférez être averti plutôt que demander, enregistrez un webhook et supprimez complètement la boucle. Voir [Webhooks](/fr/docs/guides/webhooks.md) pour les charges utiles, le schéma de signature et la politique de nouvelles tentatives.

---

## Récupération après timeout : envoyez votre propre identifiant de tâche

Les conversions longues ont un problème de connexion, pas un problème de traitement. Le rendu d'une page lourde ou un document volumineux peut dépasser le reverse-proxy placé devant l'API (typiquement 60 à 120 secondes), et la passerelle elle-même annule toute requête qui n'a pas commencé à répondre dans les 300 secondes, en répondant `504` avec `{"error": "Request timeout"}`. Dans les deux cas, la conversion se termine souvent quand même sur le serveur. Le résultat existe. C'est seulement votre connexion qui n'a pas survécu pour le voir.

La solution est de nommer la tâche avant de la démarrer :

1. Générez un UUID et envoyez-le comme `job_id`, dans le corps JSON sur les endpoints URL ou comme champ de formulaire sur les téléversements de fichier.
2. Si la requête renvoie une 5xx ou si la connexion tombe, ne resoumettez pas. Interrogez `GET /v1/convert/status/{job_id}`.
3. Arrêtez-vous quand `status` vaut `success` ou `failed`.

```python
import time
import uuid
import requests

HEADERS = {"X-API-Key": "sk_your_private_key"}
job_id = str(uuid.uuid4())

response = requests.post(
    "https://api.enconvert.com/v1/convert/url-to-pdf",
    headers=HEADERS,
    json={"url": "https://example.com/heavy-report", "job_id": job_id},
)

if response.status_code >= 500:
    while True:
        status = requests.get(
            f"https://api.enconvert.com/v1/convert/status/{job_id}",
            headers=HEADERS,
        ).json()
        if status["status"] != "processing":
            break
        time.sleep(3)
else:
    status = response.json()
```

L'endpoint de statut répond toujours `200` avec l'un de trois corps possibles : vérifiez donc le champ `status` plutôt que le code HTTP :

```json
{"status": "processing"}
```

```json
{
    "status": "success",
    "presigned_url": "https://spaces.example.com/...signed...",
    "object_key": "env/files/4127/url-to-pdf/report_20260405_123456789.pdf"
}
```

```json
{"status": "failed", "error": "Page load timeout"}
```

Un identifiant inconnu renvoie `404`, et un identifiant appartenant à un autre projet renvoie `403`. Réutiliser l'un de vos propres identifiants réinitialise la ligne de cette tâche : choisissez donc un nouvel UUID par requête ; revendiquer un identifiant déjà détenu par un autre projet renvoie `409` avec `job_id already in use`.

<div class="alert alert-info">
<strong>Les SDK le font pour vous.</strong> Chaque SDK officiel génère un <code>job_id</code> par conversion V1, et si l'appel renvoie une 5xx, il bascule silencieusement vers l'interrogation de <code>GET /v1/convert/status/{job_id}</code> jusqu'à ce que la tâche soit <code>success</code> ou <code>failed</code>. Vous n'écrivez aucun code de récupération. Voir <a href="/fr/docs/guides/integrations/sdks">SDK</a>.
</div>

La V2 n'a pas besoin de cette astuce. Ses traitements de longue durée renvoient déjà un objet de tâche explicite : vous interrogez donc `GET /v2/perceive/batch/{job_id}` ou `GET /v2/ingest/{job_id}` à la place.

---

## Quand l'async est le seul choix raisonnable

Certaines tâches ne tiennent pas dans une requête et l'API ne prétend pas le contraire :

- **Rendus de site entier.** `website-to-pdf` et `website-to-screenshot` explorent un site et regroupent la sortie dans un ZIP. Ils sont exclusivement async et répondent toujours `202`.
- **Ingest.** Chaque page d'une tâche ingest passe par un vrai rendu navigateur d'environ 10 à 30 secondes, si bien que tout crawl non trivial dépasse la fenêtre de requête de 300 secondes avant d'être à moitié fait. Les deux points d'entrée ingest répondent `202` par construction.
- **Lots perceive de plus de 10 URLs.** Dix est le plafond en ligne. Au-delà, vous obtenez une tâche.
- **Tout ce pour quoi vous préférez ne pas garder une socket ouverte.** Un lot de 40 URLs est techniquement interrogeable dans une seule boucle, mais un webhook doublé d'une file d'attente de votre côté survit à vos propres déploiements et redémarrages. Les tâches par lot survivent à un redémarrage de la passerelle et reprennent : vous ne resoumettez jamais.

Les conversions par téléversement de fichier font exception à tout cela. Elles n'ont aucun mode async : une conversion de document lente se récupère donc par interrogation avec `job_id` plutôt qu'avec `async_mode`. Si le problème vient du fichier lui-même, vérifiez le plafond d'envoi par plan dans [Limites de débit et quotas](/fr/docs/reference/rate-limits.md) avant de conclure à un timeout.

Pour la forme complète d'une requête par lot, le regroupement en ZIP et les résultats élément par élément, voir [Traitement par lot](/fr/docs/guides/batch-processing.md).

---

## Questions fréquentes

### Comment rendre une conversion EnConvert asynchrone ?

Définissez `async_mode: true` dans le corps JSON de `url-to-pdf`, `url-to-screenshot` ou `url-to-markdown`, ou transmettez un tableau d'URLs, ce qui force l'async à lui seul. L'appel répond `202` avec un `batch_id` que vous interrogez à `GET /v1/convert/batch/{batch_id}`. Les endpoints de téléversement de fichier ne lisent jamais `async_mode` et s'exécutent toujours de façon synchrone.

### Quels sont les statuts terminaux d'une tâche EnConvert ?

Un lot V1 se termine sur `completed`, `partial` ou `failed`. Un lot perceive V2 se termine sur `completed`, `partial`, `failed` ou `canceled`. Une tâche ingest V2 se termine sur `completed`, `failed` ou `canceled`. Tout le reste (`processing`, `queued`, `discovering`) signifie qu'il faut continuer à interroger.

### À quelle fréquence faut-il interroger un endpoint de statut de tâche ?

L'API ne fixe aucune cadence et n'envoie aucun en-tête `Retry-After`. Cinq secondes est une valeur par défaut sensée pour les lots et les tâches ingest, puisque chaque rendu de page prend environ 10 à 30 secondes. Les lectures de statut sont des GET : elles échappent donc au limiteur de débit et ne facturent aucune op, mais il n'y a pas non plus de raison d'interroger toutes les 200 ms.

### Ma requête de conversion a expiré. Le fichier est-il perdu ?

En général non. Si vous avez envoyé votre propre `job_id`, interrogez `GET /v1/convert/status/{job_id}` : la conversion se termine souvent sur le serveur après que la connexion est déjà tombée. L'endpoint répond `200` avec `processing`, `success` ou `failed`. Chaque SDK officiel effectue cette récupération automatiquement.

### Pourquoi mon lot renvoie-t-il batch_id alors que la doc mentionne job_id ?

Les deux existent. Les endpoints de conversion V1 renvoient `batch_id` et s'interrogent à `GET /v1/convert/batch/{batch_id}`. Les endpoints V2 renvoient `job_id` et s'interrogent à `GET /v2/perceive/batch/{job_id}` ou `GET /v2/ingest/{job_id}`. Lisez le champ que l'endpoint appelé a renvoyé.
