---
seo_title: SDK PHP de Conversión de Archivos: Cliente Composer | EnConvert
meta_desc: SDK oficial de EnConvert para PHP 8.1+. Instálalo con Composer para convertir documentos e imágenes y para perceive, discover, distill, ingest y watch.
keywords: sdk de conversión de archivos para php, convertir archivos en php, url a pdf en php, api de web scraping con php, docx a pdf en php, enconvert php sdk, paquete composer de conversión de archivos, heic a webp en php, html a pdf php composer, api url a markdown en php, api de conversión de documentos para laravel
---

# SDK de Conversión de Archivos para PHP

`enconvert/enconvert-php` es el cliente oficial de PHP para la API de EnConvert. Instálalo con Composer, entrégale una clave de API y obtienes dos cosas: doce métodos de conversión que convierten URL, imágenes y documentos en PDF, PNG, Markdown y formatos de datos, y un espacio de nombres `$client->v2` que lee la web en vivo y la transforma en Markdown, capturas de pantalla y JSON estructurado listos para agentes. Está dirigido a PHP 8.1+, se construye sobre Guzzle 7, usa arrays de opciones en camelCase de principio a fin y devuelve objetos de resultado tipados de solo lectura en lugar de arrays sueltos. Las conversiones largas que superan un timeout HTTP se recuperan automáticamente mediante sondeo del job.

<div class="alert alert-info">
<strong>Composer:</strong> <code>enconvert/enconvert-php</code> · <strong>Fuente:</strong> <a href="https://github.com/conversionapi/php-sdk">conversionapi/php-sdk</a> · <strong>PHP:</strong> 8.1+ · <strong>Requiere:</strong> <code>guzzlehttp/guzzle ^7.8</code>
</div>

---

## Instalación

```bash
composer require enconvert/enconvert-php
```

El paquete se autocarga bajo el espacio de nombres PSR-4 `Enconvert\` y no incluye CLI, ni archivo de configuración, ni service provider. Funciona sin cambios en PHP puro, Laravel, Symfony, WordPress y cualquier proyecto PSR-4.

---

## Inicio rápido

```php
<?php

require __DIR__ . '/vendor/autoload.php';

use Enconvert\Client;

$client = new Client(getenv('ENCONVERT_API_KEY'));

// Convierte una página en vivo a PDF y transmítela directamente a disco.
$result = $client->convertUrlToPdf('https://example.com', [
    'saveTo' => 'page.pdf',
]);

echo $result->presignedUrl, ' ', $result->fileSize, ' bytes', PHP_EOL;

// Leer una página para un agente usa el mismo cliente y la misma clave.
$op = $client->v2->perceive('https://example.com', ['outputs' => ['markdown', 'structured']]);

echo $op->renderQuality, PHP_EOL;              // de 0.0 a 1.0, p. ej. 0.93
echo $op->outputs['markdown']->url, PHP_EOL;   // URL de descarga firmada
```

El SDK es solo del lado del servidor. Una clave privada (`sk_live_...`) nunca debe llegar a un navegador ni a una aplicación móvil.

---

## Qué expone el cliente

| Superficie | Cómo se accede | Qué hace |
|------------|----------------|----------|
| Conversión de archivos y URL | `$client->convert*()` | Doce métodos sobre `POST /v1/convert/*`, más sondeo de jobs y lotes. |
| Inteligencia web (V2) | `$client->v2` (o `$client->v2()`) | Veintitrés métodos sobre `/v2/*`: perceive, discover, lookup, distill, ingest, watch. |
| Introspección de formatos | `Enconvert\Formats` | Los 43 pares `{input}-to-{output}` implementados, búsqueda de MIME y listas de salidas por entrada. |
| Errores | `Enconvert\Exception\*` | La base `EnconvertException` más `ApiException`, `AuthenticationException`, `QuotaException`, `RateLimitException`. |
| Resultados | `Enconvert\Model\*` | Objetos de valor de solo lectura con propiedades en camelCase. |

`$client->v2` es una propiedad pública de solo lectura. `$client->v2()` es un accesor idéntico para quien prefiera la sintaxis de método. Todos los endpoints V2 requieren una clave de API privada; las claves públicas se rechazan.

---

## Conversión de archivos

Cada método de un solo archivo devuelve un `ConversionResult` que lleva una URL de descarga prefirmada. Si pasas `saveTo`, el SDK además transmite los bytes a esa ruta local, creando los directorios padre que hagan falta.

### convertUrlToPdf

Renderiza a PDF cualquier URL accesible.

```php
$result = $client->convertUrlToPdf('https://example.com', [
    'singlePage' => false,
    'pdfOptions' => ['pageSize' => 'A4', 'orientation' => 'landscape'],
    'viewportWidth' => 1440,
    'saveTo' => 'report.pdf',
]);
```

| Opción | Tipo | Por defecto | Descripción |
|--------|------|-------------|-------------|
| `saveTo` | `string` | ninguno | Ruta local a la que transmitir el PDF. Los directorios padre se crean. |
| `singlePage` | `bool` | `true` | `true` genera una única página continua. `false` pagina usando `pdfOptions.pageSize`. |
| `pdfOptions` | `array` | ninguno | 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` | Tamaño del viewport del navegador en píxeles. |
| `loadMedia` | `bool` | `true` | Espera a las imágenes y el vídeo antes de capturar. |
| `enableScroll` | `bool` | `true` | Desplaza de arriba abajo para que se disparen los cargadores diferidos. |
| `outputFilename` | `string` | auto | Sustituye el nombre de archivo generado. |
| `auth`, `cookies`, `headers` | `array` | ninguno | Acceso a la página: Basic Auth `['username' => ..., 'password' => ...]`, cookies inyectadas, encabezados de solicitud personalizados. |

<div class="alert alert-warning">
<strong>No combines <code>auth</code> con un encabezado <code>Authorization</code>.</strong> La API rechaza el conflicto en lugar de adivinar qué credencial querías usar.
</div>

### convertUrlToScreenshot

Captura un PNG de cualquier URL.

```php
$shot = $client->convertUrlToScreenshot('https://example.com', [
    'viewportWidth' => 1440,
    'saveTo' => 'shot.png',
]);
```

Acepta las mismas opciones de viewport, medios, desplazamiento, nombre de archivo y acceso a la página que `convertUrlToPdf`, menos `singlePage` y `pdfOptions`.

### convertUrlToMarkdown

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

```php
$md = $client->convertUrlToMarkdown('https://example.com/article', [
    'saveTo' => 'article.md',
]);
```

El mismo conjunto de opciones que `convertUrlToScreenshot`.

### convertImage

Convierte entre `jpeg`, `png`, `svg`, `heic` y `webp`, o rasteriza un PDF a JPEG.

```php
// Desde una ruta.
$client->convertImage('photo.heic', ['outputFormat' => 'webp', 'saveTo' => 'photo.webp']);

// Desde bytes en crudo. El nombre de archivo resuelve el formato de entrada y el tipo MIME.
$client->convertImage(
    ['data' => file_get_contents('photo.heic'), 'filename' => 'photo.heic'],
    ['outputFormat' => 'webp', 'saveTo' => 'photo.webp']
);

$client->convertImage('scan.pdf', ['outputFormat' => 'jpeg', 'saveTo' => 'scan.jpeg']); // rasteriza
```

El formato de entrada procede de la extensión del archivo: `.jpg`, `.jpeg`, `.png`, `.svg`, `.heic`, `.webp` y `.pdf`. El formato de salida es obligatorio y se normaliza por ti, así que `jpg` se resuelve como `jpeg`.

| Opción | Tipo | Obligatorio | Descripción |
|--------|------|-------------|-------------|
| `outputFormat` | `string` | Sí | Uno de `jpeg`, `png`, `svg`, `heic`, `webp`. |
| `saveTo` | `string` | No | Ruta local a la que transmitir el resultado. |
| `outputFilename` | `string` | No | Sustituye el nombre de archivo generado. |

### convertDocument

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

```php
// docx a pdf, luego json a yaml
$client->convertDocument('report.docx', ['saveTo' => 'report.pdf']);
$client->convertDocument('data.json', ['outputFormat' => 'yaml', 'saveTo' => 'data.yaml']);

// markdown a pdf con configuración de página
$client->convertDocument('README.md', [
    'outputFormat' => 'pdf',
    'pdfOptions' => ['pageSize' => 'A4', 'margins' => ['top' => 20, 'bottom' => 20]],
    'saveTo' => 'readme.pdf',
]);
```

**Extensiones de entrada reconocidas:** `.doc`, `.docx`, `.xls`, `.xlsx`, `.ppt`, `.pptx`, `.html`, `.htm`, `.odt`, `.ods`, `.odp`, `.ots`, `.pages`, `.numbers`, `.md`, `.markdown`, `.csv`, `.json`, `.xml`, `.yaml`, `.yml`, `.toml`.

| Opción | Tipo | Por defecto | Descripción |
|--------|------|-------------|-------------|
| `outputFormat` | `string` | `"pdf"` | Formato de destino. Los alias `jpg`, `yml`, `htm` y `md` se resuelven. |
| `saveTo` | `string` | ninguno | Ruta local a la que transmitir el resultado. |
| `outputFilename` | `string` | ninguno | Sustituye el nombre de archivo generado. |
| `pdfOptions` | `array` | ninguno | Configuración de página. Se respeta cuando la salida es PDF. |

EPUB no tiene un par de documento propio. Envía los archivos `.epub` a través de `convertToPdf` o `convertToMarkdown`.

#### Pares de conversión admitidos

| Entrada | Salidas |
|---------|---------|
| `json` | `csv`, `toml`, `xml`, `yaml` |
| `xml` | `csv`, `json` |
| `yaml`, `toml` | `json` |
| `csv` | `json`, `xml` |
| `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` |

El SDK valida el par `{input}-to-{output}` contra esa tabla y lanza `EnconvertException` antes de que ninguna solicitud salga de tu proceso, con la lista de salidas válidas para esa entrada en el mensaje. Puedes consultar la misma tabla directamente:

```php
use Enconvert\Formats;

Formats::validOutputsFor('json');   // ["csv", "toml", "xml", "yaml"]
Formats::validOutputsFor('pdf');    // ["jpeg"]
Formats::normalizeOutputFormat('JPG');   // "jpeg"
Formats::mimeFor('deck.pptx');           // el tipo MIME de PPTX
count(Formats::IMPLEMENTED_CONVERSIONS); // 43
```

### convertToMarkdown

Detecta automáticamente el formato de un documento en el servidor y devuelve Markdown. Este es el bloque de construcción para ingesta de RAG a partir de archivos que ya tienes en disco.

```php
$client->convertToMarkdown('handbook.docx', ['saveTo' => 'handbook.md']);
```

Acepta PDF, DOCX, PPTX, XLSX, CSV, HTML, EPUB, TXT/MD y archivos ofimáticos heredados u ODF. Las imágenes no están admitidas. Las opciones son `saveTo` y `outputFilename`; este endpoint no tiene opciones de PDF.

### convertToPdf

Detecta automáticamente casi cualquier entrada y devuelve un PDF.

```php
$client->convertToPdf('slides.pptx', ['saveTo' => 'slides.pdf']);

// Una entrada PDF pasa directamente, así que esto sirve también para normalizar a escala de grises.
$client->convertToPdf('scan.pdf', ['pdfOptions' => ['grayscale' => true], 'saveTo' => 'gray.pdf']);
```

Acepta formatos ofimáticos, ODF, Pages, Numbers, RTF, CSV, HTML, Markdown, texto plano, imágenes rasterizadas, SVG, EPUB y un PDF existente como paso directo.

<div class="alert alert-warning">
<strong>Aquí solo se respeta <code>pdfOptions.grayscale</code>.</strong> El formato se detecta en el servidor, así que la geometría de página viene del documento de origen. Usa <code>convertDocument</code> o <code>convertUrlToPdf</code> cuando necesites tamaño de página, márgenes, orientación, encabezados o pies.
</div>

### convertWebsiteToPdf y convertWebsiteToScreenshot

Descubren todas las páginas de un sitio, convierten cada una en segundo plano y reúnen un único ZIP. Ambos son asíncronos y devuelven un `BatchSubmission` en lugar de un `ConversionResult`. Requieren una clave de API privada con acceso de rastreo.

```php
$batch = $client->convertWebsiteToPdf('https://example.com', [
    'crawlMode' => 'sitemap',              // "auto" (por defecto) | "sitemap" | "full"
    'excludePatterns' => ['/blog/tag/'],   // solo en modo de rastreo completo
    'notificationEmail' => 'ops@my.app',
]);

echo $batch->batchId, ' ', $batch->urlCount, ' ', $batch->discoveryMethod, PHP_EOL;

// Bloquea hasta que el lote salga de "processing", luego guarda el ZIP.
$status = $client->waitForBatch($batch->batchId, ['saveTo' => 'site.zip']);
echo $status->completed, ' of ', $status->total, ' pages converted', PHP_EOL;

// O sondéalo tú mismo con getBatchStatus().
$s = $client->getBatchStatus($batch->batchId);
echo $s->status === 'processing' ? 'still working' : $s->zipDownloadUrl, PHP_EOL;
```

`convertWebsiteToScreenshot` toma las mismas opciones menos `singlePage` y `pdfOptions`, y produce un ZIP de PNG.

| Opción | Tipo | Por defecto | Descripción |
|--------|------|-------------|-------------|
| `crawlMode` | `string` | `"auto"` | `auto`, `sitemap` o `full`. |
| `includePatterns`, `excludePatterns` | `string[]` | ninguno | Conserva o descarta URL por patrón. Las exclusiones se aplican en modo de rastreo completo. |
| `notificationEmail`, `callbackUrl` | `string` | ninguno | Dirección de correo y webhook a los que notificar al finalizar. |
| `viewportWidth`, `viewportHeight`, `loadMedia`, `enableScroll` | mixto | valor del servidor | Opciones de renderizado por página. Se envían solo cuando las estableces. |
| `auth`, `cookies`, `headers` | `array` | ninguno | Acceso a la página para sitios tras un inicio de sesión. |
| `outputFilename` | `string` | ninguno | Sustituye el nombre de archivo del ZIP. |

`waitForBatch` acepta `intervalMs` (por defecto `5000`), `timeoutMs` (por defecto `1800000`, es decir 30 minutos) y `saveTo`. Lanza `ApiException` con estado `504` si vence el plazo mientras el lote sigue procesándose.

### getJobStatus

Sondea un único job de conversión asíncrono o recuperado.

```php
$status = $client->getJobStatus('job_abc123');

if ($status->status === 'success') {
    echo $status->presignedUrl, PHP_EOL;
} elseif ($status->status === 'failed') {
    echo $status->error, PHP_EOL;
}
```

Devuelve un `JobStatus` con `status` (`processing`, `success` o `failed`), `presignedUrl`, `objectKey` y `error`. Rara vez lo necesitas directamente, porque el SDK sondea en tu nombre. Ver [Recuperación de timeouts](#recuperacion-de-timeouts).

---

## Inteligencia web (V2)

El espacio de nombres `$client->v2` convierte páginas web en vivo en datos listos para agentes: renderizar, buscar, extraer, ingerir y monitorizar. Cada lectura lleva `renderQuality`, una puntuación de 0.0 a 1.0 que distingue un renderizado real de uno fallido. Una página de desafío, un muro de cookies, una pantalla de inicio de sesión o el armazón vacío de una SPA vuelven con una puntuación baja y con `warnings` rellenos, de modo que una mala lectura nunca entra en silencio en el contexto de un agente. El contenido se sigue devolviendo; queda marcado, no oculto. Lee los conceptos en la [visión general de V2](/es/docs/v2-overview).

### Perceive

Renderiza una URL y materializa las salidas que hayas pedido a partir de ese único renderizado. Ver [la referencia de perceive](/es/docs/v2-perceive).

```php
$op = $client->v2->perceive('https://example.com/pricing', [
    'outputs' => ['markdown', 'screenshot', 'structured'],
    'extract' => ['tables', 'metadata'],
    'onlyMainContent' => true,
    'waitFor' => '.price',
]);

echo $op->renderQuality, ' ', $op->statusCode, PHP_EOL;  // puntuación, estado HTTP del origen
print_r($op->deductions);                       // p. ej. ["login_wall" => 0.65]
echo $op->outputs['markdown']->url, PHP_EOL;    // URL firmada, 15 minutos
print_r($op->structured);

// Las URL firmadas caducan. Vuelve a firmarlas después sin renderizar de nuevo.
$again = $client->v2->getPerceiveOperation($op->operationId);
```

| Opción | Tipo | Descripción |
|--------|------|-------------|
| `outputs` | `string[]` | Cualquiera de `markdown`, `html_cleaned`, `html_raw`, `screenshot`, `screenshot_full_page`, `pdf`, `links`, `images`, `structured`. |
| `extract` | `string[]` | Campos estructurados que extraer, por ejemplo `metadata`, `structured_data`, `headings`, `tables`, `main_content`, `all`. |
| `schema` | `array` | Schema para la extracción estructurada respaldada por LLM. |
| `onlyMainContent` | `bool` | Elimina el armazón del sitio de la salida Markdown. |
| `waitFor`, `waitTimeoutMs` | `string`, `int` | Espera a un selector CSS o a una expresión JS tras la navegación. |
| `jsCode` | `string` | JavaScript que ejecutar en la página tras la navegación. |
| `viewport`, `mobile`, `blockResources` | `array`, `bool`, `string[]` | Geometría de renderizado y tipos de recurso que abortar antes de que carguen. |
| `cacheMode` | `string` | `enabled`, `bypass` o `refresh`. |
| `headers`, `cookies`, `auth` | `array` | Acceso a páginas autenticadas. |
| `respectRobots` | `bool` | Rechaza las URL desautorizadas por `robots.txt`. |
| `pdfOptions` | `array` | Configuración de página para la salida `pdf`. |
| `directDownload` | `bool` | Devuelve los bytes del artefacto en crudo. Solo lo acepta `perceive()`. |
| `proxyUrl`, `geolocation`, `actionChain` | mixto | Serializados por el SDK, reservados por la API. |

#### Percepción por lotes

```php
$batch = $client->v2->perceiveBatch(['https://a.com', 'https://b.com'], [
    'outputs' => ['markdown'],
    'outputMode' => 'zip',
]);

// Los lotes pequeños se completan en línea. Los más grandes vuelven en cola, así que sondea.
if ($batch->status !== 'completed') {
    $batch = $client->v2->getPerceiveBatch($batch->jobId);
}
foreach ($batch->items as $item) {
    echo $item->url, ' ', $item->renderQuality, PHP_EOL;
}
echo $batch->zip?->url, PHP_EOL;
```

Hasta 1000 URL comparten un único bloque de opciones. `outputMode` es `manifest` (por defecto) o `zip`. Aquí no se acepta `directDownload`.

#### Transmitir un único artefacto

`perceiveDirect()` se salta el envoltorio JSON y te entrega los bytes. Hay que solicitar exactamente una salida que produzca artefacto, y el SDK lo impone antes de enviar, así que `structured` por sí solo lanza excepción.

```php
$direct = $client->v2->perceiveDirect('https://example.com', [
    'outputs' => ['pdf'],
]);

file_put_contents($direct->filename ?? 'page.pdf', $direct->content);
echo $direct->renderQuality, ' ', $direct->contentType, PHP_EOL;
echo $direct->operationId, ' cache hit: ', var_export($direct->cacheHit, true), PHP_EOL;

// Vuelve a descargar después un artefacto almacenado. Omite el nombre de salida
// cuando la operación produjo exactamente un artefacto.
$saved = $client->v2->downloadPerceiveArtifact($direct->operationId, 'pdf');
```

`PerceiveDirectResult` lleva `content`, `contentType`, `filename`, `operationId`, `objectKey`, `cacheHit`, `renderQuality`, `sourceStatusCode`, `contentHash` y `warningsCount`. Una `ApiException` con estado `410` significa que el artefacto almacenado ya no está disponible.

### Discover

Enumera las URL de un sitio solo por HTTP. Sin renderizado en navegador, así que es rápido. Ver [la referencia de discover](/es/docs/v2-discover).

```php
$found = $client->v2->discover('https://example.com', [
    'mode' => 'hybrid',                 // "sitemap" | "crawl" | "hybrid"
    'maxUrls' => 200,
    'maxDepth' => 3,
    'excludePatterns' => ['/tag/'],
    'sameDomainOnly' => true,
]);

echo $found->total, ' urls, truncated: ', var_export($found->truncated, true), PHP_EOL;
print_r($found->sources);   // p. ej. ["sitemap" => 42, "crawl" => 30]
```

Las opciones son `mode`, `maxUrls`, `maxDepth`, `includePatterns`, `excludePatterns`, `sameDomainOnly` y `respectRobots`.

### Lookup

Ejecuta una búsqueda web por categorías y, opcionalmente, renderiza automáticamente los primeros resultados. Ver [la referencia de lookup](/es/docs/v2-lookup).

```php
$search = $client->v2->lookup('best static site generators', [
    'category' => 'web',        // web | news | images | scholar | patents | maps
    'numResults' => 10,
    'country' => 'us',
    'timeFilter' => 'month',
    'perceiveTop' => 3,         // renderiza automáticamente los 3 primeros resultados
]);

foreach ($search->results as $hit) {
    echo $hit->position, '. ', $hit->title, ' ', $hit->url, PHP_EOL;
    echo '   quality ', $hit->perceive?->renderQuality ?? 'not perceived', PHP_EOL;
}
```

Las opciones son `category`, `country`, `locale`, `timeFilter`, `numResults`, `page`, `location`, `autocorrect` y `perceiveTop`.

### Distill

Extracción estructurada guiada por schema sobre una lista de URL o un sitio descubierto. Ver [la referencia de distill](/es/docs/v2-distill).

```php
$extraction = $client->v2->distill([
    'urls' => ['https://example.com/pricing'],
    'schema' => ['plans' => 'list of plan names with monthly prices'],
    'cssSchema' => [                     // pasada CSS gratuita opcional antes del nivel del LLM
        'baseSelector' => '.plan-card',
        'fields' => [
            ['name' => 'name', 'type' => 'text', 'selector' => 'h3'],
            ['name' => 'price', 'type' => 'text', 'selector' => '.price'],
        ],
    ],
]);

print_r($extraction->results[0]->data);
echo $extraction->results[0]->extractionTier, PHP_EOL;  // css | llm | mixed | none
echo $extraction->results[0]->fieldsFromCss, '/', $extraction->results[0]->fieldsFromLlm, PHP_EOL;

// O descubre primero el conjunto de URL.
$client->v2->distill([
    'discoverFrom' => ['url' => 'https://example.com', 'mode' => 'sitemap', 'maxPages' => 10],
    'schema' => ['title' => 'page title', 'summary' => 'one-line summary'],
]);
```

`schema` es obligatorio, y debe estar presente exactamente uno de `urls` o `discoverFrom`. Si incumples cualquiera de las dos reglas, el SDK lanza `EnconvertException` antes de enviar. Las demás opciones son `waitFor`, `waitTimeoutMs`, `headers`, `cookies` y `respectRobots`.

### Ingest

Convierte un sitio entero, o un montón de documentos subidos, en JSONL troceado y listo para RAG mediante un único pipeline. Ingest siempre es asíncrono. Ver [la referencia de ingest](/es/docs/v2-ingest).

```php
// Desde un sitio.
$job = $client->v2->ingest([
    'mode' => 'sitemap',                 // "urls" (por defecto) | "sitemap" | "crawl"
    'url' => 'https://docs.example.com',
    'maxPages' => 100,
    'chunk' => ['maxWords' => 512, 'sentenceOverlap' => 1],
    'webhookUrl' => 'https://my.app/hooks/enconvert',
]);

// O desde archivos subidos: PDF, DOCX, PPTX, XLSX, CSV, HTML, EPUB, TXT/MD, ofimática heredada y ODF.
$fileJob = $client->v2->ingestFiles(['handbook.pdf', 'notes.docx'], [
    'chunk' => ['maxWords' => 512, 'sentenceOverlap' => 1],
]);

// Sondea hasta que aterrice.
do {
    sleep(5);
    $status = $client->v2->getIngestJob($job->jobId);
} while (in_array($status->status, ['queued', 'discovering', 'processing'], true));

if ($status->status === 'completed') {
    echo $status->totalChunks, ' chunks: ', $status->outputUrl, PHP_EOL;  // JSONL firmado
}
```

`mode` es `urls` por defecto, lo que exige un array `urls` no vacío y rechaza `url`. Los modos `sitemap` y `crawl` exigen una `url` semilla y rechazan `urls`. El SDK impone ambas reglas localmente. Las demás opciones son `maxPages`, `maxDepth`, `sameDomainOnly`, `includePatterns`, `excludePatterns`, `respectRobots`, `waitFor`, `waitTimeoutMs`, `chunk` y `webhookUrl`.

```php
// Gestión de jobs.
$list = $client->v2->listIngestJobs(['limit' => 20, 'skip' => 0]);
foreach ($list->jobs as $summary) {
    echo $summary->jobId, ' ', $summary->status, ' ', $summary->totalChunks, PHP_EOL;
}
$client->v2->cancelIngestJob($job->jobId);        // idempotente

// Firma de webhooks.
$secret = $client->v2->getWebhookSecret();
echo $secret->signatureHeader, ' ', $secret->signatureScheme, PHP_EOL;
$client->v2->rotateWebhookSecret();               // las firmas antiguas dejan de verificarse
$client->v2->retryIngestWebhook($job->jobId);     // reenvía un callback de finalización
```

### Watch

Vuelve a renderizar una URL a una cadencia fija y te avisa cuando cambia. Ver [la referencia de watch](/es/docs/v2-watch).

```php
$watcher = $client->v2->createWatcher('https://example.com/pricing', [
    'frequencyMinutes' => 60,            // mínimo de una hora
    'diffMode' => 'auto',                // auto | text | structured | tables | metadata
    'webhookUrl' => 'https://my.app/hooks/changes',
    'notifyEmail' => true,
]);

echo $watcher->watcherId, ' next check ', $watcher->nextCheckAt, PHP_EOL;

$snapshots = $client->v2->getWatcherSnapshots($watcher->watcherId, ['limit' => 10]);
foreach ($snapshots->snapshots as $snap) {
    if ($snap->hasChanges) {
        echo $snap->checkedAt, ' similarity ', $snap->similarity,
             ' changes ', $snap->changeCount, PHP_EOL;
    }
}
$client->v2->updateWatcher($watcher->watcherId, ['status' => 'paused']);
$client->v2->updateWatcher($watcher->watcherId, ['webhookUrl' => '']);  // borra el webhook
$client->v2->deleteWatcher($watcher->watcherId);                        // borrado lógico, idempotente
```

`createWatcher` acepta `frequencyMinutes`, `diffMode`, `trackFields`, `webhookUrl` y `notifyEmail`. `updateWatcher` acepta esos cinco más `status`, y requiere al menos un campo o lanza `EnconvertException`. `listWatchers` acepta `skip` y `limit`.

<div class="alert alert-warning">
<strong>Los diffs de snapshot contienen contenido de página no confiable.</strong> Las entradas `changes` de un `WatcherSnapshot` se copian de la página monitorizada. Escápalas con `htmlspecialchars()` antes de renderizarlas en tu propia interfaz.
</div>

---

## Opciones de PDF

Se pasan como el array `pdfOptions` en `convertUrlToPdf`, `convertDocument`, `convertToPdf` (solo escala de grises), `convertWebsiteToPdf` y el `perceive` de V2 con una salida `pdf`. El SDK serializa las claves camelCase al formato de transmisión de la API y envía solo las claves que estableces.

```php
$client->convertUrlToPdf('https://example.com', [
    'pdfOptions' => [
        'pageSize' => 'A4',
        'orientation' => 'landscape',
        'margins' => ['top' => 10, 'bottom' => 10, 'left' => 15, 'right' => 15],
        'scale' => 0.9,
        'header' => ['content' => 'Quarterly Report', 'height' => 15],
        'footer' => ['content' => 'Page {{page}} of {{total_pages}}', 'height' => 12],
    ],
    'singlePage' => false,
    'saveTo' => 'report.pdf',
]);
```

| Campo | Tipo | Descripción |
|-------|------|-------------|
| `pageSize` | `string` | De `A0` a `A6`, de `B0` a `B5`, `Letter`, `Legal`, `Tabloid`, `Ledger`. Se ignora cuando `pageWidth` y `pageHeight` están ambos establecidos. |
| `pageWidth`, `pageHeight` | `float` | Tamaño de página personalizado en milímetros. Establécelos juntos. |
| `orientation` | `string` | `portrait` o `landscape`. |
| `margins` | `array` | `['top' => ..., 'bottom' => ..., 'left' => ..., 'right' => ...]` en milímetros. |
| `scale` | `float` | Escala de renderizado, de `0.1` a `2.0`. Solo para salida paginada. |
| `grayscale` | `bool` | Posprocesa el PDF a escala de grises. |
| `header` | `array` | `['content' => '<html>', 'height' => 15]`. Altura en milímetros. |
| `footer` | `array` | La misma forma que `header`. |

El contenido del encabezado y del pie admite las variables de plantilla `{{page}}`, `{{total_pages}}`, `{{date}}`, `{{title}}` y `{{url}}`. La referencia completa de parámetros vive en [parámetros y opciones](/es/docs/parameters-options).

---

## Manejo de errores

Todo lo que lanza el SDK desciende de `Enconvert\Exception\EnconvertException`, así que un único bloque catch puede acotar toda la superficie.

```php
use Enconvert\Exception\{
    ApiException, AuthenticationException, EnconvertException, QuotaException, RateLimitException
};

try {
    $client->convertUrlToPdf('https://example.com', ['saveTo' => 'page.pdf']);
} catch (AuthenticationException $e) {
    error_log('Check ENCONVERT_API_KEY: ' . $e->getMessage());
} catch (RateLimitException $e) {
    error_log('Too many requests, back off and retry.');
} catch (QuotaException $e) {
    error_log($e->getMessage());
} catch (ApiException $e) {
    error_log(sprintf('API error [%d]: %s', $e->getStatusCode(), $e->getMessage()));
} catch (EnconvertException $e) {
    error_log('Client-side or transport failure: ' . $e->getMessage());
}
```

| Clase | Se lanza en | Código de estado |
|-------|-------------|------------------|
| `AuthenticationException` | Clave de API inválida, ausente o revocada | respuestas `401` y `403` |
| `QuotaException` | HTTP 402 | `402` |
| `RateLimitException` | Demasiadas solicitudes | `429` |
| `ApiException` | Cualquier otra respuesta 4xx o 5xx | el código real |
| `EnconvertException` | Clase base, y todo fallo del lado del cliente | ninguno |

`ApiException::getStatusCode()` devuelve el estado numérico; el mensaje lleva el prefijo `[code]`. Una respuesta `403` se traduce en `AuthenticationException`, cuyo `getStatusCode()` informa de `401`, así que ramifica según la clase y no según el número cuando necesites distinguir ambos casos.

`EnconvertException` también se lanza sin ninguna ida y vuelta de red cuando el SDK detecta que la solicitud está condenada: una clave de API vacía, una ruta de archivo ausente, una extensión de archivo no reconocida, un par de conversión no implementado, una llamada a `distill` sin schema o con `urls` y `discoverFrom` a la vez, un desajuste entre el modo y la carga útil de `ingest`, una lista `ingestFiles` vacía, una llamada a `perceiveDirect` que no nombra exactamente una salida de artefacto, una llamada a `updateWatcher` sin campos y cualquier fallo de transporte de Guzzle (expuesto como `HTTP request failed: ...`).

El mapa completo de mensajes está en la [referencia de códigos de error](/es/docs/error-codes).

---

## Recuperación de timeouts

Las conversiones de documentos grandes y las páginas lentas pueden sobrevivir al timeout de un proxy inverso incluso cuando la conversión acaba teniendo éxito en el servidor. El SDK se recupera de eso por su cuenta:

1. Antes de cada solicitud de conversión genera un id de job hexadecimal de 32 caracteres y lo envía como `job_id` en el cuerpo o en el formulario multiparte.
2. Si esa solicitud vuelve con 5xx, el SDK deja de lanzar excepciones y empieza a sondear `GET /v1/convert/status/{job_id}` cada 3 segundos.
3. En cuanto el job se lee como `success`, el SDK devuelve el resultado. Si se lee como `failed`, lanza `ApiException` con el mensaje de error del servidor.
4. Un `404` durante el sondeo significa "todavía no registrado" y el sondeo continúa. El plazo es de 5 minutos, tras lo cual lanza `ApiException(504, 'Conversion timed out')`.

No hay código que escribir para esto. Dos excepciones deliberadas: los envíos de lotes de sitios web (`convertWebsiteToPdf`, `convertWebsiteToScreenshot`) no tienen una fila por job, así que un 5xx ahí aflora de inmediato, y los métodos V2 no usan sondeo de jobs en absoluto.

Las respuestas correctas que omiten `job_id` se rellenan con el id generado por el cliente, de modo que `$result->jobId` siempre se puede usar con `getJobStatus()`.

---

## Configuración

```php
use Enconvert\Client;

$client = new Client(getenv('ENCONVERT_API_KEY'), [
    'timeout' => 300,                            // segundos
    'base_url' => 'https://api.enconvert.com',   // sobrescríbelo para un gateway autoalojado
]);
```

| Opción | Tipo | Por defecto | Descripción |
|--------|------|-------------|-------------|
| `$apiKey` (primer argumento) | `string` | obligatorio | Clave de API privada (`sk_live_...`). Una cadena vacía lanza `EnconvertException`. |
| `timeout` | `int\|float` | `300` | Timeout de solicitud en **segundos**, la unidad idiomática de Guzzle. Se aplica a cada solicitud HTTP que hace el cliente. El plazo de sondeo del job es independiente y está fijado en 300 segundos. |
| `base_url` | `string` | `https://api.enconvert.com` | Host de la API. Las barras finales se eliminan. |

Fíjate en que la clave de opción es `base_url` en snake_case mientras que todas las demás opciones de solicitud del SDK van en camelCase. Las solicitudes se autentican con un encabezado `X-API-Key`; las descargas con `saveTo` van directamente al almacenamiento y deliberadamente no envían clave, porque la URL ya viene firmada.

<div class="alert alert-warning">
<strong>Nunca incrustes la clave de API en el código.</strong> Léela desde una variable de entorno o desde tu gestor de secretos, y mantenla fuera del control de versiones y de cualquier cosa que llegue a un navegador. Genera y rota claves en el <a href="/es/dashboard">panel de control</a>, y consulta la <a href="/es/docs/authentication">guía de autenticación</a> para conocer los tipos de clave.
</div>

---

## Forma del resultado

Toda conversión de un solo archivo devuelve un `Enconvert\Model\ConversionResult` con propiedades públicas de solo lectura:

```php
final class ConversionResult
{
    public readonly string $presignedUrl;              // URL de descarga firmada
    public readonly string $objectKey;                 // clave del objeto en almacenamiento
    public readonly string $filename;                  // nombre de archivo del servidor
    public readonly int|float|null $fileSize;          // bytes
    public readonly int|float|null $conversionTimeSeconds;
    public readonly ?string $jobId;                    // el id usado para la recuperación de timeouts
}
```

Las URL prefirmadas caducan a los 15 minutos y se pueden usar más de una vez antes de eso. Para acceso permanente, descarga los bytes (pasa `saveTo`, o descarga la URL tú mismo) y guárdalos en tu propio bucket.

Los demás tipos de resultado siguen el mismo patrón: `JobStatus`, `BatchSubmission`, `BatchStatus` con su `BatchItem[]` y, bajo `Enconvert\Model\V2`, `PerceiveResult`, `PerceiveDirectResult`, `PerceiveBatchResult`, `OutputArtifact`, `DiscoverResult`, `LookupResult`, `LookupItem`, `DistillResult`, `DistillItem`, `IngestJob`, `IngestJobList`, `IngestJobSummary`, `Watcher`, `WatcherList`, `WatcherSummary`, `WatcherSnapshot`, `WatcherSnapshotList`, `WebhookSecret`, `WebhookRetryResult` y `Tokens`. Los campos de transmisión llegan en snake_case y se mapean a propiedades camelCase; las cargas útiles proporcionadas por el usuario, como schemas, datos extraídos, campos rastreados y entradas de diff, pasan intactas.

---

## Código fuente e incidencias

- **Packagist:** [enconvert/enconvert-php](https://packagist.org/packages/enconvert/enconvert-php)
- **GitHub:** [conversionapi/php-sdk](https://github.com/conversionapi/php-sdk) · [abre una incidencia](https://github.com/conversionapi/php-sdk/issues)
- **Licencia:** MIT
- **Otros clientes:** [todos los SDK](/es/docs/sdks) · [endpoints REST](/es/docs/endpoints-overview)

---

## Preguntas frecuentes

### ¿Cómo convierto archivos en PHP con Composer?

Ejecuta `composer require enconvert/enconvert-php`, construye `new Enconvert\Client($apiKey)` y llama a un método como `convertUrlToPdf`, `convertImage`, `convertDocument`, `convertToPdf` o `convertToMarkdown`. Añade `saveTo` a cualquiera de ellos y el SDK transmite los bytes convertidos a esa ruta local por ti, creando por el camino los directorios padre.

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

Llama a `$client->convertDocument('report.docx', ['saveTo' => 'report.pdf'])`. El formato de salida es `pdf` por defecto, así que no hace falta `outputFormat`. El formato de entrada se lee de la extensión del archivo, y el SDK comprueba el par `doc-to-pdf` contra su tabla de 43 conversiones implementadas antes de enviar nada, de modo que un par no admitido falla al instante con un mensaje que enumera lo que es válido para esa entrada.

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

Llama a `$client->convertUrlToPdf('https://example.com', ['saveTo' => 'page.pdf'])`. Por defecto la página se renderiza como una única página continua con un viewport de 1920x1080, con carga de medios y desplazamiento activados. Establece `singlePage` en `false` y pasa `pdfOptions` cuando quieras paginación real con tamaño de página, márgenes, encabezados y pies.

### ¿Cómo extraigo una página web en PHP y obtengo Markdown limpio?

Usa el espacio de nombres V2: `$client->v2->perceive($url, ['outputs' => ['markdown', 'structured']])`. Renderiza la página en un navegador real, así que los sitios con mucho JavaScript funcionan, y devuelve una URL de Markdown firmada más datos estructurados en línea. Comprueba `renderQuality` en el resultado antes de confiar en el contenido; una puntuación baja con `deductions` rellenos significa que el renderizado topó con una página antibots, un muro de inicio de sesión o un armazón vacío. Para una ruta más ligera de un solo paso sin las funciones de V2, `convertUrlToMarkdown` también sirve.

### ¿Cómo convierto HEIC a WebP en PHP?

Llama a `$client->convertImage('photo.heic', ['outputFormat' => 'webp', 'saveTo' => 'photo.webp'])`. Se admite cualquier par entre `jpeg`, `png`, `svg`, `heic` y `webp`, más la rasterización de `pdf` a `jpeg`. También puedes pasar bytes en crudo como `['data' => $bytes, 'filename' => 'photo.heic']`, donde el nombre de archivo es lo que resuelve el formato de entrada y el tipo MIME.

### ¿Funciona el SDK de PHP de EnConvert con Laravel o Symfony?

Sí. El paquete es una librería PSR-4 corriente con Guzzle 7 como única dependencia y sin acoplamiento a ningún framework, así que encaja sin cambios en Laravel, Symfony, WordPress o un script pelado. Registra `Enconvert\Client` en tu contenedor con la clave de tu configuración de entorno e inyéctalo donde lo necesites.

### ¿Qué pasa cuando una conversión tarda más que el timeout HTTP?

El SDK se recupera por su cuenta. Envía un `job_id` generado por el cliente con cada solicitud de conversión y, si la solicitud devuelve 5xx, sondea en silencio `GET /v1/convert/status/{job_id}` cada 3 segundos durante hasta 5 minutos, devolviendo el resultado en cuanto el job registra `success`. Pasado el plazo lanza `ApiException(504, 'Conversion timed out')`. Los envíos de lotes de sitios web quedan fuera de esto, ya que no tienen una fila por job.

### ¿Cuánto tiempo es válida la URL de descarga prefirmada?

Las URL de descarga prefirmadas caducan a los 15 minutos y se pueden usar más de una vez antes de eso. Las URL de artefacto de V2 llevan un `expiresIn` de 900 segundos por la misma razón, y `getPerceiveOperation($operationId)` las vuelve a firmar a partir de las claves de objeto almacenadas sin renderizar la página de nuevo. Para cualquier cosa permanente, descarga los bytes y guárdalos en tu propio almacenamiento.

### ¿Qué versión de PHP requiere el SDK?

PHP 8.1 o posterior. El SDK usa propiedades de solo lectura, tipos unión al estilo enum, argumentos nombrados y expresiones `match` de principio a fin, así que 8.0 y anteriores no están admitidos. Su única dependencia en tiempo de ejecución es `guzzlehttp/guzzle ^7.8`.
