Ingesta de Archivos#

Hay dos formas de enviar bytes a EnConvert: subir el archivo tú mismo como multipart/form-data, o pasar una url y dejar que la API descargue el recurso. Cuál puedes usar depende del endpoint, no de tu plan.


Qué acepta cada endpoint#

Familia de endpoints Cómo llegan los bytes Nombre del campo
Formatos de datos, documentos, imágenes Subida multipart/form-data, un archivo por solicitud file
Páginas web (url-to-pdf, url-to-screenshot, url-to-markdown, website-to-pdf, website-to-screenshot) Cuerpo JSON, EnConvert descarga la página url
POST /v2/ingest/files multipart/form-data, muchos archivos por trabajo files
POST /v2/perceive, POST /v2/ingest Cuerpo JSON, EnConvert descarga la página url

No hay una tercera vía. Los endpoints de subida de archivos no descargan una URL por ti, incluidas las rutas comodín anything-to-pdf y anything-to-markdown. Para convertir una página en vivo a PDF, llama a url-to-pdf en su lugar. Para saber qué formatos acepta cada endpoint, consulta formatos compatibles.


Subir un archivo local#

El campo del formulario se llama file, y cada endpoint V1 de subida de archivos acepta exactamente uno. Todo lo demás en el formulario es opcional.

Campo del formulario Tipo Descripción
file file El archivo a convertir. Su extensión debe estar aceptada por el endpoint.
output_filename string Nombre base personalizado para la salida. La extensión de destino se añade automáticamente.
job_id string ID de trabajo proporcionado por el cliente para recuperarse de un timeout. Consulta GET /v1/convert/status/{job_id} si se corta la conexión.
pdf_options string Cadena JSON con opciones de PDF, en los endpoints que producen un PDF.
direct_download boolean Se acepta por paridad con la forma de solicitud de los endpoints de URL. Aquí no tiene efecto: una subida siempre responde con el envoltorio JSON de abajo, envíes lo que envíes.

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 respuesta es JSON con un enlace de descarga prefirmado:

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

Los mismos valores se reflejan en los encabezados de respuesta X-Object-Key, X-File-Size, X-Conversion-Time y X-Filename, así que puedes leerlos sin parsear el cuerpo. Descarga el archivo enseguida: el enlace es de corta duración, y URLs firmadas explica exactamente cuánto.

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));

Varios archivos en una sola llamada#

POST /v2/ingest/files es el único endpoint que acepta más de un archivo. Repite el campo files, hasta 200 archivos por trabajo:

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"

Cada archivo se convierte a Markdown, se fragmenta y se ensambla en un único entregable JSONL. El trabajo siempre es asíncrono y responde 202 Accepted con un job_id. Los detalles completos están en la página del endpoint de ingesta.


Dejar que EnConvert descargue el archivo#

En los endpoints de URL envías un cuerpo JSON en lugar de un formulario, y la API se encarga de la descarga:

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 acepta una cadena o un array de cadenas. Un array cambia la solicitud a asíncrona y se explica en procesamiento por lotes.

Fuentes detrás de un inicio de sesión#

Tres campos opcionales permiten que la descarga lleve credenciales. Los tres requieren un plan con acceso a autenticación básica (Indie o superior).

Parámetro Tipo Predeterminado Descripción
auth object null Credenciales de HTTP Basic Auth: {"username": "...", "password": "..."}.
cookies array null Array de objetos cookie inyectados antes de la navegación. Máximo 50 por solicitud. Cada uno requiere name, value y, o bien domain, o bien url.
headers object null Encabezados HTTP personalizados enviados con las solicitudes. Máximo 20 por solicitud. No puede incluir encabezados bloqueados: host, content-length, transfer-encoding, connection, upgrade, te, trailer.
Alcance de las credenciales: Las credenciales del objeto auth, y un encabezado Authorization (por ejemplo, un token Bearer) pasado vía headers, se envían solo al origen de destino, nunca a los subrecursos de terceros que solicita la página. Esto evita fugas de credenciales hacia hosts de publicidad, analítica o CDN.

Direcciones privadas e internas#

La url debe ser una dirección pública http:// o https://. Antes de descargar nada, se analiza y se rechaza con 400 Bad Request cuando:

  • usa un esquema distinto de http/https;
  • incrusta credenciales como https://user:pass@host/ (usa el campo auth en su lugar);
  • apunta a localhost, a un hostname de metadatos de nube o a una IP que resuelve en un rango privado, de loopback, link-local, reservado o no público de cualquier otro tipo;
  • usa una notación de IP no estándar (octal, hexadecimal o entero empaquetado) que podría resolverse de forma ambigua.

Esto se aplica a la URL semilla, a cada URL de un lote y a las páginas descubiertas por los endpoints de rastreo website-to-*.

Dicho claramente: EnConvert se ejecuta fuera de tu red. No puede alcanzar http://10.0.0.5/report.docx, un hostname .internal ni nada que solo resuelva dentro de tu VPC. O haces que el archivo sea accesible desde la internet pública, o lees los bytes tú mismo y los subes.


Qué le pasa a tu nombre de archivo#

El nombre que envías cumple dos funciones.

Elige el conversor. La extensión decide qué ruta de entrada se ejecuta, así que nombra bien el archivo. Un archivo llamado report sin extensión es rechazado por cualquier endpoint que tenga una lista blanca de extensiones.

Sirve de base para el nombre de salida. El nombre del archivo de salida se construye así:

{base}_{YYYYMMDD_HHMMSSmmm}.{ext}

La marca de tiempo UTC siempre se añade, así que dos conversiones del mismo archivo nunca colisionan. base se resuelve en este orden:

  1. output_filename, si enviaste uno. Si incluiste en él la extensión de destino, esa extensión se elimina primero para que no acabes con report.pdf_20260405_123456789.pdf.
  2. El nombre del archivo subido sin su extensión. report.docx produce report_20260405_123456789.pdf.
  3. Para conversiones de URL, el dominio. https://example.com/page produce example_20260405_123456789.pdf.
  4. Si nada de eso aplica, el literal output.

La clave de almacenamiento se sanea antes de escribir el resultado: solo sobrevive el nombre base, se elimina .., se quitan los caracteres <>:"|?* y los espacios pasan a ser guiones bajos. Sube My Report (final).docx y el PDF acaba en My_Report_(final)_20260405_123456789.pdf. Esa ruta se te devuelve como object_key y tiene la forma {env}/files/{project_id}/{endpoint}/{filename}.

Los nombres de archivo enviados a POST /v2/ingest/files están además limitados a 255 caracteres.


Límite de tamaño y el 413#

El límite de subida es por plan y se aplica a cada archivo individual.

Plan Tamaño máximo de subida Bytes
Founding 5 MB 5242880
Indie 15 MB 15728640
Studio 50 MB 52428800
Production 150 MB 157286400
Enterprise Negociado Según contrato

El tamaño se mide desde la propia parte subida a medida que el cuerpo llega en streaming, no desde un encabezado que tú controlas, así que una subida chunked sin Content-Length se comprueba igual. Pasarse devuelve 413 Payload Too Large antes de que se haga ningún trabajo de conversión y antes de que se facture ninguna op:

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

file_size y max_size están ambos en bytes. tier es el slug del plan (free, starter, pro, business, enterprise), no el nombre visible que ves en la página de precios, así que un proyecto Studio informa "tier": "pro". key_type es private, public o dashboard, con unknown como valor de reserva.

5 MB se agotan rápido. En el plan Founding, un PDF escaneado de 40 páginas o una presentación con unas cuantas fotos a sangre completa ya suele pasarse de la raya. No hay ruta de subida fragmentada ni reanudable: la solución es un archivo más pequeño o un plan más grande.

Superar la barrera del tamaño no es el último obstáculo. Una asignación mensual agotada responde 402 Payment Required y demasiadas solicitudes en una ventana responden 429, ambos descritos en límites de frecuencia y cuotas.


Tipo de contenido y magic bytes#

Las subidas pasan por dos comprobaciones, en este orden.

1. La lista blanca de extensiones. Cada endpoint declara qué extensiones acepta. Una discrepancia devuelve 400 Bad Request:

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

2. El sniffing de magic bytes. Los primeros bytes del archivo se comparan con el grupo que declara su extensión. Una discrepancia de alta confianza también devuelve 400:

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

Eso es lo que obtienes cuando renombras un PDF a .png y lo subes: los bytes empiezan con %PDF-, la extensión dice PNG, y ambos se contradicen. La comprobación existe porque la extensión es lo que enruta tu solicitud. Sin ella, los bytes de PDF llegan a un decodificador de imágenes y obtienes un fallo opaco en lo más profundo del conversor en lugar de un 400 claro en la puerta, y un archivo mal etiquetado a propósito acaba en manos de un parser que nunca debió verlo.

Dos cosas que esto no hace, y conviene saberlas:

  • El Content-Type de la parte nunca se inspecciona. No hay ninguna lista blanca de MIME en toda la ruta de subida, así que application/octet-stream vale. La extensión del nombre de archivo es lo único que enruta la solicitud, y por eso enviar un archivo sin extensión falla.
  • Los formatos de texto no tienen una firma fiable y se saltan el sniffing por completo: .json, .csv, .xml, .yaml, .toml, .md, .html, .svg, .txt. Un archivo .json que en realidad contiene CSV se acepta en esta etapa y falla después, en el parser.

Las firmas binarias reconocidas son PNG, JPEG, GIF, WebP, HEIC/HEIF, PDF y el grupo ofimático (un contenedor ZIP para .docx/.xlsx/.pptx/ODF/EPUB, o el OLE2 heredado para .doc/.xls/.ppt). El sniffing es deliberadamente fail-open: los bytes que no reconoce se dejan pasar en lugar de bloquearse.


Archivos grandes: la lista de comprobación#

  1. Comprueba el límite primero. Un 413 le sale barato a la API y caro a ti, porque subiste el cuerpo entero para ganártelo.
  2. Cuenta con que la subida es síncrona. async_mode solo existe en url-to-pdf, url-to-screenshot y url-to-markdown. Los endpoints de subida de archivos lo ignoran y siempre convierten dentro de la solicitud. Consulta trabajos síncronos y asíncronos.
  3. Envía un job_id generado por ti. Si un proxy delante de ti corta la conexión antes de que termine la conversión, el trabajo se completa igualmente. Consulta GET /v1/convert/status/{job_id} y obtendrás {"status": "processing"}, y luego {"status": "success", "presigned_url": ..., "object_key": ...} o {"status": "failed", "error": ...}. Reutilizar tu propio ID reinicia ese trabajo; reutilizar el ID de otro proyecto devuelve 409.
  4. Ten en cuenta los timeouts. Una solicitud está limitada a 300 segundos de extremo a extremo, tras los cuales obtienes 504 con {"error": "Request timeout"}. Las conversiones ofimáticas basadas en LibreOffice tienen su propio tope de 120 segundos, que también se expone como un 504.
  5. Gestiona el 503 con Retry-After: 10. Las conversiones de archivos se ejecutan detrás de una barrera de admisión con una cola de pendientes acotada. Cuando la cola está llena, la solicitud se rechaza de inmediato en lugar de quedarse esperando, así que reintenta pasado el intervalo indicado en el encabezado.
  6. Para muchos documentos, cambia de endpoint. POST /v2/ingest/files acepta hasta 200 archivos, devuelve 202 de inmediato con un job_id, y admite un webhook_url para que nunca tengas que hacer sondeo. Consulta webhooks.
  7. Descarga enseguida. Los enlaces de salida están firmados y caducan. Si el tuyo expiró, vuelve a leer el endpoint de estado: cada consulta genera un enlace nuevo sobre el mismo objeto almacenado.

Preguntas frecuentes#

¿Qué nombre de campo espera la API de EnConvert para subir un archivo?#

file, enviado como multipart/form-data, un archivo por solicitud, en todos los endpoints de conversión V1 que aceptan una subida. La excepción es POST /v2/ingest/files, que usa files y acepta hasta 200 por trabajo.

¿Puede EnConvert descargar el archivo desde una URL en lugar de subirlo yo?#

Solo en los endpoints de URL (url-to-pdf, url-to-screenshot, url-to-markdown, website-to-pdf, website-to-screenshot) y en los endpoints V2 /v2/perceive y /v2/ingest. Los endpoints de conversión por subida de archivos no tienen parámetro url. Cualquier URL que pases debe ser accesible públicamente: las direcciones privadas, de loopback, link-local y de metadatos de nube se rechazan con 400.

¿Por qué mi subida devolvió 413 Payload Too Large?#

El archivo superaba el límite por archivo de tu plan, que es de 5 MB en Founding, 15 MB en Indie, 50 MB en Studio y 150 MB en Production. El cuerpo de la respuesta lleva un objeto detail con error, file_size, max_size, tier y key_type para que puedas mostrar al llamante las cifras exactas.

¿Por qué mi subida de PNG falla con «File content does not match»?#

Los primeros bytes del archivo pertenecen a un formato distinto del que declara su extensión, por ejemplo un PDF renombrado a .png. Envía el archivo con su extensión real. Los formatos de texto como .json y .csv nunca se comprueban a nivel de bytes, así que este error solo aparece con tipos binarios.

¿Puedo subir un archivo grande de forma asíncrona?#

En los endpoints V1 de subida de archivos no: siempre convierten dentro de la solicitud. Envía un job_id generado por el cliente y consulta GET /v1/convert/status/{job_id} para sobrevivir a una conexión caída, o usa POST /v2/ingest/files, que es asíncrono por diseño y puede llamar a un webhook cuando el trabajo termina.