URLs de descarga firmadas#
EnConvert no pone tu archivo convertido en el cuerpo de la respuesta por defecto. Sube el archivo al almacenamiento de objetos y devuelve una URL firmada: un enlace HTTPS corriente que lleva su propia autorización en la query string y deja de funcionar 15 minutos después de emitirse.
Por qué la salida es un enlace#
Dos razones, ambas prácticas.
La respuesta se mantiene pequeña. Una respuesta de conversión son unos cientos de bytes de JSON pese lo que pese la salida, así que tu cliente parsea una única forma predecible tanto si el resultado es un archivo Markdown de 4 KB como un ZIP de 140 MB de un sitio entero. También significa que una respuesta de estado de lote puede llevar 400 resultados sin llevar 400 archivos.
Los bytes vienen del almacenamiento, no de la API. Las descargas las sirve directamente la capa de almacenamiento, así que un cliente lento que se descarga un PDF grande no mantiene ocupado un worker de la API ni queda sujeto a los mismos timeouts de proxy que acotan una solicitud de conversión. El enlace no necesita la cabecera X-API-Key, que es lo que hace seguro pasárselo a un navegador, a un consumidor de cola o a un curl dentro de un script de shell.
Qué aspecto tiene la URL#
Es una URL GET estándar de AWS SigV4 en formato path contra el host de almacenamiento:
https://<region>.digitaloceanspaces.com/<bucket>/<object_key>
?X-Amz-Algorithm=AWS4-HMAC-SHA256
&X-Amz-Credential=<key>%2F<date>%2F<region>%2Fs3%2Faws4_request
&X-Amz-Date=<timestamp>
&X-Amz-Expires=900
&X-Amz-SignedHeaders=host
&X-Amz-Signature=<hex>
El host y el bucket dependen del despliegue, así que léelos de la URL que recibiste en lugar de codificarlos a mano. La forma no cambia: un GET simple, sin cabeceras necesarias, X-Amz-Expires=900.
La object key que lleva dentro es determinista y usa un espacio de nombres por proyecto:
{env}/files/{project_id}/{endpoint}/{filename}
live/files/4127/v2-perceive/per_3f9a2c1b8e7d4a6f90b1c2d3e4f5a6b7_markdown.md
La firma está acotada a ese prefijo. A un proyecto solo se le puede entregar una firma sobre sus propias keys, así que una object_key del proyecto de otra persona no puede convertirse en una URL que funcione.
Expiración: 15 minutos, V1 y V2#
Toda URL firmada que emite EnConvert vive 900 segundos. No hay ningún parámetro de solicitud para hacerla más larga ni más corta.
| Dónde aparece | Campo |
|---|---|
| Respuesta de conversión síncrona V1 | presigned_url |
| Estado de lote V1, por elemento | download_url |
| Estado de lote V1, modo ZIP | zip_download_url |
| Consulta de estado de job V1 | presigned_url |
| Perceive V2, por salida | outputs.<name>.url, junto a expires_in: 900 |
| Lote de perceive V2, modo ZIP | zip.url |
| Ingest V2, al completarse | output_url |
Las URLs firmadas son reutilizables, no de un solo uso. La misma URL sigue funcionando para GETs repetidos hasta que pasan los 15 minutos. Nada la invalida antes, y descargarla una vez no la consume.
Si necesitas el archivo durante más de 15 minutos, descarga los bytes y guárdalos tú. Volver a firmar te da un enlace nuevo, no acceso permanente.
Volver a firmar#
Un enlace expirado no es un archivo perdido. Pídeselo otra vez a la API y acuñará una nueva firma sobre el mismo objeto almacenado:
| Job | Vuelve a firmar con |
|---|---|
| Conversión V1 asíncrona o por lotes | GET /v1/convert/batch/{batch_id} |
| Conversión síncrona V1 consultada por tu propio id | GET /v1/convert/status/{job_id} |
| Operación de perceive V2 | GET /v2/perceive/{operation_id} |
| Lote de perceive V2 | GET /v2/perceive/batch/{job_id} |
| Job de ingest V2 | GET /v2/ingest/{job_id} |
Cada uno de esos endpoints reconstruye las URLs a partir de las object keys persistidas en cada llamada. Volver a firmar no renderiza nada, no convierte nada y no factura ops. Funciona mientras el objeto siga en el almacenamiento, que es el otro reloj de esta página.
Un comportamiento que debes contemplar en tu código: si la firma no se puede producir, el campo vuelve como null en lugar de fallar la solicitud. Una consulta de estado nunca devuelve un 500 por una key obsoleta. Así que comprueba si hay null en url, download_url y output_url antes de dereferenciarlos.
La retención es un reloj distinto#
Esta es la distinción que la gente confunde, así que aquí va en una línea cada una:
- La expiración de la firma (15 minutos) decide cuánto tiempo funciona una URL determinada.
- La retención (de horas a días, según el plan) decide cuánto tiempo existe el archivo.
Son independientes, lo que significa que los dos casos confusos son reales:
Una URL expirada no significa que el archivo haya desaparecido. Quince minutos después de una conversión, el enlace está muerto pero el objeto casi seguro sigue ahí. Vuelve a consultar el job y recuperas un enlace que funciona.
Una URL viva no garantiza que el archivo siga ahí. Si vuelves a firmar una salida del plan Founding en el minuto 59 y usas el enlace en el minuto 62, la barrida de retención puede haber borrado el objeto entretanto. La firma es válida; el objeto no. La descarga falla en la capa de almacenamiento, no en la API.
La duración de la retención se fija por plan en tu suscripción, y la ventana del plan Founding es de una hora, lo bastante corta como para toparse con ella por accidente durante el desarrollo. La tabla por plan está en Límites de frecuencia y cuotas.
Dos cosas más sobre la retención que conviene saber:
- La eliminación se programa y luego se barre a intervalos, así que un archivo puede sobrevivir a su ventana unos minutos. No construyas nada sobre eso. Es holgura del barredor, no un periodo de gracia.
- Los proyectos con un add-on de almacenamiento no tienen ninguna eliminación programada. Sus salidas permanecen hasta que se eliminan deliberadamente y cuentan, en su lugar, contra la cuota de almacenamiento del add-on.
Aparte de la ventana de tu plan, las capturas de HTML renderizado que se usan para puntuar la calidad del renderizado se conservan 90 días y pueden desactivarse por solicitud con la cabecera X-Enconvert-No-Capture: true. Los archivos fuente subidos a POST /v2/ingest/files se borran en cuanto se ensambla el JSONL, con un respaldo de 24 horas por si algo falla antes.
direct_download: transmitir los bytes en su lugar#
Si seguir una URL es un salto extra que no quieres, pide los bytes en el cuerpo de la respuesta.
En perceive V2#
direct_download: true en POST /v2/perceive sustituye por completo al sobre JSON. El cuerpo de la respuesta es el artefacto, servido con el propio content type del artefacto.
curl -X POST https://api.enconvert.com/v2/perceive \
-H "X-API-Key: sk_your_private_key" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/pricing",
"outputs": ["markdown"],
"direct_download": true
}' \
--output pricing.md
Necesita exactamente una salida que produzca artefacto: markdown, html_cleaned, html_raw, screenshot, screenshot_full_page, pdf, links o images, todas ellas descritas en la página de perceive. Pide dos y la llamada devuelve 400 enumerando lo que enviaste. structured no cuenta, porque es JSON inline y no un archivo almacenado, así que puede acompañar sin romper la regla.
Los metadatos que habrían estado en el cuerpo JSON se mueven a las cabeceras: X-Operation-Id, X-Object-Key y X-Cache-Hit siempre, más X-Render-Quality, X-Source-Status-Code, X-Content-Hash y X-Warnings-Count cuando esos valores existen. El cuerpo también lleva Content-Disposition: attachment, Content-Length y Cache-Control: no-transform.
Dos endpoints GET aceptan direct_download como parámetro de query:
GET /v2/perceive/{operation_id}?direct_download=true&output=markdowntransmite un artefacto de una operación pasada.outputes obligatorio cuando la operación produjo más de un artefacto, y un nombre desconocido devuelve404.GET /v2/perceive/batch/{job_id}?direct_download=truetransmite el ZIP del lote para lotes conoutput_mode: "zip"cuyo archivo ya esté listo, y responde400en caso contrario.
POST /v2/perceive/batch rechaza direct_download con 422. Define output_mode a "zip" y descarga el archivo en su lugar.
410 Gone con un mensaje que te indica que vuelvas a ejecutar la solicitud, en lugar de devolver bytes vacíos en silencio. El ahorro es un viaje de ida y vuelta, no una escritura en almacenamiento.
En las conversiones V1#
V1 también tiene un campo direct_download, con un comportamiento distinto al de V2. En los endpoints de URL decide si la API devuelve los bytes del archivo en bruto o un cuerpo JSON con una URL de descarga prefirmada. En los endpoints de subida de archivos se acepta por paridad con la forma de la solicitud, pero no tiene efecto: esos endpoints siempre responden con el cuerpo JSON.
| Tipo de endpoint | Tipo de clave | Por defecto | Qué recibes |
|---|---|---|---|
| Endpoints de subida de archivos | Todas las claves | true |
JSON con presigned_url, pongas lo que pongas. Aquí el campo es inerte. |
| Endpoints de URL | Clave privada | false |
JSON con presigned_url. Pon true para recibir bytes en bruto. |
| Endpoints de URL | Clave pública / de dashboard | true (forzado) |
JSON con presigned_url |
direct_download se fuerza a true, pero la respuesta es un objeto JSON con una presigned_url (no bytes en bruto). Esto evita problemas de timeout del proxy inverso con archivos grandes que pueden tardar de 60 a 120 segundos en convertirse.
En un endpoint de URL, una clave privada con direct_download=true devuelve los bytes del archivo en bruto:
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", "direct_download": true}' \
--output output.pdf
Restricciones:
direct_downloadno se puede usar conasync_mode: true(devuelve400)direct_downloadno se puede usar con varias URLs (devuelve400)
Ambas restricciones se derivan del mismo hecho: no queda respuesta donde meter los bytes una vez que la llamada ha contestado 202. Los resultados asíncronos se recogen mediante consultas periódicas o un webhook. Consulta Trabajos síncronos y asíncronos.
Cabeceras de respuesta#
Las conversiones de archivos subidos, y las conversiones de URL hechas con una clave pública o de dashboard, repiten los metadatos de la conversión en cabeceras además de en el cuerpo JSON:
| Cabecera | Descripción |
|---|---|
X-Object-Key |
Ruta de almacenamiento del archivo convertido |
X-File-Size |
Tamaño del archivo convertido, en bytes |
X-Conversion-Time |
Tiempo empleado en la conversión, en segundos |
X-Filename |
Nombre de archivo generado |
Una respuesta de bytes en bruto (endpoint de URL, clave privada, direct_download: true) lleva esas cuatro más Content-Disposition: attachment; filename="{filename}", Content-Length y Cache-Control: no-transform. Una conversión de URL que devuelve JSON a una clave privada no lleva ninguna, así que lee el cuerpo.
Preguntas frecuentes#
¿Cuánto tiempo siguen siendo válidas las URLs de descarga firmadas de EnConvert?#
Quince minutos, o 900 segundos, tanto en V1 como en V2. El sobre de artefacto de V2 lo indica explícitamente como expires_in: 900. No hay ningún parámetro para extenderlo. Vuelve a obtener el job o la operación para acuñar una URL nueva sobre el mismo archivo.
¿Puedo usar una URL firmada más de una vez?#
Sí. Las URLs firmadas no son de un solo uso. El mismo enlace sirve GETs repetidos hasta que pasan los 15 minutos, y descargarlo no lo invalida.
Mi URL de descarga ha expirado. ¿Se ha borrado el archivo?#
Casi seguro que no. La expiración de la firma y la retención del archivo son relojes separados. Vuelve a consultar el job o la operación (GET /v1/convert/batch/{batch_id}, GET /v2/perceive/{operation_id}, GET /v2/ingest/{job_id}) y recibes un enlace recién firmado sin coste en ops, siempre que el archivo siga dentro de la ventana de retención de tu plan.
¿Cuánto tiempo guarda EnConvert mis archivos convertidos?#
La retención se fija por plan, y el plan Founding conserva las salidas durante una hora. Los planes de pago las conservan más tiempo, y los proyectos con un add-on de almacenamiento no se barren nunca. Las cifras por plan están en Límites de frecuencia y cuotas.
¿Qué significa un 410 Gone en un artefacto de perceive?#
El objeto ha superado la ventana de retención de tu plan y se ha borrado del almacenamiento. direct_download vuelve a leer el artefacto del almacenamiento antes de transmitirlo, así que un artefacto caducado responde 410 en lugar de un cuerpo vacío. Vuelve a ejecutar la solicitud de perceive para regenerarlo.
¿Cómo obtengo los bytes en bruto en lugar de una URL de descarga?#
Define direct_download: true. En POST /v2/perceive requiere exactamente una salida que produzca artefacto y el cuerpo de la respuesta pasa a ser ese artefacto. En los endpoints de URL de V1 con una clave privada devuelve los bytes del archivo, y no se puede combinar con async_mode ni con varias URLs.