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)
Restriction par plan : Le mode async n'est pas disponible sur le plan free. Toute tentative de définir async_mode: true ou de soumettre plusieurs URLs sur un plan free renvoie 403 Forbidden.

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.


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. --

Une soumission async minimale :

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 :

{
    "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.

Async#

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

{
    "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 :

{
    "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_ :

{
    "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"
}
La prise porte deux noms. La V1 renvoie batch_id. La V2 renvoie job_id. 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.

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 :

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.

Si vous préférez être averti plutôt que demander, enregistrez un webhook et supprimez complètement la boucle. Voir Webhooks 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.
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 :

{"status": "processing"}
{
    "status": "success",
    "presigned_url": "https://spaces.example.com/...signed...",
    "object_key": "env/files/4127/url-to-pdf/report_20260405_123456789.pdf"
}
{"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.

Les SDK le font pour vous. Chaque SDK officiel génère un job_id par conversion V1, et si l'appel renvoie une 5xx, il bascule silencieusement vers l'interrogation de GET /v1/convert/status/{job_id} jusqu'à ce que la tâche soit success ou failed. Vous n'écrivez aucun code de récupération. Voir SDK.

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 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.


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é.