---
seo_title: URLs de descarga firmadas: expiración y retención | EnConvert
meta_desc: Cada salida de EnConvert llega como una URL firmada de duración limitada. Qué contiene la URL, cuánto vive y cómo transmitir los bytes directamente.
keywords: expiración url prefirmada, url de descarga firmada api, parámetro direct_download, ventana de retención de archivos, url prefirmada s3 15 minutos, volver a firmar url de descarga, entrega de archivos convertidos api, error 410 artefacto expirado
---

# 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.

<div class="alert alert-warning">
<strong>El enlace es una credencial al portador.</strong> Cualquiera que tenga la URL puede descargar ese archivo hasta que expire. No hay una segunda comprobación de la clave de API. Trata una URL firmada como una contraseña con 15 minutos de vida: no la registres en logs, no la publiques en un gestor de incidencias público y no la pegues en un canal compartido.
</div>

---

## 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](/es/docs/reference/rate-limits.md).

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.

```bash
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](/es/docs/endpoints/perceive.md). 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=markdown` transmite un artefacto de una operación pasada. `output` es obligatorio cuando la operación produjo más de un artefacto, y un nombre desconocido devuelve `404`.
- `GET /v2/perceive/batch/{job_id}?direct_download=true` transmite el ZIP del lote para lotes con `output_mode: "zip"` cuyo archivo ya esté listo, y responde `400` en caso contrario.

`POST /v2/perceive/batch` rechaza `direct_download` con `422`. Define `output_mode` a `"zip"` y descarga el archivo en su lugar.

<div class="alert alert-info">
<strong>direct_download no se salta el almacenamiento.</strong> El artefacto se sube primero, luego se vuelve a leer y se te transmite. Por eso un artefacto que ha superado su ventana de retención responde <code>410 Gone</code> 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.
</div>

### 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` |

<div class="alert alert-info">
<strong>Comportamiento de la clave pública:</strong> Para las claves públicas y de dashboard, <code>direct_download</code> se fuerza a <code>true</code>, pero la respuesta es un objeto JSON con una <code>presigned_url</code> (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.
</div>

En un endpoint de URL, una clave privada con `direct_download=true` devuelve los bytes del archivo en bruto:

```bash
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_download` no se puede usar con `async_mode: true` (devuelve `400`)
- `direct_download` no se puede usar con varias URLs (devuelve `400`)

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](/es/docs/concepts/sync-and-async.md).

### 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](/es/docs/reference/rate-limits.md).

### ¿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.
