---
seo_title: SDK C# y .NET de Conversión de Archivos: NuGet | EnConvert
meta_desc: SDK oficial de EnConvert para C# y .NET 8+. Métodos asíncronos de conversión de archivos más el espacio V2: perceive, discover, distill, ingest y watch.
keywords: sdk de conversión de archivos para c# .net, convertir archivos en c# .net, url a pdf en c# .net, api de web scraping con c# .net, docx a pdf en c# .net, enconvert csharp sdk, cliente nuget de api de conversión, heic a webp en c#, url a markdown en dotnet, api de capturas de sitios web en dotnet, pipeline de ingesta rag en c#
---

# SDK de Conversión de Archivos para C# y .NET

`Enconvert` es el cliente oficial de C# y .NET para la API de EnConvert, publicado en NuGet y dirigido a .NET 8 o posterior. Convierte archivos desde C#: URL a PDF, DOCX a PDF, HEIC a WebP, JSON a YAML, cualquier documento a Markdown y sitios web completos en un único ZIP. El mismo cliente lleva un espacio de nombres `V2` para web scraping e inteligencia web, así que una sola clave cubre perceive, discover, lookup, distill, ingest y watch. Cada llamada es asíncrona y cancelable, cada respuesta es un record tipado, y el paquete no arrastra ninguna dependencia de terceros.

<div class="alert alert-info">
<strong>NuGet:</strong> <code>Enconvert</code> · <strong>Fuente:</strong> <a href="https://github.com/conversionapi/csharp-sdk">conversionapi/csharp-sdk</a> · <strong>Runtime:</strong> .NET 8+ · <strong>Dependencias:</strong> ninguna más allá de la BCL
</div>

---

## Instalación

```bash
dotnet add package Enconvert
```

El ensamblado apunta a `net8.0` con los tipos de referencia anulables habilitados y está construido únicamente sobre `System.Net.Http` y `System.Text.Json`.

## Inicio rápido

```csharp
using Enconvert;

using var client = new EnconvertClient(Environment.GetEnvironmentVariable("ENCONVERT_API_KEY")!);

// Convierte una página en vivo a PDF y transmítela a disco.
var pdf = await client.ConvertUrlToPdfAsync("https://example.com", new UrlToPdfOptions { SaveTo = "page.pdf" });
Console.WriteLine(pdf.PresignedUrl);

// Lee una página como debería hacerlo tu agente, con una puntuación de calidad adjunta.
var op = await client.V2.PerceiveAsync("https://example.com", new PerceiveOptions { Outputs = new[] { "markdown", "structured" } });
Console.WriteLine($"{op.Outputs["markdown"].Url} scored {op.RenderQuality}");
```

`EnconvertClient` implementa `IDisposable`, así que decláralo con `using` o regístralo como singleton. Se autentica con una clave de API privada, lo que significa que su sitio es el servidor: nunca envíes la clave dentro de una build de escritorio, móvil o Blazor WebAssembly. La configuración de claves se trata en [Autenticación](/es/docs/authentication).

---

## Qué expone el cliente

| Miembro | Endpoint | Devuelve |
|---------|----------|----------|
| `ConvertUrlToPdfAsync(url, opts?)` | `POST /v1/convert/url-to-pdf` | `ConversionResult` |
| `ConvertUrlToScreenshotAsync(url, opts?)` | `POST /v1/convert/url-to-screenshot` | `ConversionResult` |
| `ConvertUrlToMarkdownAsync(url, opts?)` | `POST /v1/convert/url-to-markdown` | `ConversionResult` |
| `ConvertImageAsync(file, opts)` | `POST /v1/convert/{from}-to-{to}` | `ConversionResult` |
| `ConvertDocumentAsync(file, opts?)` | `POST /v1/convert/{from}-to-{to}` | `ConversionResult` |
| `ConvertToMarkdownAsync(file, opts?)` | `POST /v1/convert/anything-to-markdown` | `ConversionResult` |
| `ConvertToPdfAsync(file, opts?)` | `POST /v1/convert/anything-to-pdf` | `ConversionResult` |
| `ConvertWebsiteToPdfAsync(url, opts?)` | `POST /v1/convert/website-to-pdf` | `BatchSubmission` |
| `ConvertWebsiteToScreenshotAsync(url, opts?)` | `POST /v1/convert/website-to-screenshot` | `BatchSubmission` |
| `GetJobStatusAsync(jobId)` | `GET /v1/convert/status/{jobId}` | `JobStatus` |
| `GetBatchStatusAsync(batchId)` | `GET /v1/convert/batch/{batchId}` | `BatchStatus` |
| `WaitForBatchAsync(batchId, opts?)` | `GET /v1/convert/batch/{batchId}` (sondeado) | `BatchStatus` |
| `V2` | la superficie `/v2/*` | `EnconvertV2` |

`client.V2` agrupa 23 métodos repartidos en seis capacidades:

| Capacidad | Métodos | Referencia |
|-----------|---------|------------|
| Perceive | `PerceiveAsync`, `GetPerceiveOperationAsync`, `PerceiveBatchAsync`, `GetPerceiveBatchAsync`, `PerceiveDirectAsync`, `DownloadPerceiveArtifactAsync` | [/es/docs/v2-perceive](/es/docs/v2-perceive) |
| Discover | `DiscoverAsync` | [/es/docs/v2-discover](/es/docs/v2-discover) |
| Lookup | `LookupAsync` | [/es/docs/v2-lookup](/es/docs/v2-lookup) |
| Distill | `DistillAsync` | [/es/docs/v2-distill](/es/docs/v2-distill) |
| Ingest | `IngestAsync`, `IngestFilesAsync`, `ListIngestJobsAsync`, `GetIngestJobAsync`, `CancelIngestJobAsync`, `RetryIngestWebhookAsync`, `GetWebhookSecretAsync`, `RotateWebhookSecretAsync` | [/es/docs/v2-ingest](/es/docs/v2-ingest) |
| Watch | `CreateWatcherAsync`, `ListWatchersAsync`, `GetWatcherAsync`, `GetWatcherSnapshotsAsync`, `UpdateWatcherAsync`, `DeleteWatcherAsync` | [/es/docs/v2-watch](/es/docs/v2-watch) |

Cada método admite un `CancellationToken` opcional al final. Las opciones son records con propiedades `init`, así que constrúyelas con un inicializador de objeto y reutilízalas con `with`. Los clientes de otros lenguajes están en el [índice de SDK](/es/docs/sdks); la superficie REST en crudo está en [Visión general de endpoints](/es/docs/endpoints-overview).

---

## Conversión de archivos

### ConvertUrlToPdfAsync

Renderiza a PDF cualquier URL accesible.

```csharp
var result = await client.ConvertUrlToPdfAsync("https://example.com", new UrlToPdfOptions
{
    SinglePage = false,
    ViewportWidth = 1440,
    PdfOptions = new PdfOptions { PageSize = "A4", Orientation = "landscape" },
    SaveTo = "report.pdf",
});

Console.WriteLine($"{result.Filename} ({result.FileSize} bytes)");
```

| Opción | Tipo | Por defecto | Descripción |
|--------|------|-------------|-------------|
| `SaveTo` | `string?` | -- | Ruta local a la que transmitir el PDF. Los directorios padre que falten se crean. |
| `SinglePage` | `bool?` | `true` | `true` genera una única página continua. `false` pagina usando `PdfOptions.PageSize`. |
| `PdfOptions` | `PdfOptions?` | -- | Tamaño de página, orientación, márgenes, escala, escala de grises, encabezado, pie. Ver [Opciones de PDF](#opciones-de-pdf). |
| `ViewportWidth`, `ViewportHeight` | `int?` | `1920`, `1080` | Viewport del navegador en píxeles. |
| `LoadMedia`, `EnableScroll` | `bool?` | `true` | Espera a las imágenes y el vídeo antes de capturar; desplaza de arriba abajo para que se disparen los cargadores diferidos. |
| `OutputFilename` | `string?` | auto | Sustituye el nombre de archivo generado. |
| `Auth` | `HttpBasicAuth?` | -- | Credenciales HTTP Basic para una página tras un inicio de sesión. |
| `Cookies`, `Headers` | `IReadOnlyList<BrowserCookie>?`, `IReadOnlyDictionary<string, string>?` | -- | Hasta 50 cookies inyectadas antes de renderizar, y hasta 20 encabezados de solicitud extra. Los encabezados salto a salto se rechazan. |

<div class="alert alert-warning">
<strong>No combines <code>Auth</code> con tu propio encabezado <code>Authorization</code>.</strong> La API rechaza ese conflicto en lugar de adivinar qué credencial gana. Elige una.
</div>

### ConvertUrlToScreenshotAsync

Captura un PNG de cualquier URL.

```csharp
await client.ConvertUrlToScreenshotAsync("https://example.com", new UrlToScreenshotOptions
{
    ViewportWidth = 1440,
    SaveTo = "screenshot.png",
});
```

`UrlToScreenshotOptions` acepta las mismas opciones de viewport, medios, desplazamiento, nombre de archivo y acceso que `ConvertUrlToPdfAsync`, menos `SinglePage` y `PdfOptions`.

### ConvertUrlToMarkdownAsync

Extrae Markdown limpio con sabor GitHub desde una URL. Se eliminan la navegación, los pies de página, los anuncios y los scripts, se conserva el cuerpo principal del artículo y se antepone un frontmatter YAML (title, description, url, links, images).

```csharp
await client.ConvertUrlToMarkdownAsync("https://example.com/article", new UrlToMarkdownOptions { SaveTo = "article.md" });
```

`UrlToMarkdownOptions` comparte las mismas opciones de renderizado y acceso. Para lecturas orientadas a agentes que además necesiten una puntuación de calidad y extracción estructurada, usa [Perceive](#perceive) en su lugar.

### ConvertImageAsync

Convierte entre `jpeg`, `png`, `svg`, `heic` y `webp` en cualquier dirección, y rasteriza un PDF a JPEG.

```csharp
// Desde una ruta.
await client.ConvertImageAsync("photo.heic", new ConvertImageOptions { OutputFormat = "webp", SaveTo = "photo.webp" });

// Desde bytes, con un nombre de archivo explícito para poder detectar el formato de entrada.
var bytes = await File.ReadAllBytesAsync("scan.pdf");
await client.ConvertImageAsync(new FileInput(bytes, "scan.pdf"), new ConvertImageOptions
{
    OutputFormat = "jpeg",
    SaveTo = "scan.jpeg",
});
```

Tres sobrecargas aceptan una ruta `string`, un `byte[]` en crudo o un record `FileInput`. El formato de entrada procede de la extensión del nombre de archivo, así que prefiere `FileInput` cuando tengas bytes: la sobrecarga de `byte[]` pelado envía `upload.bin`, que solo funciona en los endpoints con detección automática.

| Opción | Tipo | Obligatorio | Descripción |
|--------|------|-------------|-------------|
| `OutputFormat` | `string` | Sí | Formato de destino. `jpg`, `yml`, `htm` y `md` se normalizan a sus nombres canónicos. |
| `SaveTo` | `string?` | -- | Ruta local a la que transmitir el resultado. |
| `OutputFilename` | `string?` | -- | Sustituye el nombre de archivo generado. |

Los pares no admitidos lanzan `ArgumentException` antes de realizar ninguna solicitud HTTP, así que una errata no cuesta nada.

### ConvertDocumentAsync

Convierte documentos y formatos de datos. `OutputFormat` es `"pdf"` por defecto.

```csharp
// docx a pdf
await client.ConvertDocumentAsync("report.docx", new ConvertDocumentOptions { SaveTo = "report.pdf" });

// json a yaml
await client.ConvertDocumentAsync("data.json", new ConvertDocumentOptions { OutputFormat = "yaml", SaveTo = "data.yaml" });

// markdown a pdf con geometría de página
await client.ConvertDocumentAsync("README.md", new ConvertDocumentOptions
{
    PdfOptions = new PdfOptions { PageSize = "A4", Margins = new PdfMargins { Top = 20, Bottom = 20 } },
    SaveTo = "readme.pdf",
});
```

**Entradas admitidas:** `.doc`, `.docx`, `.xls`, `.xlsx`, `.ppt`, `.pptx`, `.html`, `.htm`, `.odt`, `.ods`, `.odp`, `.ots`, `.pages`, `.numbers`, `.md`, `.markdown`, `.csv`, `.json`, `.xml`, `.yaml`, `.yml`, `.toml`. EPUB no tiene un par de documento propio, así que envía `.epub` a través de [`ConvertToPdfAsync`](#converttopdfasync) o [`ConvertToMarkdownAsync`](#converttomarkdownasync).

| Opción | Tipo | Por defecto | Descripción |
|--------|------|-------------|-------------|
| `OutputFormat` | `string?` | `"pdf"` | Formato de destino. |
| `SaveTo` | `string?` | -- | Ruta local a la que transmitir el resultado. |
| `OutputFilename` | `string?` | -- | Sustituye el nombre de archivo generado. |
| `PdfOptions` | `PdfOptions?` | -- | Configuración de página. Solo tiene sentido cuando la salida es PDF. |

Igual que `ConvertImageAsync`, este método tiene sobrecargas de ruta, `byte[]` y `FileInput`.

### ConvertToMarkdownAsync

Envía casi cualquier documento al endpoint de Markdown con detección automática: PDF, DOCX, PPTX, XLSX, CSV, HTML, EPUB, TXT y MD, además de formatos ofimáticos heredados y ODF. Aquí las imágenes no están admitidas.

```csharp
await client.ConvertToMarkdownAsync("handbook.docx", new ConvertToMarkdownOptions { SaveTo = "handbook.md" });
```

El formato se detecta en el servidor, así que no hay comprobación de extensión en el cliente ni opciones de PDF en este endpoint. `ConvertToMarkdownOptions` lleva únicamente `SaveTo` y `OutputFilename`. La salida es un único archivo `.md` consciente de los encabezados, lo que lo convierte en una primera etapa sólida de un pipeline de RAG: un troceador semántico puede dividir por la propia jerarquía de encabezados del documento en lugar de por recuentos arbitrarios de caracteres.

### ConvertToPdfAsync

El otro endpoint con detección automática: formatos ofimáticos, ODF, Pages, Numbers, RTF, CSV, HTML, Markdown, texto plano, imágenes rasterizadas, SVG, EPUB o un PDF existente que se pasa directamente para normalizarlo.

```csharp
// Diapositivas a PDF.
await client.ConvertToPdfAsync("slides.pptx", new ConvertToPdfOptions { SaveTo = "slides.pdf" });

// PDF de paso directo, convertido a escala de grises.
await client.ConvertToPdfAsync("scan.pdf", new ConvertToPdfOptions
{
    PdfOptions = new PdfOptions { Grayscale = true },
    SaveTo = "gray.pdf",
});
```

<div class="alert alert-warning">
<strong>Aquí solo se respeta <code>Grayscale</code>.</strong> El endpoint anything-to-pdf ignora el resto de <code>PdfOptions</code>. Cuando necesites tamaño de página, orientación, márgenes o un encabezado y un pie, enruta el HTML y el Markdown por <code>ConvertDocumentAsync</code>, o renderiza la página con <code>ConvertUrlToPdfAsync</code>.
</div>

`ConvertToPdfOptions` lleva `SaveTo`, `OutputFilename` y `PdfOptions`, y tiene las mismas tres sobrecargas de entrada que los métodos anteriores.

### Lotes de sitios completos

`ConvertWebsiteToPdfAsync` y `ConvertWebsiteToScreenshotAsync` descubren todas las páginas de un sitio, convierten cada una en segundo plano y empaquetan los resultados en un único ZIP. Ambos son únicamente asíncronos y requieren una clave de API privada.

```csharp
var batch = await client.ConvertWebsiteToPdfAsync("https://example.com", new WebsiteToPdfOptions
{
    CrawlMode = "sitemap",
    ExcludePatterns = new[] { "/blog/tag/" },
    NotificationEmail = "ops@example.com",
});
Console.WriteLine($"{batch.BatchId}: {batch.UrlCount} pages via {batch.DiscoveryMethod}");

// Bloquea hasta que el ZIP esté listo, luego guárdalo.
var status = await client.WaitForBatchAsync(batch.BatchId, new WaitForBatchOptions { SaveTo = "site.zip" });
Console.WriteLine($"{status.Completed} of {status.Total} converted, {status.Failed} failed");
```

Para sondear según tu propio calendario, llama a `GetBatchStatusAsync(batchId)` y lee `ZipDownloadUrl` en cuanto `Status` salga de `"processing"`.

| Opción | Tipo | Por defecto | Descripción |
|--------|------|-------------|-------------|
| `CrawlMode` | `string?` | `"auto"` | `"auto"`, `"sitemap"` (solo sitemap.xml) o `"full"` (sitemap más un rastreo en anchura). |
| `IncludePatterns`, `ExcludePatterns` | `IReadOnlyList<string>?` | -- | Rastrea solo, u omite, las URL que coincidan con estos patrones. Modo de rastreo completo. |
| `NotificationEmail`, `CallbackUrl` | `string?` | propietario del proyecto, -- | Dirección a la que se avisa cuando termina el lote, y webhook al que se hace POST al finalizar. |
| `SinglePage`, `PdfOptions` | `bool?`, `PdfOptions?` | -- | Solo para lotes de PDF. Las opciones de viewport, medios, desplazamiento y acceso se comparten con los métodos de URL individual. |

`WaitForBatchOptions` toma `IntervalMs` (por defecto `5_000`), `TimeoutMs` (por defecto `1_800_000`, es decir 30 minutos) y `SaveTo`. Reventar el plazo lanza `ApiException` con estado `504`.

### GetJobStatusAsync

Sondea un único job de conversión por id.

```csharp
var status = await client.GetJobStatusAsync("job_abc123");

if (status.Status == "success") Console.WriteLine(status.PresignedUrl);
else if (status.Status == "failed") Console.Error.WriteLine(status.Error);
```

<div class="alert alert-info">
<strong>Rara vez necesitas esto directamente.</strong> El cliente ya sondea el estado del job por ti cuando una solicitud síncrona muere en el proxy. Ver <a href="#timeout-recovery">Recuperación de timeouts</a>.
</div>

### Pares de conversión admitidos

El SDK refleja el mapa de conversores del gateway e implementa 43 pares `{input}-to-{output}` tipados.

| Entrada | Salidas |
|---------|---------|
| `json` | `csv`, `toml`, `xml`, `yaml` |
| `xml` | `csv`, `json` |
| `yaml` | `json` |
| `csv` | `json`, `xml` |
| `toml` | `json` |
| `markdown` | `html`, `pdf` |
| `html` | `pdf` |
| `doc`, `excel`, `ppt`, `odt`, `ods`, `odp`, `ots`, `pages`, `numbers` | `pdf` |
| `jpeg`, `png`, `svg`, `heic`, `webp` | entre sí, los 20 pares |
| `pdf` | `jpeg` |

La clase estática `Formats` expone la misma tabla, así que puedes validar antes de construir una interfaz o una cola de jobs:

```csharp
Formats.ValidOutputsFor("json");        // ["csv", "toml", "xml", "yaml"]
Formats.ValidOutputsFor("pdf");         // ["jpeg"]
Formats.ImplementedConversions;         // el conjunto completo de los 43 nombres de endpoint
Formats.NormalizeOutputFormat(".JPG");  // "jpeg"
```

Cualquier cosa fuera de esta tabla pasa por `ConvertToPdfAsync` o `ConvertToMarkdownAsync`, que detectan el formato en el servidor. La referencia completa de parámetros vive en [Parámetros y opciones](/es/docs/parameters-options).

---

## Inteligencia web (V2)

Cada lectura V2 lleva `RenderQuality`, una puntuación de 0.0 a 1.0 que indica con qué honestidad se renderizó la página. Un desafío antibots, un muro de cookies o de inicio de sesión, un error HTTP o el armazón vacío de una aplicación de una sola página vuelven con una puntuación baja, un mapa `Deductions` relleno que nombra qué comprobaciones saltaron y una lista `Warnings`, mientras que el contenido se sigue devolviendo. De eso se trata: una mala lectura queda marcada en lugar de entrar en silencio en el contexto de tu agente. La misma puntuación aparece en los resultados de perceive, en los elementos de distill, en los resultados de lookup percibidos automáticamente y en los snapshots de los watchers. Los conceptos se tratan en la [visión general de V2](/es/docs/v2-overview). Los endpoints V2 requieren una clave de API privada; las claves públicas se rechazan.

### Perceive

Renderiza una URL en artefactos listos para agentes.

```csharp
var op = await client.V2.PerceiveAsync("https://example.com", new PerceiveOptions
{
    Outputs = new[] { "markdown", "screenshot", "structured" },
    Extract = new[] { "tables", "metadata" },
    OnlyMainContent = true,
});

Console.WriteLine(op.RenderQuality);            // de 0.0 a 1.0
Console.WriteLine(op.StatusCode);               // estado HTTP del origen
Console.WriteLine(op.Outputs["markdown"].Url);  // firmada durante 15 minutos
Console.WriteLine(op.Structured);               // JsonObject, la forma es tuya

foreach (var (check, penalty) in op.Deductions) Console.WriteLine($"deduction {check}: {penalty}");
```

| Opción | Tipo | Por defecto | Descripción |
|--------|------|-------------|-------------|
| `Outputs` | `IReadOnlyList<string>?` | `["markdown", "structured"]` | Cualquiera de `markdown`, `html_cleaned`, `html_raw`, `screenshot`, `screenshot_full_page`, `pdf`, `links`, `images`, `structured`. |
| `Extract` | `IReadOnlyList<string>?` | -- | Objetivos heurísticos: `tables`, `prices`, `contacts`, `metadata`, `main_content`, `headings`, `structured_data`, `technologies`, `all`. |
| `Schema` | `JsonObject?` | -- | Schema JSON que guía la extracción estructurada del nivel del LLM. |
| `WaitFor`, `WaitTimeoutMs` | `string?`, `int?` | --, `30000` | Un selector CSS (opcionalmente `css:...`) o `js:<expr>` que esperar antes de capturar, acotado entre 0 y 60000 ms. |
| `JsCode` | `string?` | -- | JavaScript ejecutado tras la navegación, máximo 20000 caracteres. |
| `Viewport` | `PerceiveViewport?` | 1920 x 1080 | `Width` de 320 a 3840, `Height` de 240 a 2160. |
| `Headers`, `Cookies`, `Auth` | ver [arriba](#converturltopdfasync) | -- | Encabezados extra, cookies inyectadas, credenciales HTTP Basic. |
| `CacheMode` | `string?` | `"enabled"` | `"enabled"` reutiliza una caché de 1 hora, `"bypass"` la omite, `"refresh"` vuelve a renderizar. |
| `PdfOptions` | `PdfOptions?` | -- | Solo tiene sentido cuando `Outputs` contiene `pdf`. |
| `BlockResources` | `IReadOnlyList<string>?` | -- | Tipos de recurso que omitir: `image`, `media`, `font`, `stylesheet`, `script`. |
| `RespectRobots`, `Mobile` | `bool?` | -- | Respeta robots.txt; emula un dispositivo móvil. |
| `OnlyMainContent` | `bool?` | `true` | Elimina navegación, encabezado, pie y banners de cookies del artefacto markdown y del extracto `main_content`. |
| `DirectDownload` | `bool?` | -- | Transmite los bytes del artefacto en lugar del envoltorio JSON. Es preferible `PerceiveDirectAsync`. |

`ProxyUrl`, `Geolocation` y `ActionChain` existen en `PerceiveOptions` pero todavía no están disponibles en el servidor y actualmente vuelven como `422`.

Vuelve a firmar las URL de artefacto más adelante, agrupa hasta 1000 URL o transmite bytes sin la ida y vuelta de la URL firmada:

```csharp
// Las URL de artefacto se vuelven a firmar en cada obtención de la operación.
var again = await client.V2.GetPerceiveOperationAsync(op.OperationId);

// Lotes: los pequeños se ejecutan en línea, los más grandes devuelven "queued" y los sondeas.
var batch = await client.V2.PerceiveBatchAsync(
    new[] { "https://a.example.com", "https://b.example.com" },
    new PerceiveBatchOptions { Outputs = new[] { "markdown" }, OutputMode = "zip" });

var done = await client.V2.GetPerceiveBatchAsync(batch.JobId);
Console.WriteLine($"{done.Completed}/{done.Total} done, {done.Failed} failed, zip {done.Zip?.Url}");

// Descarga directa: se requiere exactamente una salida que produzca artefacto.
var direct = await client.V2.PerceiveDirectAsync("https://example.com", new PerceiveOptions { Outputs = new[] { "pdf" } });
await File.WriteAllBytesAsync(direct.Filename ?? "page.pdf", direct.Content);

// Vuelve a descargar un artefacto almacenado de una operación anterior.
var artifact = await client.V2.DownloadPerceiveArtifactAsync(op.OperationId, "markdown");
```

`PerceiveDirectAsync` lanza `ArgumentException` localmente a menos que se pida exactamente uno de `markdown`, `html_cleaned`, `html_raw`, `screenshot`, `screenshot_full_page`, `pdf`, `links` o `images`, ya que `structured` es JSON en línea y no un archivo almacenado. Ambos métodos directos devuelven un `PerceiveDirectResult` que lleva `Content`, `ContentType`, `Filename`, `OperationId`, `ObjectKey`, `CacheHit`, `RenderQuality`, `SourceStatusCode`, `ContentHash` y `WarningsCount`. `DownloadPerceiveArtifactAsync` toma el nombre de la salida solo cuando la operación produjo más de un artefacto, y devuelve `410` una vez que el artefacto supera su ventana de retención.

### Discover

Enumera las URL de un sitio sin renderizado en navegador, lo que lo hace mucho más barato que un rastreo.

```csharp
var found = await client.V2.DiscoverAsync("https://example.com", new DiscoverOptions
{
    Mode = "hybrid",
    MaxUrls = 200,
    ExcludePatterns = new[] { "/tag/" },
});

Console.WriteLine($"{found.Total} urls, truncated: {found.Truncated}");
foreach (var url in found.Urls) Console.WriteLine(url);
```

| Opción | Tipo | Por defecto | Descripción |
|--------|------|-------------|-------------|
| `Mode` | `string?` | `"hybrid"` | `"sitemap"`, `"crawl"` o `"hybrid"` (sitemap más rastreo HTTP). |
| `MaxUrls` | `int?` | `100` | De 1 a 1000. |
| `MaxDepth` | `int?` | `2` | Profundidad de rastreo, de 1 a 5. |
| `IncludePatterns`, `ExcludePatterns` | `IReadOnlyList<string>?` | -- | Lista de permitidos y lista de bloqueados con expresiones regulares, máximo 50 entradas cada una. La lista de bloqueados se aplica en segundo lugar. |
| `SameDomainOnly`, `RespectRobots` | `bool?` | `true`, -- | Permanece en el dominio semilla; respeta robots.txt durante el descubrimiento. |

`DiscoverResult` informa de `Total`, `Urls`, `PagesCrawled`, `Truncated`, `RobotsRespected`, un mapa de recuentos por origen en `Sources` y `Warnings`.

### Lookup

Ejecuta una búsqueda web por categorías y, opcionalmente, renderiza los primeros resultados en la misma llamada.

```csharp
var search = await client.V2.LookupAsync("best static site generators", new LookupOptions
{
    Category = "web",
    NumResults = 10,
    TimeFilter = "month",
    PerceiveTop = 3,
});

foreach (var hit in search.Results)
{
    Console.WriteLine($"{hit.Position}. {hit.Title} {hit.Url}");
    if (hit.Perceive is { } page)
        Console.WriteLine($"   quality {page.RenderQuality}, markdown {page.Outputs["markdown"].Url}");
}
```

| Opción | Tipo | Por defecto | Descripción |
|--------|------|-------------|-------------|
| `Category` | `string?` | `"web"` | `"web"`, `"news"`, `"images"`, `"scholar"`, `"patents"`, `"maps"`. |
| `Country`, `Locale` | `string?` | -- | Código de país `gl` de Google (`"us"`, `"in"`) y idioma de interfaz `hl` (`"en"`). |
| `TimeFilter` | `string?` | -- | `"hour"`, `"day"`, `"week"`, `"month"`, `"year"`. |
| `NumResults`, `Page` | `int?` | `10`, `1` | De 1 a 100 resultados; página de 1 a 10. |
| `Location`, `Autocorrect` | `string?`, `bool?` | --, `true` | Ubicación en texto libre como `"Austin, Texas"`; deja que el proveedor corrija erratas evidentes. |
| `PerceiveTop` | `int?` | `0` | De 0 a 10. Percibe automáticamente las N primeras URL de resultado con un renderizado completo en navegador. |

`LookupResult` también lleva `AnswerBox`, `KnowledgeGraph`, `PerceiveOperationIds` y `Warnings`.

### Distill

Extracción estructurada guiada por schema. Cuando proporcionas uno, se ejecuta primero una pasada CSS gratuita, y solo los campos que se le escapan escalan al nivel del LLM.

```csharp
using System.Text.Json.Nodes;

var extraction = await client.V2.DistillAsync(new DistillOptions
{
    Urls = new[] { "https://example.com/pricing" },
    Schema = new JsonObject { ["plans"] = "list of plan names with monthly prices" },
    CssSchema = new CssSchema
    {
        BaseSelector = ".plan-card",
        Fields = new[]
        {
            new CssField { Name = "name", Type = "text", Selector = "h3" },
            new CssField { Name = "price", Type = "text", Selector = ".price" },
        },
    },
});

var first = extraction.Results[0];
Console.WriteLine($"{first.Data} via {first.ExtractionTier}");
Console.WriteLine($"css fields {first.FieldsFromCss}, llm fields {first.FieldsFromLlm}");

// O descubre primero un sitio y luego destila cada página que encuentre.
await client.V2.DistillAsync(new DistillOptions
{
    DiscoverFrom = new DistillDiscoverFrom { Url = "https://example.com", Mode = "sitemap", MaxPages = 10 },
    Schema = new JsonObject { ["title"] = "page title" },
});
```

| Opción | Tipo | Por defecto | Descripción |
|--------|------|-------------|-------------|
| `Schema` | `JsonObject` | obligatorio | Un objeto JSON-Schema o un mapa plano `{field: description}`. El `Data` de la respuesta se ajusta a esta forma. |
| `Urls` | `IReadOnlyList<string>?` | -- | URL explícitas, máximo 50. Exactamente uno de `Urls` o `DiscoverFrom`. |
| `DiscoverFrom` | `DistillDiscoverFrom?` | -- | `Url`, `Mode` (`"sitemap"`, `"crawl"`, `"hybrid"`) y `MaxPages` de 1 a 50, por defecto 10. |
| `CssSchema` | `CssSchema?` | -- | `BaseSelector` más `Fields`, con `Name` y `TargetField` opcionales. |
| `WaitFor`, `WaitTimeoutMs` | `string?`, `int?` | --, `30000` | Condición de espera antes de la extracción. |
| `Headers`, `Cookies`, `RespectRobots` | -- | -- | Las mismas formas que las opciones de renderizado anteriores. |

`CssField.Type` es uno de `text`, `attribute`, `html`, `regex`, `nested`, `list` o `nested_list`, con anidamiento de hasta cinco niveles de profundidad. Pasar `Urls` y `DiscoverFrom` a la vez, o ninguno de los dos, lanza `ArgumentException` antes de que salga ninguna solicitud.

### Ingest

Convierte un sitio entero, o un montón de documentos subidos, en JSONL troceado que un almacén vectorial pueda leer. Ingest siempre es asíncrono.

```csharp
// Desde un sitio.
var job = await client.V2.IngestAsync(new IngestOptions
{
    Mode = "sitemap",
    Url = "https://docs.example.com",
    MaxPages = 100,
    Chunk = new IngestChunkOptions { MaxWords = 512 },
    WebhookUrl = "https://my.app/hooks/enconvert",
});

// Desde archivos subidos: PDF, DOCX, PPTX, XLSX, CSV, HTML, EPUB, TXT y MD, ofimática heredada y ODF.
var fileJob = await client.V2.IngestFilesAsync(
    new[] { new FileInput(await File.ReadAllBytesAsync("handbook.pdf"), "handbook.pdf") },
    new IngestFilesOptions { Chunk = new IngestChunkOptions { MaxWords = 512 } });

// Sondea hasta que el JSONL esté listo.
var status = await client.V2.GetIngestJobAsync(job.JobId);
Console.WriteLine($"{status.Status}: {status.PagesProcessed}/{status.PagesDiscovered} pages, {status.TotalChunks} chunks");
if (status.Status == "completed") Console.WriteLine(status.OutputUrl);

var recent = await client.V2.ListIngestJobsAsync(new V2ListOptions { Limit = 20 });
await client.V2.CancelIngestJobAsync(job.JobId); // idempotente
```

| Opción | Tipo | Por defecto | Descripción |
|--------|------|-------------|-------------|
| `Mode` | `string?` | `"urls"` | `"urls"`, `"sitemap"` o `"crawl"`. |
| `Url` | `string?` | -- | URL semilla. Obligatoria para `sitemap` y `crawl`, rechazada para `urls`. |
| `Urls` | `IReadOnlyList<string>?` | -- | URL explícitas, máximo 1000. Obligatoria para `urls`, rechazada en el resto de casos. |
| `MaxPages`, `MaxDepth`, `SameDomainOnly` | `int?`, `int?`, `bool?` | `50`, `2`, `true` | Límite de descubrimiento de 1 a 1000 para `sitemap` y `crawl`, profundidad de rastreo de 1 a 5, permanecer en el dominio semilla. |
| `IncludePatterns`, `ExcludePatterns` | `IReadOnlyList<string>?` | -- | Lista de permitidos y lista de bloqueados con expresiones regulares. |
| `RespectRobots`, `WaitFor`, `WaitTimeoutMs` | -- | -- | Controles de renderizado por página. |
| `Chunk` | `IngestChunkOptions?` | -- | `MaxWords` de 32 a 4000, por defecto 512. `SentenceOverlap` de 0 a 10, por defecto 1. |
| `WebhookUrl` | `string?` | -- | Webhook de finalización, firmado con HMAC. |

Las reglas de modo se imponen en el cliente, así que un job `urls` que además establece `Url` lanza `ArgumentException` de inmediato. Un job pasa por `queued`, `discovering`, `processing` y luego `completed`, `failed` o `canceled`. La entrega del webhook se puede verificar de extremo a extremo:

```csharp
var secret = await client.V2.GetWebhookSecretAsync();
Console.WriteLine($"{secret.SignatureHeader} using {secret.SignatureScheme}, replay tolerance {secret.ReplayToleranceSeconds}s");

await client.V2.RotateWebhookSecretAsync();  // las firmas antiguas dejan de verificarse
var retry = await client.V2.RetryIngestWebhookAsync(job.JobId);
Console.WriteLine($"delivered: {retry.Delivered} after {retry.Attempts} attempts");
```

### Watch

Vuelve a renderizar una página según un calendario y te avisa cuando cambia.

```csharp
var watcher = await client.V2.CreateWatcherAsync("https://example.com/pricing", new WatchCreateOptions
{
    FrequencyMinutes = 60,
    DiffMode = "auto",
    WebhookUrl = "https://my.app/hooks/changes",
});

var page = await client.V2.ListWatchersAsync(new V2ListOptions { Limit = 20 });
var one = await client.V2.GetWatcherAsync(watcher.WatcherId);
var history = await client.V2.GetWatcherSnapshotsAsync(watcher.WatcherId, new SnapshotListOptions { Limit = 10 });
foreach (var snap in history.Snapshots)
    Console.WriteLine($"{snap.CheckedAt} changed: {snap.HasChanges} similarity: {snap.Similarity}");

await client.V2.UpdateWatcherAsync(watcher.WatcherId, new WatcherUpdate { Status = "paused" });
await client.V2.UpdateWatcherAsync(watcher.WatcherId, new WatcherUpdate { WebhookUrl = "" }); // lo borra
await client.V2.DeleteWatcherAsync(watcher.WatcherId); // borrado lógico, idempotente
```

| Opción | Tipo | Por defecto | Descripción |
|--------|------|-------------|-------------|
| `FrequencyMinutes` | `int?` | `60` | De 60 a 43200. El mínimo de una hora es estricto. |
| `DiffMode` | `string?` | `"auto"` | `"auto"`, `"text"`, `"structured"`, `"tables"`, `"metadata"`. |
| `TrackFields` | `JsonObject?` | -- | Subconjunto de campos o selectores para el motor de diff. |
| `WebhookUrl` | `string?` | -- | Webhook de cambios, firmado con HMAC. Establécelo a `""` en una actualización para borrarlo. |
| `NotifyEmail` | `bool?` | `true` | Envía un correo al propietario del proyecto cuando hay cambios. |

`WatcherUpdate` también acepta `Status` (`"active"` o `"paused"`) y lanza `ArgumentException` si le pasas una actualización sin ningún campo establecido.

<div class="alert alert-warning">
<strong>Los diffs de snapshot contienen contenido de página no confiable.</strong> <code>WatcherSnapshot.Changes</code> es JSON en crudo tomado de la página vigilada. Escápalo antes de renderizarlo en un panel o en un correo electrónico.
</div>

---

## Opciones de PDF

`PdfOptions` es compartido por `ConvertUrlToPdfAsync`, `ConvertDocumentAsync`, `ConvertToPdfAsync` (solo escala de grises) y `PerceiveOptions` cuando `pdf` está entre las salidas.

```csharp
await client.ConvertUrlToPdfAsync("https://internal.example.com/report", new UrlToPdfOptions
{
    PdfOptions = new PdfOptions
    {
        PageSize = "A4",
        Orientation = "landscape",
        Margins = new PdfMargins { Top = 10, Bottom = 10, Left = 15, Right = 15 },
        Scale = 0.9,
        Header = new PdfHeaderFooter { Content = "Quarterly Report", Height = 15 },
        Footer = new PdfHeaderFooter { Content = "Confidential", Height = 12 },
    },
    Auth = new HttpBasicAuth { Username = "user", Password = "pass" },
    SaveTo = "report.pdf",
});
```

| Campo | Tipo | Descripción |
|-------|------|-------------|
| `PageSize` | `string?` | `"A4"`, `"A3"`, `"Letter"`, `"Legal"` y compañía. |
| `PageWidth`, `PageHeight` | `double?` | Dimensiones personalizadas. Establecidas juntas, prevalecen sobre `PageSize`. |
| `Orientation` | `string?` | `"portrait"` o `"landscape"`. |
| `Margins` | `PdfMargins?` | `Top`, `Bottom`, `Left`, `Right`, todos opcionales. |
| `Scale` | `double?` | Escala de renderizado, por ejemplo `0.9` para el 90 por ciento. |
| `Grayscale` | `bool?` | Posprocesa el PDF a escala de grises. |
| `Header`, `Footer` | `PdfHeaderFooter?` | `Content` de hasta 2000 caracteres, más `Height`. |

## Manejo de errores

Todo fallo aflora como una excepción tipada, así que los bloques `catch` se leen de lo más específico a lo menos específico.

```csharp
using Enconvert;

try
{
    await client.V2.PerceiveAsync("https://example.com");
}
catch (AuthenticationException) { Console.Error.WriteLine("Invalid or missing API key."); }
catch (QuotaException) { Console.Error.WriteLine("Request rejected with 402."); }
catch (RateLimitException) { Console.Error.WriteLine("Too many requests, back off and retry."); }
catch (ApiException e) { Console.Error.WriteLine($"API error [{e.StatusCode}]: {e.Message}"); }
```

| Clase | Se lanza en | Código de estado |
|-------|-------------|------------------|
| `AuthenticationException` | Clave inválida, ausente o revocada | `401`, `403` (informado como `401`) |
| `QuotaException` | Se lanza ante un HTTP 402 | `402` |
| `RateLimitException` | Límite de tasa superado | `429` |
| `ApiException` | Cualquier otro 4xx o 5xx | el código real |
| `EnconvertException` | Clase base de todas las anteriores | -- |

La jerarquía va de `EnconvertException` a `ApiException` (que lleva `StatusCode`) y de ahí a las tres clases específicas, así que un único `catch (EnconvertException)` captura todo lo que lanza el SDK. Los mensajes se toman del campo `detail` o `error` del cuerpo de la respuesta cuando está presente. La validación que el SDK hace localmente, como un par de conversión no admitido o una solicitud de distill malformada, lanza `ArgumentException` en su lugar y nunca llega a la red. Los códigos de respuesta están catalogados en [Códigos de error](/es/docs/error-codes).

## Recuperación de timeouts

Los renderizados de URL largos y las conversiones de documentos grandes pueden sobrevivir a un timeout de proxy inverso de 60 a 120 segundos incluso cuando la conversión en sí tiene éxito. El cliente absorbe eso por ti:

1. Antes de cada conversión de un solo archivo o de una sola URL, el SDK genera un id de job y lo envía con la solicitud.
2. Si esa solicitud vuelve con 5xx, el SDK pasa a sondear `GET /v1/convert/status/{jobId}` cada 3 segundos en lugar de fallar.
3. Un job registrado como `success` devuelve el `ConversionResult` normal. Un job registrado como `failed` lanza `ApiException` con estado `500` y el mensaje de error del servidor.
4. El plazo de sondeo es de 5 minutos, tras lo cual el SDK lanza `ApiException(504, "Conversion timed out")`.

Esto cubre `ConvertUrlToPdfAsync`, `ConvertUrlToScreenshotAsync`, `ConvertUrlToMarkdownAsync` y los cuatro métodos de subida de archivos. Los envíos de lotes de sitios web quedan fuera deliberadamente, porque un envío fallido no tiene fila de job que sondear y debe aflorar de inmediato. Cuando una respuesta omite `job_id`, el SDK rellena el id que generó, de modo que `ConversionResult.JobId` siempre se puede usar con `GetJobStatusAsync`.

## Configuración

```csharp
using var client = new EnconvertClient(
    apiKey: Environment.GetEnvironmentVariable("ENCONVERT_API_KEY")!,
    baseUrl: null,       // por defecto https://api.enconvert.com
    timeoutMs: 300_000);
```

| Parámetro | Tipo | Por defecto | Descripción |
|-----------|------|-------------|-------------|
| `apiKey` | `string` | obligatorio | Clave de API privada. Un valor vacío lanza `ArgumentException`. |
| `baseUrl` | `string?` | `https://api.enconvert.com` | Sustituye la URL base de la API. Las barras finales se eliminan. |
| `timeoutMs` | `int` | `300_000` | Timeout de solicitud en milisegundos, 5 minutos por defecto. |

La clave viaja en un encabezado `X-API-Key` en cada solicitud. Las descargas de artefactos van directamente a URL de almacenamiento firmadas mediante un segundo `HttpClient` que no envía clave ni aplica timeout, así que un ZIP grande puede transmitirse todo el tiempo que necesite. Libera el cliente una sola vez al final de tu proceso, o regístralo como singleton en lugar de construir uno por solicitud.

<div class="alert alert-warning">
<strong>Nunca incrustes la clave en el código.</strong> Léela desde una variable de entorno, desde los user secrets o desde tu gestor de secretos. Cualquiera que tenga tu clave privada puede ejecutar conversiones contra tu proyecto.
</div>

## Forma del resultado

Cada método de conversión devuelve un `ConversionResult`:

```csharp
public sealed record ConversionResult
{
    public required string PresignedUrl { get; init; }  // URL de descarga firmada
    public required string ObjectKey { get; init; }     // clave del objeto en almacenamiento
    public required string Filename { get; init; }      // nombre de archivo del servidor
    public int? FileSize { get; init; }                 // bytes
    public double? ConversionTimeSeconds { get; init; }
    public string? JobId { get; init; }                 // usable con GetJobStatusAsync
}
```

El trabajo asíncrono devuelve sus propios records: `JobStatus` (`Status`, `PresignedUrl`, `ObjectKey`, `Error`), `BatchSubmission` (`BatchId`, `Status`, `UrlCount`, `TotalDiscovered`, `DiscoveryMethod`, `OutputFormat`) y `BatchStatus` (recuentos agregados, `OutputMode`, `ZipDownloadUrl` y los `Items` por URL).

Los artefactos V2 llegan como valores `V2OutputArtifact` indexados por nombre de salida, cada uno con `Url` (firmada durante 15 minutos), `ObjectKey`, `SizeBytes`, `ContentType` y `ExpiresIn` (900 segundos por defecto). Las URL firmadas caducan, así que para acceso permanente descarga los bytes (pasa `SaveTo`, usa `PerceiveDirectAsync` o descarga la URL tú mismo) y guárdalos en tu propio bucket. Llamar de nuevo a `GetPerceiveOperationAsync` vuelve a firmar las URL de artefacto de una operación que siga dentro de su ventana de retención.

## Código fuente e incidencias

- **NuGet:** `Enconvert`
- **GitHub:** [conversionapi/csharp-sdk](https://github.com/conversionapi/csharp-sdk)
- **Licencia:** MIT
- **Otros lenguajes:** [índice de SDK](/es/docs/sdks) · **Claves de API:** [panel de control](/es/dashboard) · [precios](/es/pricing)

---

## Preguntas frecuentes

### ¿Cómo convierto archivos en C# con un paquete de NuGet?

Ejecuta `dotnet add package Enconvert`, crea un cliente con tu clave (`new EnconvertClient(apiKey)`) y llama a un método asíncrono como `ConvertUrlToPdfAsync`, `ConvertImageAsync` o `ConvertDocumentAsync`. Pasa `SaveTo` en el record de opciones para transmitir la salida directamente a una ruta local, o lee `result.PresignedUrl` para descargarla tú mismo.

### ¿Cómo convierto una URL a PDF en .NET?

Llama a `await client.ConvertUrlToPdfAsync("https://example.com", new UrlToPdfOptions { SaveTo = "page.pdf" })`. Establece `SinglePage = false` para paginar, y pasa un record `PdfOptions` para el tamaño de página, la orientación, los márgenes, la escala, la escala de grises, el encabezado y el pie. El viewport es de 1920 x 1080 por defecto.

### ¿Cómo convierto DOCX a PDF en C#?

Llama a `ConvertDocumentAsync("report.docx", new ConvertDocumentOptions { SaveTo = "report.pdf" })`. PDF es el formato de salida por defecto, así que `OutputFormat` es opcional aquí. El mismo método gestiona entradas XLSX, PPTX, ODF, Pages, Numbers, HTML, Markdown, CSV, JSON, XML, YAML y TOML.

### ¿Cómo extraigo el contenido de una página web con el SDK de C#?

Usa el espacio de nombres V2: `await client.V2.PerceiveAsync(url, new PerceiveOptions { Outputs = new[] { "markdown", "structured" } })`. Obtienes Markdown, HTML limpio o en crudo, capturas de pantalla, PDF, enlaces, imágenes y extracción estructurada, cada uno como artefacto firmado, más una puntuación `RenderQuality` para la lectura. Para enumerar primero las URL de un sitio sin renderizar nada, llama a `DiscoverAsync`.

### ¿Qué significa la calidad de renderizado y por qué está en cada lectura?

`RenderQuality` es una puntuación de honestidad de 0.0 a 1.0 adjunta a cada lectura V2. Una puntuación baja significa que la página no se renderizó limpiamente: un desafío antibots, un muro de cookies o de inicio de sesión, un error HTTP o el armazón vacío de una aplicación de una sola página. El contenido sigue llegando, junto con un mapa `Deductions` que nombra qué comprobaciones saltaron y una lista `Warnings`, para que tu agente pueda rechazar una mala lectura en lugar de tratarla como un hecho.

### ¿Gestiona el SDK las conversiones que superan el timeout del proxy?

Sí. Envía un id de job generado con cada conversión de un solo archivo o de una sola URL y, si la solicitud devuelve 5xx, sondea `GET /v1/convert/status/{jobId}` cada 3 segundos durante hasta 5 minutos. El éxito devuelve el resultado normal, un fallo registrado lanza `ApiException`, y agotar el plazo lanza `ApiException(504, "Conversion timed out")`. Los envíos de lotes de sitios web quedan excluidos a propósito.

### ¿Puedo usar este SDK desde Blazor WebAssembly o desde una aplicación móvil?

No. El cliente se autentica con una clave de API privada que nunca debe viajar en código que un usuario pueda leer. Ejecútalo desde ASP.NET Core, un worker service, una Azure Function o cualquier otro host .NET 8 del lado del servidor, y haz que tu front end llame a tu propio endpoint en su lugar.

### ¿Qué conversiones de imagen están admitidas?

Todos los pares entre `jpeg`, `png`, `svg`, `heic` y `webp`, que son 20 combinaciones, más la rasterización de `pdf` a `jpeg`. El formato de entrada procede de la extensión del nombre de archivo, y los pares no admitidos lanzan `ArgumentException` antes de cualquier llamada de red. Consulta la tabla tú mismo con `Formats.ValidOutputsFor("heic")`.

### ¿Cómo convierto un sitio de documentación en fragmentos listos para RAG?

Llama a `client.V2.IngestAsync(new IngestOptions { Mode = "sitemap", Url = "https://docs.example.com", MaxPages = 100 })`, luego sondea `GetIngestJobAsync` hasta que `Status` sea `"completed"` y lee `OutputUrl` para obtener el JSONL. Para documentos locales, `IngestFilesAsync` pasa las subidas por el mismo troceador. Ajusta `Chunk.MaxWords` y `Chunk.SentenceOverlap` para que encajen con tu modelo de embeddings.

### ¿Cuánto tiempo son válidas las URL de descarga?

Las URL de artefacto de V2 se firman durante 15 minutos (`ExpiresIn` es de 900 segundos) y se vuelven a firmar cada vez que obtienes la operación con `GetPerceiveOperationAsync`. Los resultados de conversión también devuelven una URL prefirmada. En ambos casos, si necesitas que el archivo sobreviva a la firma, descárgalo y guárdalo en tu propio bucket.
