Trabajos síncronos y asíncronos#

La mayoría de las llamadas a EnConvert te entregan el resultado terminado en el cuerpo de la respuesta. Algunas te entregan un id en su lugar y hacen el trabajo en segundo plano. Cuál de las dos te toca depende del endpoint que llames y, en unos pocos endpoints, de lo que pongas en la solicitud.


Qué decide el modo#

Endpoint Modo
Todas las conversiones de subida de archivos (documentos, formatos de datos, imágenes) Siempre síncronas. async_mode nunca se lee en estos endpoints.
url-to-pdf, url-to-screenshot, url-to-markdown Síncronos por defecto. Asíncronos cuando defines async_mode: true, o cuando url es un array.
website-to-pdf, website-to-screenshot Siempre asíncronos. Ambos responden 202 con un batch_id y output_format: "zip".
POST /v2/perceive Siempre síncrono. Una sola URL se renderiza dentro de la solicitud y no hay interruptor asíncrono.
POST /v2/perceive/batch Síncrono para 10 URLs o menos, asíncrono por encima de eso.
POST /v2/ingest, POST /v2/ingest/files Siempre asíncronos. Ambos responden 202 con un job_id.

Las claves públicas y de dashboard quedan restringidas a solicitudes síncronas de una sola URL en los endpoints de URL de V1, diga lo que diga el cuerpo.

Modo síncrono Modo asíncrono
Activación Predeterminado para una sola URL / subida de archivo Múltiples URLs, o async_mode: true
Respuesta 200 OK con el resultado 202 Accepted con batch_id
Entrega del resultado Bytes del archivo o URL prefirmada en la respuesta Sondeo, webhook o correo electrónico
Tipos de clave Claves privadas y públicas Solo claves privadas
Requisito de plan Todos los planes Requiere acceso asíncrono (Indie+)
Restricción por plan: El modo asíncrono no está disponible en el plan gratuito. Intentar establecer async_mode: true o enviar múltiples URLs en un plan gratuito devuelve 403 Forbidden.

El modo asíncrono y los lotes pertenecen ambos a los planes de pago. El plan Founding no tiene ninguno de los dos, y por eso una primera prueba con una clave gratuita que envía tres URLs vuelve como 403 y no como 202. Las cifras por plan, incluido el tope de tamaño de lote, están en Límites de frecuencia y cuotas.


Pedir el modo asíncrono explícitamente#

Estos son los campos de la solicitud que deciden el modo o que te dan un identificador con el que seguir el job resultante. Todo lo demás de la solicitud (opciones de renderizado, opciones de PDF, nombrado de la salida) no cambia entre los dos modos.

Parámetro Tipo Por defecto Descripción Restricción por plan
async_mode boolean false Encola el trabajo y responde 202 en lugar de mantener la conexión abierta. Solo lo leen url-to-pdf, url-to-screenshot y url-to-markdown. Requiere acceso asíncrono
url (array) string[] -- Más de una URL fuerza async_mode a true lo hayas definido o no, y se comprueba contra el límite de lotes de tu plan. Requiere acceso a lotes
job_id string null Un id que generas tú, usado para recuperar el resultado si la propia solicitud muere. Se envía en el cuerpo JSON en los endpoints de URL y como campo de formulario en los endpoints de subida de archivos. Funciona con cualquier tipo de clave. --
callback_url string null URL de webhook que recibe un POST al completarse. Requiere acceso a webhooks
notification_email string Correo del propietario del proyecto Dirección de correo a la que notificar al completarse. Si se omite, se usa por defecto el correo del propietario del proyecto. --
direct_download boolean Depende del endpoint No se puede combinar con async_mode: true ni con varias URLs. Cualquiera de las dos combinaciones devuelve 400. Consulta URLs firmadas. --

Un envío asíncrono mínimo:

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}'

Qué vuelve en cada modo#

Síncrono#

Una conversión V1 que termina dentro de la solicitud responde 200 con los metadatos y un enlace firmado a la salida:

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

Las claves públicas y de dashboard reciben los mismos cinco campos más job_id, y los mismos valores reflejados en las cabeceras de respuesta X-Object-Key, X-File-Size, X-Conversion-Time y X-Filename.

POST /v2/perceive también es síncrono, pero su cuerpo es el resultado completo de perceive: operation_id, status, render_quality, un mapa outputs de artefactos firmados y el bloque structured inline. Esa forma está documentada en la página de perceive.

Asíncrono#

Un envío V1 asíncrono o por lotes responde 202 y nada más:

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

Un lote de perceive demasiado grande para ejecutarse inline responde 202 con un job_id:

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

El cuerpo completo del lote lleva además zip, items y warnings. Un envío de ingest responde 202 con un id con el prefijo 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"
}
El identificador tiene dos nombres. V1 devuelve batch_id. V2 devuelve job_id. Son la misma idea (una cadena opaca con la que sondeas), pero son campos distintos en endpoints distintos, y nada traduce entre ellos. Lee el campo que devuelve de verdad el endpoint que llamaste.

Hay un caso más que conviene conocer. Un lote de perceive de 10 URLs o menos normalmente se ejecuta inline y responde 200 con todos los elementos rellenos, pero si esa ejecución inline supera su ventana de espera de 240 segundos, degrada a un 202 con status: "processing" y una advertencia que te dice que sondees. Así que da por posible un 202 en cualquier llamada por lotes, no solo en las grandes.


Endpoints de estado y estados terminales#

Job Sondeo No terminal Terminal
Conversión V1 asíncrona o por lotes GET /v1/convert/batch/{batch_id} processing completed, partial, failed
Conversión síncrona V1 con tu propio job_id GET /v1/convert/status/{job_id} processing success, failed
Lote de 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 significa que el job terminó y que algunas unidades fallaron. Es terminal. No lo trates como señal de reintento por sí solo; lee las filas por elemento y reintenta solo los fallos.

Dentro de un lote de perceive V2, cada elemento lleva su propio status: queued, processing, completed o failed. No hay partial ni canceled a nivel de elemento, solo en el lote.

La respuesta de lote de V1 mezcla mayúsculas y minúsculas: el status agregado va en minúsculas (processing, completed, partial, failed) mientras que el status de cada elemento va con iniciales en mayúscula (Success, In Progress, Failed). Compara de forma exacta, o normaliza antes de comparar.

Los dos tipos de job de V2 se pueden cancelar: DELETE /v2/perceive/batch/{job_id} y DELETE /v2/ingest/{job_id}. Ambos son idempotentes, ambos detienen al worker entre unidades, y el trabajo ya terminado conserva sus artefactos.


El contrato de sondeo#

La API no te dice a qué ritmo sondear. No hay cabecera Retry-After en un 202 ni intervalo recomendado en el cuerpo. El contrato es solo este: el 202 lleva el id, haces un GET al endpoint de estado correspondiente y paras cuando status alcanza un valor terminal.

Qué usar en la práctica:

  • Cinco segundos es un valor por defecto razonable para los lotes de V1 y para los jobs de ingest. Ambos pasan la mayor parte de su vida en renderizados de navegador que tardan entre 10 y 30 segundos por página, así que sondear más rápido sobre todo te compra solicitudes de más.
  • Tres segundos es lo que usan los SDK oficiales para la recuperación tras timeout de una sola conversión, donde la respuesta suele estar a segundos de distancia.
  • Pon un plazo límite. Los SDK esperan por defecto 30 minutos en los lotes de sitio completo y 5 minutos en la recuperación tras timeout.
  • Las lecturas de estado son GET. El limitador de frecuencia solo se aplica a las solicitudes POST, así que sondear no cuenta contra tu límite por minuto, y leer un estado no factura ninguna op.

Un bucle de sondeo contra un job de 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

Cada sondeo acuña un conjunto nuevo de URLs de descarga firmadas sobre los mismos objetos almacenados, así que un enlace que expiró mientras leías se reemplaza sin más que volver a sondear. Eso está cubierto en URLs firmadas.

Si prefieres que te avisen a preguntar, registra un webhook y sáltate el bucle por completo. Consulta Webhooks para los payloads, el esquema de firma y la política de reintentos.


Recuperación tras timeout: envía tu propio id de job#

Las conversiones largas tienen un problema de conexión, no de procesamiento. Un renderizado de página pesado o un documento grande pueden durar más que el proxy inverso que está delante de la API (normalmente de 60 a 120 segundos), y la propia gateway cancela cualquier solicitud que no haya empezado a responder en 300 segundos, contestando 504 con {"error": "Request timeout"}. En ambos casos la conversión a menudo termina igualmente en el servidor. El resultado existe. Lo que no sobrevivió para verlo fue tu conexión.

La solución es dar nombre al job antes de arrancarlo:

  1. Genera un UUID y envíalo como job_id, en el cuerpo JSON en los endpoints de URL o como campo de formulario en las subidas de archivos.
  2. Si la solicitud devuelve un 5xx o la conexión se cae, no reenvíes. Sondea GET /v1/convert/status/{job_id}.
  3. Para cuando status sea success o 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()

El endpoint de estado responde siempre 200 con uno de tres cuerpos, así que comprueba el campo status en lugar del código 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 id desconocido devuelve 404, y un id que pertenece a otro proyecto devuelve 403. Reutilizar uno de tus propios ids reinicia esa fila de job, así que elige un UUID nuevo por solicitud; reclamar un id que ya tiene otro proyecto devuelve 409 con job_id already in use.

Los SDK hacen esto por ti. Todos los SDK oficiales generan un job_id por cada conversión V1, y si la llamada devuelve un 5xx cambian en silencio a sondear GET /v1/convert/status/{job_id} hasta que el job esté en success o failed. No escribes nada de código de recuperación. Consulta SDK.

V2 no necesita este truco. Su trabajo de larga duración ya devuelve un objeto de job explícito, así que en su lugar sondeas GET /v2/perceive/batch/{job_id} o GET /v2/ingest/{job_id}.


Cuándo el modo asíncrono es la única opción sensata#

Algunos jobs no caben en una solicitud y la API no va a fingir lo contrario:

  • Renderizados de sitio completo. website-to-pdf y website-to-screenshot rastrean un sitio y empaquetan la salida en un ZIP. Son solo asíncronos y responden siempre 202.
  • Ingest. Cada página de un job de ingest pasa por un renderizado de navegador real de entre 10 y 30 segundos, así que cualquier rastreo no trivial supera la ventana de 300 segundos de la solicitud antes de ir por la mitad. Los dos puntos de entrada de ingest son 202 por construcción.
  • Lotes de perceive de más de 10 URLs. Diez es el techo inline. Por encima, recibes un job.
  • Cualquier cosa por la que prefieras no mantener un socket abierto. Un lote de 40 URLs se puede sondear técnicamente en un solo bucle, pero un webhook más una cola de tu lado sobrevive a tus propios despliegues y reinicios. Los jobs por lotes sobreviven a un reinicio de la gateway y se reanudan, así que nunca reenvías.

Las conversiones de subida de archivos son la excepción a todo esto. No tienen modo asíncrono en absoluto, así que una conversión de documento lenta se recupera sondeando con job_id y no con async_mode. Si el problema es el archivo en sí, comprueba el techo de subida por plan en Límites de frecuencia y cuotas antes de dar por hecho que fue un timeout.

Para la forma completa de la solicitud por lotes, el empaquetado en ZIP y los resultados por elemento, consulta Procesamiento por lotes.


Preguntas frecuentes#

¿Cómo hago que una conversión de EnConvert sea asíncrona?#

Define async_mode: true en el cuerpo JSON de url-to-pdf, url-to-screenshot o url-to-markdown, o pasa un array de URLs, que ya fuerza el modo asíncrono por sí solo. La llamada responde 202 con un batch_id que sondeas en GET /v1/convert/batch/{batch_id}. Los endpoints de subida de archivos nunca leen async_mode y siempre se ejecutan de forma síncrona.

¿Cuáles son los estados terminales de un job de EnConvert?#

Un lote de V1 termina en completed, partial o failed. Un lote de perceive de V2 termina en completed, partial, failed o canceled. Un job de ingest de V2 termina en completed, failed o canceled. Todo lo demás (processing, queued, discovering) significa seguir sondeando.

¿Con qué frecuencia debo sondear un endpoint de estado de job?#

La API no fija una cadencia y no envía cabecera Retry-After. Cinco segundos es un valor por defecto sensato para los lotes y los jobs de ingest, ya que cada renderizado de página tarda entre 10 y 30 segundos. Las lecturas de estado son GET, así que quedan fuera del limitador de frecuencia y no facturan ops, pero aun así no hay ninguna razón para sondear cada 200 ms.

Mi solicitud de conversión ha agotado el tiempo de espera. ¿Se ha perdido el archivo?#

Normalmente no. Si enviaste tu propio job_id, sondea GET /v1/convert/status/{job_id}: la conversión suele terminar en el servidor después de que la conexión ya se haya caído. El endpoint responde 200 con processing, success o failed. Todos los SDK oficiales hacen esta recuperación automáticamente.

¿Por qué mi lote devuelve batch_id pero la documentación menciona job_id?#

Existen los dos. Los endpoints de conversión de V1 devuelven batch_id y se sondean en GET /v1/convert/batch/{batch_id}. Los endpoints de V2 devuelven job_id y se sondean en GET /v2/perceive/batch/{job_id} o GET /v2/ingest/{job_id}. Lee el campo que devolvió el endpoint que llamaste.