Ingestione dei file#

Ci sono due modi per far arrivare dei byte a EnConvert: caricare tu stesso il file come multipart/form-data, oppure passare un url e lasciare che sia l'API a recuperare la risorsa. Quale dei due puoi usare dipende dall'endpoint, non dal tuo piano.


Quale endpoint accetta cosa#

Famiglia di endpoint Come arrivano i byte Nome del campo
Formati dati, documenti, immagini Upload multipart/form-data, un file per richiesta file
Pagine web (url-to-pdf, url-to-screenshot, url-to-markdown, website-to-pdf, website-to-screenshot) Body JSON, la pagina la recupera EnConvert url
POST /v2/ingest/files multipart/form-data, molti file per job files
POST /v2/perceive, POST /v2/ingest Body JSON, la pagina la recupera EnConvert url

Non esiste una terza via. Gli endpoint di upload file non recuperano un URL al posto tuo, comprese le route generiche anything-to-pdf e anything-to-markdown. Per trasformare una pagina live in un PDF, chiama invece url-to-pdf. Per sapere quali formati accetta ciascun endpoint, vedi formati supportati.


Caricare un file locale#

Il campo del form si chiama file, e ogni endpoint di upload file V1 ne accetta esattamente uno. Tutto il resto nel form è opzionale.

Campo del form Tipo Descrizione
file file Il file da convertire. La sua estensione deve essere accettata dall'endpoint.
output_filename string Nome base personalizzato per l'output. L'estensione di destinazione viene aggiunta automaticamente.
job_id string ID del job fornito dal client per il recupero in caso di timeout. Fai polling su GET /v1/convert/status/{job_id} se la connessione cade.
pdf_options string Stringa JSON di opzioni PDF, sugli endpoint che producono un PDF.
direct_download boolean Accettato solo per parità di forma con gli endpoint URL. Qui non ha effetto: un upload risponde sempre con la busta JSON qui sotto, qualunque cosa invii.

curl#

curl -X POST https://api.enconvert.com/v1/convert/anything-to-pdf \
  -H "X-API-Key: sk_your_private_key" \
  -F "[email protected]" \
  -F "direct_download=false"

La risposta è un JSON con un link di download pre-firmato:

{
    "presigned_url": "https://spaces.example.com/...signed...",
    "object_key": "env/files/{project_id}/anything-to-pdf/quarterly-report_20260714_101530123.pdf",
    "filename": "quarterly-report_20260714_101530123.pdf",
    "file_size": 51240,
    "conversion_time_seconds": 2.1,
    "job_id": null
}

Gli stessi valori vengono riportati negli header di risposta X-Object-Key, X-File-Size, X-Conversion-Time e X-Filename, così puoi leggerli senza analizzare il body. Scarica il file tempestivamente: il link ha vita breve, e URL firmati spiega esattamente quanto.

Python#

import requests

with open("quarterly-report.docx", "rb") as f:
    response = requests.post(
        "https://api.enconvert.com/v1/convert/anything-to-pdf",
        headers={"X-API-Key": "sk_your_private_key"},
        files={"file": ("quarterly-report.docx", f)},
        data={"direct_download": "false"},
    )

response.raise_for_status()
result = response.json()

# Download the PDF from the pre-signed URL.
pdf = requests.get(result["presigned_url"]).content
with open("quarterly-report.pdf", "wb") as out:
    out.write(pdf)

Node.js#

import { readFile, writeFile } from "node:fs/promises";

const form = new FormData();
form.append(
    "file",
    new Blob([await readFile("quarterly-report.docx")]),
    "quarterly-report.docx"
);
form.append("direct_download", "false");

const response = await fetch(
    "https://api.enconvert.com/v1/convert/anything-to-pdf",
    { method: "POST", headers: { "X-API-Key": "sk_your_private_key" }, body: form }
);

const result = await response.json();
const pdf = await fetch(result.presigned_url).then((r) => r.arrayBuffer());
await writeFile("quarterly-report.pdf", Buffer.from(pdf));

Molti file in una sola chiamata#

POST /v2/ingest/files è l'unico endpoint che accetta più di un file. Ripeti il campo files, fino a 200 file per job:

curl -X POST https://api.enconvert.com/v2/ingest/files \
  -H "X-API-Key: sk_your_private_key" \
  -F "[email protected]" \
  -F "[email protected]" \
  -F "[email protected]" \
  -F "max_words=700" \
  -F "webhook_url=https://your-app.example.com/hooks/ingest"

Ogni file viene convertito in Markdown, suddiviso in chunk e assemblato in un unico deliverable JSONL. Il job è sempre asincrono e risponde 202 Accepted con un job_id. Tutti i dettagli sono sulla pagina dell'endpoint ingest.


Lasciare che sia EnConvert a recuperare il file#

Sugli endpoint URL invii un body JSON invece di un form, e il recupero lo fa l'API:

curl -X POST https://api.enconvert.com/v1/convert/url-to-markdown \
  -H "X-API-Key: sk_your_private_key" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://example.com/report"}'

url accetta una stringa o un array di stringhe. Un array commuta la richiesta in modalità asincrona ed è trattato in elaborazione batch.

Sorgenti dietro un login#

Tre campi opzionali permettono al recupero di trasportare credenziali. Tutti e tre richiedono un piano con accesso alla basic auth (Indie in su).

Parametro Tipo Default Descrizione
auth object null Credenziali HTTP Basic Auth: {"username": "...", "password": "..."}.
cookies array null Array di oggetti cookie iniettati prima della navigazione. Massimo 50 per richiesta. Ognuno richiede name, value e, in alternativa, domain o url.
headers object null Header HTTP personalizzati inviati con le richieste. Massimo 20 per richiesta. Non possono includere header bloccati: host, content-length, transfer-encoding, connection, upgrade, te, trailer.
Scope delle credenziali: le credenziali dell'oggetto auth, e un header Authorization (per esempio un Bearer token) passato tramite headers, vengono inviate solo all'origin di destinazione, mai alle sottorisorse di terze parti richieste dalla pagina. Questo impedisce la fuga di credenziali verso host pubblicitari, di analytics o CDN.

Indirizzi privati e interni#

L'url deve essere un indirizzo pubblico http:// o https://. Prima che venga recuperato qualsiasi contenuto, l'URL viene filtrato e rifiutato con 400 Bad Request quando:

  • usa uno schema diverso da http/https;
  • incorpora credenziali come https://user:pass@host/ (usa invece il campo auth);
  • punta a localhost, a un hostname di metadata cloud o a un IP che risolve in un intervallo privato, loopback, link-local, riservato o comunque non pubblico;
  • usa una notazione IP non standard (ottale, esadecimale o packed-integer) che potrebbe risolversi in modo ambiguo.

Vale per l'URL seed, per ogni URL di un batch e per le pagine scoperte dagli endpoint di crawl website-to-*.

Detto chiaramente: EnConvert gira fuori dalla tua rete. Non può raggiungere http://10.0.0.5/report.docx, un hostname .internal o qualsiasi cosa che risolva solo dentro la tua VPC. O rendi il file raggiungibile dalla rete pubblica, oppure leggi tu stesso i byte e caricali.


Che fine fa il nome del tuo file#

Il nome che invii svolge due compiti.

Sceglie il convertitore. L'estensione decide quale percorso di input viene eseguito, quindi dai al file il nome giusto. Un file chiamato report senza estensione viene rifiutato da qualsiasi endpoint dotato di allowlist di estensioni.

Fa da base per il nome dell'output. Il nome del file di output è costruito così:

{base}_{YYYYMMDD_HHMMSSmmm}.{ext}

Il timestamp UTC viene sempre aggiunto in coda, così due conversioni dello stesso file non collidono mai. base viene risolto in questo ordine:

  1. output_filename, se ne hai inviato uno. Se ci hai incluso l'estensione di destinazione, l'estensione viene prima rimossa, così non ottieni report.pdf_20260405_123456789.pdf.
  2. Il nome del file caricato senza la sua estensione. report.docx produce report_20260405_123456789.pdf.
  3. Per le conversioni da URL, il dominio. https://example.com/page produce example_20260405_123456789.pdf.
  4. In mancanza di tutto questo, il letterale output.

La chiave di storage viene sanificata prima che il risultato venga scritto: sopravvive solo il basename, .. viene rimosso, i caratteri <>:"|?* vengono eliminati e gli spazi diventano underscore. Carica My Report (final).docx e il PDF finisce sotto My_Report_(final)_20260405_123456789.pdf. Quel percorso ti viene restituito come object_key e ha la forma {env}/files/{project_id}/{endpoint}/{filename}.

I nomi dei file inviati a POST /v2/ingest/files sono inoltre limitati a 255 caratteri.


Il tetto di dimensione e il 413#

Il tetto di upload è per piano e vale per ogni singolo file.

Piano Upload massimo Byte
Founding 5 MB 5242880
Indie 15 MB 15728640
Studio 50 MB 52428800
Production 150 MB 157286400
Enterprise Negoziato Da contratto

La dimensione viene misurata sulla parte caricata stessa mentre il body arriva in streaming, non su un header che controlli tu, quindi anche un upload chunked senza Content-Length viene verificato allo stesso modo. Superare il limite restituisce 413 Payload Too Large prima che venga svolto qualsiasi lavoro di conversione e prima che venga addebitata qualsiasi op:

{
    "detail": {
        "error": "File too large",
        "file_size": 10485760,
        "max_size": 5242880,
        "tier": "free",
        "key_type": "private"
    }
}

file_size e max_size sono entrambi in byte. tier è lo slug del piano (free, starter, pro, business, enterprise), non il nome visualizzato che vedi sulla pagina dei prezzi, quindi un progetto Studio riporta "tier": "pro". key_type è private, public o dashboard, con fallback a unknown.

5 MB si esauriscono in fretta. Sul piano Founding un PDF scansionato di 40 pagine, o una presentazione con qualche foto a tutta pagina, di solito è già oltre la soglia. Non esiste alcun percorso di upload chunked o riprendibile: la soluzione è un file più piccolo o un piano più grande.

Superare il gate della dimensione non è l'ultimo ostacolo. Una quota mensile esaurita risponde 402 Payment Required e troppe richieste in una finestra rispondono 429, entrambi descritti in rate limit e quote.


Content type e magic byte#

Gli upload passano due controlli, in quest'ordine.

1. L'allowlist delle estensioni. Ogni endpoint dichiara quali estensioni accetta. Una discordanza è 400 Bad Request:

{
    "detail": "Invalid file format '.pdf' for png-to-jpeg. Allowed: .png"
}

2. Il sniff dei magic byte. I primi byte del file vengono confrontati con il gruppo dichiarato dalla sua estensione. Una discordanza ad alta confidenza è anch'essa 400:

{
    "detail": "File content does not match the 'png-to-jpeg' input type."
}

È quello che ottieni se rinomini un PDF in .png e lo carichi: i byte iniziano con %PDF-, l'estensione dice PNG, e i due non concordano. Il controllo esiste perché è l'estensione a instradare la tua richiesta. Senza di esso, i byte PDF arriverebbero a un decoder di immagini e otterresti un errore opaco nelle profondità del convertitore invece di un 400 chiaro sulla soglia, e un file etichettato male di proposito finirebbe in mano a un parser che non avrebbe mai dovuto vederlo.

Due cose che questo non fa, entrambe da sapere:

  • Il Content-Type della parte non viene mai ispezionato. Non c'è alcuna allowlist MIME in tutto il percorso di upload, quindi application/octet-stream va bene. L'estensione del nome file è l'unica cosa che instrada la richiesta, ed è per questo che inviare un file senza estensione fallisce.
  • I formati testuali non hanno una firma affidabile e saltano del tutto il sniff: .json, .csv, .xml, .yaml, .toml, .md, .html, .svg, .txt. Un file .json che in realtà contiene CSV viene accettato in questa fase e fallisce più avanti, nel parser.

Le firme binarie riconosciute sono PNG, JPEG, GIF, WebP, HEIC/HEIF, PDF e il gruppo office (un contenitore ZIP per .docx/.xlsx/.pptx/ODF/EPUB, oppure il vecchio OLE2 per .doc/.xls/.ppt). Il sniff è volutamente fail-open: i byte che non riconosce passano invece di essere bloccati.


File di grandi dimensioni: la checklist#

  1. Controlla prima il tetto. Un 413 costa poco all'API e molto a te, perché per guadagnartelo hai caricato l'intero body.
  2. Aspettati che l'upload sia sincrono. async_mode esiste solo su url-to-pdf, url-to-screenshot e url-to-markdown. Gli endpoint di upload file lo ignorano e convertono sempre dentro la richiesta. Vedi job sincroni e asincroni.
  3. Invia un job_id generato da te. Se un proxy davanti a te chiude la connessione prima che la conversione finisca, il lavoro si completa comunque. Fai polling su GET /v1/convert/status/{job_id} e ottieni {"status": "processing"}, poi {"status": "success", "presigned_url": ..., "object_key": ...} oppure {"status": "failed", "error": ...}. Riusare un tuo ID fa ripartire quel job; riusare l'ID di un altro progetto restituisce 409.
  4. Metti in conto i timeout. Una richiesta è limitata a 300 secondi end to end, dopodiché ricevi 504 con {"error": "Request timeout"}. Le conversioni office basate su LibreOffice hanno un proprio limite di 120 secondi, esposto anch'esso come 504.
  5. Gestisci il 503 con Retry-After: 10. Le conversioni di file girano dietro un gate di ammissione con una coda di attesa limitata. Quando la coda è piena la richiesta viene rifiutata subito invece di mettersi in fila, quindi riprova dopo l'intervallo indicato nell'header.
  6. Per molti documenti, cambia endpoint. POST /v2/ingest/files accetta fino a 200 file, restituisce subito 202 con un job_id e accetta un webhook_url così non fai mai polling. Vedi webhook.
  7. Scarica tempestivamente. I link di output sono firmati e scadono. Se il tuo è scaduto, rileggi l'endpoint di stato: ogni polling conia un link nuovo sullo stesso oggetto in storage.

Domande frequenti#

Quale nome di campo si aspetta l'API EnConvert per l'upload di un file?#

file, inviato come multipart/form-data, un file per richiesta, su ogni endpoint di conversione V1 che accetta un upload. L'eccezione è POST /v2/ingest/files, che usa files e ne accetta fino a 200 per job.

EnConvert può scaricare il file da un URL invece di farmelo caricare?#

Solo sugli endpoint URL (url-to-pdf, url-to-screenshot, url-to-markdown, website-to-pdf, website-to-screenshot) e sugli endpoint V2 /v2/perceive e /v2/ingest. Gli endpoint di conversione con upload file non hanno alcun parametro url. Qualsiasi URL che passi deve essere raggiungibile pubblicamente: gli indirizzi privati, di loopback, link-local e di metadata cloud vengono rifiutati con 400.

Perché il mio upload ha restituito 413 Payload Too Large?#

Il file era più grande del tetto per file del tuo piano: 5 MB su Founding, 15 MB su Indie, 50 MB su Studio e 150 MB su Production. Il body della risposta porta un oggetto detail con error, file_size, max_size, tier e key_type, così puoi mostrare al chiamante i numeri esatti.

Perché il mio upload PNG fallisce con "File content does not match"?#

I primi byte del file appartengono a un formato diverso da quello dichiarato dall'estensione, per esempio un PDF rinominato in .png. Invia il file con la sua estensione reale. I formati testuali come .json e .csv non vengono mai controllati a livello di byte, quindi questo errore compare solo per i tipi binari.

Posso caricare un file di grandi dimensioni in modo asincrono?#

Non sugli endpoint di upload file V1: convertono sempre nella richiesta. Invia un job_id generato dal client e fai polling su GET /v1/convert/status/{job_id} per sopravvivere a una connessione caduta, oppure usa POST /v2/ingest/files, che è asincrono per progettazione e può chiamare un webhook quando il job termina.