Job sync e async#

La maggior parte delle chiamate a EnConvert ti consegna il risultato finito nel body della risposta. Alcune ti consegnano invece un id e svolgono il lavoro in background. Quale delle due ottieni dipende dall'endpoint che chiami e, su alcuni endpoint, da che cosa metti nella richiesta.


Che cosa decide la modalità#

Endpoint Modalità
Tutte le conversioni con caricamento file (documenti, formati dati, immagini) Sempre sync. Su questi endpoint async_mode non viene mai letto.
url-to-pdf, url-to-screenshot, url-to-markdown Sync per impostazione predefinita. Async quando imposti async_mode: true, oppure quando url è un array.
website-to-pdf, website-to-screenshot Sempre async. Entrambi rispondono 202 con un batch_id e output_format: "zip".
POST /v2/perceive Sempre sync. Un singolo URL viene renderizzato nella richiesta stessa e non esiste un interruttore async.
POST /v2/perceive/batch Sync fino a 10 URL, async oltre.
POST /v2/ingest, POST /v2/ingest/files Sempre async. Entrambi rispondono 202 con un job_id.

Le chiavi pubbliche e dashboard sono vincolate a richieste sync con URL singolo sugli endpoint URL V1, qualunque cosa dica il body.

Modalità Sync Modalità Async
Trigger Predefinita per URL singolo / caricamento file URL multipli, oppure async_mode: true
Risposta 200 OK con il risultato 202 Accepted con batch_id
Consegna del risultato Byte del file o URL presigned nella risposta Polling, webhook o email
Tipi di chiave Chiavi private e pubbliche Solo chiavi private
Requisito del piano Tutti i piani Richiede l'accesso async (Indie+)
Limitazioni del piano: la modalità async non è disponibile nel piano free. Se provi a impostare async_mode: true o a inviare più URL con un piano free, la risposta è 403 Forbidden.

Async e batch appartengono entrambi ai piani a pagamento. Il piano Founding non ha né l'uno né l'altro, ed è per questo che un primo test con una chiave free che invia tre URL torna indietro 403 e non 202. I numeri per piano, incluso il tetto sulla dimensione del batch, stanno in Rate limit e quote.


Chiedere esplicitamente l'async#

Questi sono i campi della richiesta che decidono la modalità o che ti danno un riferimento al job risultante. Tutto il resto della richiesta (opzioni di rendering, opzioni PDF, denominazione dell'output) non cambia tra le due modalità.

Parametro Tipo Predefinito Descrizione Limitazioni del piano
async_mode boolean false Mette il lavoro in coda e risponde 202 invece di tenere aperta la connessione. Letto solo da url-to-pdf, url-to-screenshot e url-to-markdown. Richiede l'accesso async
url (array) string[] -- Più di un URL forza async_mode a true che tu lo imposti o meno, e viene verificato rispetto al limite di batch del tuo piano. Richiede l'accesso batch
job_id string null Un id che generi tu, usato per recuperare il risultato se la richiesta stessa muore. Inviato nel body JSON sugli endpoint URL e come campo del form sugli endpoint di caricamento file. Funziona con qualsiasi tipo di chiave. --
callback_url string null URL webhook che riceve un POST al completamento. Richiede l'accesso webhook
notification_email string Email del proprietario del progetto Indirizzo email da notificare al completamento. Se omesso, viene usata l'email del proprietario del progetto. --
direct_download boolean Dipende dall'endpoint Non può essere combinato con async_mode: true né con più URL. Entrambe le combinazioni restituiscono 400. Vedi URL firmati. --

Un invio async minimo:

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

Che cosa torna indietro in ciascuna modalità#

Sync#

Una conversione V1 che si completa nella richiesta stessa risponde 200 con i metadati e un link firmato all'output:

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

Le chiavi pubbliche e dashboard ottengono gli stessi cinque campi più job_id, e gli stessi valori replicati negli header di risposta X-Object-Key, X-File-Size, X-Conversion-Time e X-Filename.

Anche POST /v2/perceive è sync, ma il suo body è il risultato completo del perceive: operation_id, status, render_quality, una mappa outputs di artefatti firmati e il blocco structured inline. Questa forma è documentata nella pagina di perceive.

Async#

Un invio V1 async o batch risponde 202 e nient'altro:

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

Un batch di perceive troppo grande per essere eseguito inline risponde 202 con un job_id:

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

Il body completo del batch trasporta anche zip, items e warnings. Un invio di ingest risponde 202 con un id preceduto dal prefisso 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"
}
Il riferimento ha due nomi. V1 restituisce batch_id. V2 restituisce job_id. Sono la stessa idea (una stringa opaca con cui fare polling) ma sono campi diversi su endpoint diversi, e nulla converte l'uno nell'altro. Leggi il campo che l'endpoint che hai chiamato restituisce davvero.

C'è un altro caso che vale la pena conoscere. Un batch di perceive di 10 URL o meno normalmente viene eseguito inline e risponde 200 con ogni elemento popolato, ma se quell'esecuzione inline supera la sua finestra di attesa di 240 secondi, degrada a un 202 con status: "processing" e un warning che ti dice di fare polling. Quindi considera 202 possibile su ogni chiamata batch, non solo su quelle grandi.


Endpoint di stato e stati terminali#

Job Polling Non terminali Terminali
Conversione V1 async o batch GET /v1/convert/batch/{batch_id} processing completed, partial, failed
Conversione V1 sync con un tuo job_id GET /v1/convert/status/{job_id} processing success, failed
Batch di 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 che il job è terminato e alcune unità sono fallite. È terminale. Non trattarlo da solo come un segnale di retry; leggi le righe per singolo elemento e riprova solo i fallimenti.

All'interno di un batch di perceive V2, ogni elemento porta con sé il proprio status tra queued, processing, completed o failed. Non esiste partialcanceled a livello di elemento, solo sul batch.

La risposta batch V1 mescola le maiuscole: lo status aggregato è in minuscolo (processing, completed, partial, failed) mentre lo status di ogni elemento ha le iniziali maiuscole (Success, In Progress, Failed). Confronta in modo esatto, oppure normalizza prima di confrontare.

Entrambi i tipi di job V2 possono essere annullati: DELETE /v2/perceive/batch/{job_id} e DELETE /v2/ingest/{job_id}. Entrambi sono idempotenti, entrambi fermano il worker tra un'unità e l'altra, e il lavoro già completato conserva i suoi artefatti.


Il contratto di polling#

L'API non ti dice con quale frequenza fare polling. Non c'è un header Retry-After su un 202 e non c'è un intervallo consigliato nel body. Il contratto è solo questo: il 202 trasporta l'id, tu fai una GET sull'endpoint di stato corrispondente e ti fermi quando status raggiunge un valore terminale.

Che cosa usare nella pratica:

  • Cinque secondi sono un valore predefinito ragionevole per i batch V1 e per i job di ingest. Entrambi passano gran parte della loro vita su render del browser che richiedono all'incirca dai 10 ai 30 secondi per pagina, quindi fare polling più in fretta ti compra soprattutto richieste in più.
  • Tre secondi è quello che usano gli SDK ufficiali per il recupero dopo un timeout su una singola conversione, dove la risposta di solito è a pochi secondi di distanza.
  • Metti una scadenza. Gli SDK usano di default un'attesa di 30 minuti sui batch di interi siti e di 5 minuti sul recupero dopo un timeout.
  • Le letture di stato sono GET. Il rate limiter si applica solo alle richieste POST, quindi il polling non pesa sul tuo limite al minuto e leggere uno stato non consuma una op.

Un ciclo di polling su un job di 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

Ogni polling conia un nuovo set di URL di download firmati sugli stessi oggetti archiviati, quindi un link scaduto mentre lo stavi leggendo viene sostituito semplicemente rifacendo polling. Se ne parla in URL firmati.

Se preferisci essere avvisato invece di chiedere, registra un webhook e salta del tutto il ciclo. Vedi Webhook per i payload, lo schema di firma e la politica di retry.


Recupero dopo un timeout: invia un tuo job id#

Le conversioni lunghe hanno un problema di connessione, non di elaborazione. Il render di una pagina pesante o un documento di grandi dimensioni possono durare più del reverse proxy davanti all'API (tipicamente dai 60 ai 120 secondi), e il gateway stesso annulla qualsiasi richiesta che non ha iniziato a rispondere entro 300 secondi, rispondendo 504 con {"error": "Request timeout"}. In entrambi i casi la conversione spesso si completa comunque sul server. Il risultato esiste. È solo la tua connessione a non essere sopravvissuta per vederlo.

La soluzione è dare un nome al job prima di avviarlo:

  1. Genera un UUID e invialo come job_id, nel body JSON sugli endpoint URL o come campo del form sui caricamenti di file.
  2. Se la richiesta restituisce un 5xx o la connessione cade, non reinviare. Fai polling su GET /v1/convert/status/{job_id}.
  3. Fermati quando status è 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()

L'endpoint di stato risponde sempre 200 con uno di tre body, quindi controlla il campo status anziché il codice 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 sconosciuto restituisce 404, e un id di proprietà di un altro progetto restituisce 403. Riutilizzare uno dei tuoi id azzera quella riga di job, quindi scegli un UUID nuovo per ogni richiesta; rivendicare un id già in possesso di un altro progetto restituisce 409 con job_id already in use.

Gli SDK lo fanno per te. Ogni SDK ufficiale genera un job_id per ciascuna conversione V1 e, se la chiamata restituisce 5xx, passa silenziosamente al polling di GET /v1/convert/status/{job_id} finché il job non è success o failed. Non scrivi nessun codice di recupero. Vedi SDK.

V2 non ha bisogno di questo trucco. Il suo lavoro di lunga durata restituisce già un oggetto job esplicito, quindi fai invece polling su GET /v2/perceive/batch/{job_id} o GET /v2/ingest/{job_id}.


Quando l'async è l'unica scelta sensata#

Alcuni job non stanno dentro una richiesta e l'API non finge il contrario:

  • Render di interi siti. website-to-pdf e website-to-screenshot fanno il crawl di un sito e raggruppano l'output in uno ZIP. Sono solo async e rispondono sempre 202.
  • Ingest. Ogni pagina di un job di ingest passa attraverso un vero render del browser che richiede all'incirca dai 10 ai 30 secondi ciascuna, quindi qualsiasi crawl non banale supera la finestra di richiesta di 300 secondi prima di essere a metà. Entrambi i punti di ingresso di ingest sono 202 per costruzione.
  • Batch di perceive oltre 10 URL. Dieci è il tetto per l'esecuzione inline. Oltre, ottieni un job.
  • Qualsiasi cosa per cui preferiresti non tenere aperto un socket. Un batch di 40 URL è tecnicamente interrogabile con un solo ciclo, ma un webhook più una coda dalla tua parte sopravvive ai tuoi deploy e riavvii. I job batch sopravvivono a un riavvio del gateway e riprendono, quindi non reinvii mai.

Le conversioni con caricamento file sono l'eccezione a tutto questo. Non hanno alcuna modalità async, quindi una conversione lenta di un documento si recupera con il polling di job_id invece che con async_mode. Se il problema è il file stesso, controlla il tetto di upload per piano in Rate limit e quote prima di dare per scontato un timeout.

Per la forma completa della richiesta batch, il raggruppamento in ZIP e i risultati per singolo elemento, vedi Elaborazione batch.


Domande frequenti#

Come rendo asincrona una conversione EnConvert?#

Imposta async_mode: true nel body JSON di url-to-pdf, url-to-screenshot o url-to-markdown, oppure passa un array di URL, che da solo forza l'async. La chiamata risponde 202 con un batch_id su cui fai polling a GET /v1/convert/batch/{batch_id}. Gli endpoint di caricamento file non leggono mai async_mode e girano sempre in modo sincrono.

Quali sono gli stati terminali di un job EnConvert?#

Un batch V1 termina su completed, partial o failed. Un batch di perceive V2 termina su completed, partial, failed o canceled. Un job di ingest V2 termina su completed, failed o canceled. Tutto il resto (processing, queued, discovering) significa continuare a fare polling.

Con che frequenza devo interrogare un endpoint di stato di un job?#

L'API non fissa una cadenza e non invia alcun header Retry-After. Cinque secondi sono un valore predefinito sensato per i batch e i job di ingest, dato che ogni render di pagina richiede all'incirca dai 10 ai 30 secondi. Le letture di stato sono GET, quindi restano fuori dal rate limiter e non consumano ops, ma non c'è comunque motivo di fare polling ogni 200 ms.

La mia richiesta di conversione è andata in timeout. Il file è perso?#

Di solito no. Se hai inviato un tuo job_id, fai polling su GET /v1/convert/status/{job_id}: la conversione spesso si completa sul server dopo che la connessione è già caduta. L'endpoint risponde 200 con processing, success o failed. Ogni SDK ufficiale esegue questo recupero automaticamente.

Perché il mio batch restituisce batch_id ma la documentazione parla di job_id?#

Esistono entrambi. Gli endpoint di conversione V1 restituiscono batch_id e si interrogano a GET /v1/convert/batch/{batch_id}. Gli endpoint V2 restituiscono job_id e si interrogano a GET /v2/perceive/batch/{job_id} o GET /v2/ingest/{job_id}. Leggi il campo che ha restituito l'endpoint che hai chiamato.