---
seo_title: PHP File Conversion SDK: Composer API Client | EnConvert
meta_desc: Official EnConvert PHP SDK for PHP 8.1+. Install via Composer to convert documents and images and to perceive, discover, distill, ingest, and watch web pages.
keywords: php file conversion sdk, convert files php, url to pdf php, php web scraping api, docx to pdf php, enconvert php sdk, composer file conversion package, heic to webp php, html to pdf php composer, php url to markdown api, laravel document conversion api
---

# PHP File Conversion SDK

`enconvert/enconvert-php` is the official PHP client for the EnConvert API. Install it with Composer, hand it one API key, and you get two things: nine conversion methods that turn URLs, images, and documents into PDFs, PNGs, Markdown, and data formats, and a `$client->v2` namespace that reads the live web into agent-ready Markdown, screenshots, and structured JSON. It targets PHP 8.1+, is built on Guzzle 7, uses camelCase option arrays throughout, and returns readonly typed result objects instead of loose arrays. Long conversions that outlive an HTTP timeout are recovered automatically by job polling.

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

---

## Install

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

The package autoloads under the `Enconvert\` PSR-4 namespace and ships no CLI, no config file, and no service provider. It works unchanged in plain PHP, Laravel, Symfony, WordPress, and any PSR-4 project.

---

## Quick start

```php
<?php

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

use Enconvert\Client;

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

// Convert a live page to PDF and stream it straight to disk.
$result = $client->convertUrlToPdf('https://example.com', [
    'saveTo' => 'page.pdf',
]);

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

// Reading a page for an agent uses the same client and the same key.
$op = $client->v2->perceive('https://example.com', ['outputs' => ['markdown', 'structured']]);

echo $op->renderQuality, PHP_EOL;              // 0.0 to 1.0, e.g. 0.93
echo $op->outputs['markdown']->url, PHP_EOL;   // signed download URL
```

The SDK is server-side only. A private key (`sk_live_...`) must never reach a browser or a mobile app.

---

## What the client exposes

| Surface | How you reach it | What it does |
|---------|------------------|--------------|
| File and URL conversion | `$client->convert*()` | Nine methods over `POST /v1/convert/*`, plus three job and batch polling helpers. |
| Web intelligence (V2) | `$client->v2` (or `$client->v2()`) | Twenty-three methods over `/v2/*`: perceive, discover, lookup, distill, ingest, watch. |
| Format introspection | `Enconvert\Formats` | The 43 implemented `{input}-to-{output}` pairs, MIME lookup, and per-input output lists. |
| Errors | `Enconvert\Exception\*` | `EnconvertException` base plus `ApiException`, `AuthenticationException`, `QuotaException`, `RateLimitException`. |
| Results | `Enconvert\Model\*` | Readonly value objects with camelCase properties. |

`$client->v2` is a public readonly property. `$client->v2()` is an identical accessor for callers who prefer method syntax. Every V2 endpoint requires a private API key; public keys are rejected.

---

## File conversion

Every single-file method returns a `ConversionResult` carrying a presigned download URL. Pass `saveTo` and the SDK also streams the bytes to that local path, creating parent directories as needed.

### convertUrlToPdf

Render any reachable URL to PDF.

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

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `saveTo` | `string` | none | Local path to stream the PDF to. Parent directories are created. |
| `singlePage` | `bool` | `true` | `true` produces one continuous page. `false` paginates using `pdfOptions.pageSize`. |
| `pdfOptions` | `array` | none | Page size, orientation, margins, scale, grayscale, header, footer. See [PDF options](#pdf-options). |
| `viewportWidth`, `viewportHeight` | `int` | `1920`, `1080` | Browser viewport size in pixels. |
| `loadMedia` | `bool` | `true` | Wait for images and video before capture. |
| `enableScroll` | `bool` | `true` | Scroll top to bottom so lazy loaders fire. |
| `outputFilename` | `string` | auto | Override the generated filename. |
| `auth`, `cookies`, `headers` | `array` | none | Page access: Basic Auth `['username' => ..., 'password' => ...]`, injected cookies, custom request headers. |

<div class="alert alert-warning">
<strong>Do not combine <code>auth</code> with an <code>Authorization</code> header.</strong> The API rejects the conflict rather than guessing which credential you meant.
</div>

### convertUrlToScreenshot

Capture a PNG of any URL.

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

Accepts the same viewport, media, scroll, filename, and page-access options as `convertUrlToPdf`, minus `singlePage` and `pdfOptions`.

### convertUrlToMarkdown

Extract clean GitHub-Flavored Markdown from a URL. Navigation, footers, ads, and scripts are stripped, the main article body is kept, and YAML frontmatter (title, description, url, links, images) is prepended.

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

Same option set as `convertUrlToScreenshot`.

### convertImage

Convert between `jpeg`, `png`, `svg`, `heic`, and `webp`, or rasterize a PDF to JPEG.

```php
// From a path.
$client->convertImage('photo.heic', ['outputFormat' => 'webp', 'saveTo' => 'photo.webp']);

// From raw bytes. The filename resolves the input format and MIME type.
$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']); // rasterize
```

The input format comes from the file extension: `.jpg`, `.jpeg`, `.png`, `.svg`, `.heic`, `.webp`, and `.pdf`. The output format is required and is normalized for you, so `jpg` resolves to `jpeg`.

| Option | Type | Required | Description |
|--------|------|----------|-------------|
| `outputFormat` | `string` | Yes | One of `jpeg`, `png`, `svg`, `heic`, `webp`. |
| `saveTo` | `string` | No | Local path to stream the result to. |
| `outputFilename` | `string` | No | Override the generated filename. |

### convertDocument

Convert documents and data formats. `outputFormat` defaults to `pdf`.

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

// markdown to pdf with page setup
$client->convertDocument('README.md', [
    'outputFormat' => 'pdf',
    'pdfOptions' => ['pageSize' => 'A4', 'margins' => ['top' => 20, 'bottom' => 20]],
    'saveTo' => 'readme.pdf',
]);
```

**Recognized input extensions:** `.doc`, `.docx`, `.xls`, `.xlsx`, `.ppt`, `.pptx`, `.html`, `.htm`, `.odt`, `.ods`, `.odp`, `.ots`, `.pages`, `.numbers`, `.md`, `.markdown`, `.csv`, `.json`, `.xml`, `.yaml`, `.yml`, `.toml`.

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `outputFormat` | `string` | `"pdf"` | Target format. Aliases `jpg`, `yml`, `htm`, and `md` are resolved. |
| `saveTo` | `string` | none | Local path to stream the result to. |
| `outputFilename` | `string` | none | Override the generated filename. |
| `pdfOptions` | `array` | none | Page setup. Honored when the output is PDF. |

EPUB has no dedicated document pair. Send `.epub` files through `convertToPdf` or `convertToMarkdown`.

#### Supported conversion pairs

| Input | Outputs |
|-------|---------|
| `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` | each other (all 20 pairs) |
| `pdf` | `jpeg` |

The SDK validates the `{input}-to-{output}` pair against that table and throws `EnconvertException` before any request leaves your process, with the list of valid outputs for that input in the message. You can query the same table directly:

```php
use Enconvert\Formats;

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

### convertToMarkdown

Auto-detect a document's format server-side and return Markdown. This is the RAG-ingestion building block for files you already have on disk.

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

Accepts PDF, DOCX, PPTX, XLSX, CSV, HTML, EPUB, TXT/MD, and legacy or ODF office files. Images are not supported. Options are `saveTo` and `outputFilename`; there are no PDF options on this endpoint.

### convertToPdf

Auto-detect almost any input and return a PDF.

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

// A PDF input is passed through, so this doubles as a grayscale normalize path.
$client->convertToPdf('scan.pdf', ['pdfOptions' => ['grayscale' => true], 'saveTo' => 'gray.pdf']);
```

Accepts office, ODF, Pages, Numbers, RTF, CSV, HTML, Markdown, plain text, raster images, SVG, EPUB, and an existing PDF as passthrough.

<div class="alert alert-warning">
<strong>Only <code>pdfOptions.grayscale</code> is honored here.</strong> The format is detected server-side, so page geometry comes from the source document. Use <code>convertDocument</code> or <code>convertUrlToPdf</code> when you need page size, margins, orientation, headers, or footers.
</div>

### convertWebsiteToPdf and convertWebsiteToScreenshot

Discover every page of a site, convert each one in the background, and collect a single ZIP. Both are asynchronous and return a `BatchSubmission` rather than a `ConversionResult`. They require a private API key with crawl access.

```php
$batch = $client->convertWebsiteToPdf('https://example.com', [
    'crawlMode' => 'sitemap',              // "auto" (default) | "sitemap" | "full"
    'excludePatterns' => ['/blog/tag/'],   // full crawl mode only
    'notificationEmail' => 'ops@my.app',
]);

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

// Block until the batch leaves "processing", then save the ZIP.
$status = $client->waitForBatch($batch->batchId, ['saveTo' => 'site.zip']);
echo $status->completed, ' of ', $status->total, ' pages converted', PHP_EOL;

// Or poll it yourself with getBatchStatus().
$s = $client->getBatchStatus($batch->batchId);
echo $s->status === 'processing' ? 'still working' : $s->zipDownloadUrl, PHP_EOL;
```

`convertWebsiteToScreenshot` takes the same options minus `singlePage` and `pdfOptions`, and produces a ZIP of PNGs.

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `crawlMode` | `string` | `"auto"` | `auto`, `sitemap`, or `full`. |
| `includePatterns`, `excludePatterns` | `string[]` | none | Keep or drop URLs by pattern. Exclusions apply in full crawl mode. |
| `notificationEmail`, `callbackUrl` | `string` | none | Email address and webhook to notify on completion. |
| `viewportWidth`, `viewportHeight`, `loadMedia`, `enableScroll` | mixed | server default | Per-page render options. Sent only when you set them. |
| `auth`, `cookies`, `headers` | `array` | none | Page access for sites behind a login. |
| `outputFilename` | `string` | none | Override the ZIP filename. |

`waitForBatch` accepts `intervalMs` (default `5000`), `timeoutMs` (default `1800000`, so 30 minutes), and `saveTo`. It throws `ApiException` with status `504` if the deadline passes while the batch is still processing.

### getJobStatus

Poll a single async or recovered conversion job.

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

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

Returns a `JobStatus` with `status` (`processing`, `success`, or `failed`), `presignedUrl`, `objectKey`, and `error`. You rarely need it directly, because the SDK polls on your behalf. See [Timeout recovery](#timeout-recovery).

---

## Web intelligence (V2)

The `$client->v2` namespace turns live web pages into agent-ready data: render, search, extract, ingest, and monitor. Every read carries `renderQuality`, a score from 0.0 to 1.0 that tells a real render from a failed one. A challenge page, a cookie wall, a login screen, or an empty SPA shell comes back with a low score and populated `warnings`, so a bad read never quietly enters an agent's context. The content is still returned; it is flagged, not hidden. Read the concepts in the [V2 overview](/docs/v2-overview).

### Perceive

Render one URL and materialize whatever outputs you asked for from that single render. See [the perceive reference](/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;  // score, upstream HTTP status
print_r($op->deductions);                       // e.g. ["login_wall" => 0.65]
echo $op->outputs['markdown']->url, PHP_EOL;    // signed URL, 15 minutes
print_r($op->structured);

// Signed URLs expire. Re-sign them later without re-rendering.
$again = $client->v2->getPerceiveOperation($op->operationId);
```

| Option | Type | Description |
|--------|------|-------------|
| `outputs` | `string[]` | Any of `markdown`, `html_cleaned`, `html_raw`, `screenshot`, `screenshot_full_page`, `pdf`, `links`, `images`, `structured`. |
| `extract` | `string[]` | Structured fields to pull, for example `metadata`, `structured_data`, `headings`, `tables`, `main_content`, `all`. |
| `schema` | `array` | Schema for LLM-backed structured extraction. |
| `onlyMainContent` | `bool` | Strip site chrome from the Markdown output. |
| `waitFor`, `waitTimeoutMs` | `string`, `int` | Wait for a CSS selector or JS expression after navigation. |
| `jsCode` | `string` | JavaScript to run on the page after navigation. |
| `viewport`, `mobile`, `blockResources` | `array`, `bool`, `string[]` | Render geometry, and resource types to abort before they load. |
| `cacheMode` | `string` | `enabled`, `bypass`, or `refresh`. |
| `headers`, `cookies`, `auth` | `array` | Page access for authenticated pages. |
| `respectRobots` | `bool` | Reject URLs disallowed by `robots.txt`. |
| `pdfOptions` | `array` | Page setup for the `pdf` output. |
| `directDownload` | `bool` | Return raw artifact bytes. Accepted by `perceive()` only. |
| `proxyUrl`, `geolocation`, `actionChain` | mixed | Serialized by the SDK, reserved by the API. |

#### Batch perception

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

// Small batches complete inline. Larger ones come back queued, so poll.
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;
```

Up to 1000 URLs share one options block. `outputMode` is `manifest` (default) or `zip`. `directDownload` is not accepted here.

#### Streaming a single artifact

`perceiveDirect()` skips the JSON envelope and hands you the bytes. Exactly one artifact-producing output must be requested, and the SDK enforces that before sending, so `structured` on its own throws.

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

// Re-download a stored artifact later. Omit the output name when the
// operation produced exactly one artifact.
$saved = $client->v2->downloadPerceiveArtifact($direct->operationId, 'pdf');
```

`PerceiveDirectResult` carries `content`, `contentType`, `filename`, `operationId`, `objectKey`, `cacheHit`, `renderQuality`, `sourceStatusCode`, `contentHash`, and `warningsCount`. An `ApiException` with status `410` means the stored artifact is no longer available.

### Discover

Enumerate a site's URLs over HTTP only. No browser render, so it is fast. See [the discover reference](/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);   // e.g. ["sitemap" => 42, "crawl" => 30]
```

Options are `mode`, `maxUrls`, `maxDepth`, `includePatterns`, `excludePatterns`, `sameDomainOnly`, and `respectRobots`.

### Lookup

Run a categorized web search and optionally auto-render the top results. See [the lookup reference](/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,         // auto-render the top 3 hits
]);

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

Options are `category`, `country`, `locale`, `timeFilter`, `numResults`, `page`, `location`, `autocorrect`, and `perceiveTop`.

### Distill

Schema-driven structured extraction across a URL list or a discovered site. See [the distill reference](/docs/v2-distill).

```php
$extraction = $client->v2->distill([
    'urls' => ['https://example.com/pricing'],
    'schema' => ['plans' => 'list of plan names with monthly prices'],
    'cssSchema' => [                     // optional free CSS pass before the LLM tier
        '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;

// Or discover the URL set first.
$client->v2->distill([
    'discoverFrom' => ['url' => 'https://example.com', 'mode' => 'sitemap', 'maxPages' => 10],
    'schema' => ['title' => 'page title', 'summary' => 'one-line summary'],
]);
```

`schema` is required, and exactly one of `urls` or `discoverFrom` must be present. Break either rule and the SDK throws `EnconvertException` before sending. Other options are `waitFor`, `waitTimeoutMs`, `headers`, `cookies`, and `respectRobots`.

### Ingest

Turn a whole site, or a stack of uploaded documents, into chunked RAG-ready JSONL through one pipeline. Ingest is always asynchronous. See [the ingest reference](/docs/v2-ingest).

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

// Or from uploaded files: PDF, DOCX, PPTX, XLSX, CSV, HTML, EPUB, TXT/MD, legacy and ODF office.
$fileJob = $client->v2->ingestFiles(['handbook.pdf', 'notes.docx'], [
    'chunk' => ['maxWords' => 512, 'sentenceOverlap' => 1],
]);

// Poll until it lands.
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;  // signed JSONL
}
```

`mode` defaults to `urls`, which requires a non-empty `urls` array and rejects `url`. The `sitemap` and `crawl` modes require a seed `url` and reject `urls`. The SDK enforces both rules locally. Other options are `maxPages`, `maxDepth`, `sameDomainOnly`, `includePatterns`, `excludePatterns`, `respectRobots`, `waitFor`, `waitTimeoutMs`, `chunk`, and `webhookUrl`.

```php
// Job management.
$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);        // idempotent

// Webhook signing.
$secret = $client->v2->getWebhookSecret();
echo $secret->signatureHeader, ' ', $secret->signatureScheme, PHP_EOL;
$client->v2->rotateWebhookSecret();               // old signatures stop verifying
$client->v2->retryIngestWebhook($job->jobId);     // re-deliver a completion callback
```

### Watch

Re-render a URL on a fixed cadence and notify you when it changes. See [the watch reference](/docs/v2-watch).

```php
$watcher = $client->v2->createWatcher('https://example.com/pricing', [
    'frequencyMinutes' => 60,            // hourly floor
    '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' => '']);  // clears the webhook
$client->v2->deleteWatcher($watcher->watcherId);                        // soft delete, idempotent
```

`createWatcher` accepts `frequencyMinutes`, `diffMode`, `trackFields`, `webhookUrl`, and `notifyEmail`. `updateWatcher` accepts those five plus `status`, and requires at least one field or it throws `EnconvertException`. `listWatchers` accepts `skip` and `limit`.

<div class="alert alert-warning">
<strong>Snapshot diffs contain untrusted page content.</strong> The `changes` entries on a `WatcherSnapshot` are copied from the monitored page. Escape them with `htmlspecialchars()` before rendering them in your own UI.
</div>

---

## PDF options

Passed as the `pdfOptions` array on `convertUrlToPdf`, `convertDocument`, `convertToPdf` (grayscale only), `convertWebsiteToPdf`, and V2 `perceive` with a `pdf` output. The SDK serializes camelCase keys to the API's wire format, and sends only the keys you set.

```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',
]);
```

| Field | Type | Description |
|-------|------|-------------|
| `pageSize` | `string` | `A0` through `A6`, `B0` through `B5`, `Letter`, `Legal`, `Tabloid`, `Ledger`. Ignored when both `pageWidth` and `pageHeight` are set. |
| `pageWidth`, `pageHeight` | `float` | Custom page size in millimetres. Set both together. |
| `orientation` | `string` | `portrait` or `landscape`. |
| `margins` | `array` | `['top' => ..., 'bottom' => ..., 'left' => ..., 'right' => ...]` in millimetres. |
| `scale` | `float` | Render scale, `0.1` to `2.0`. Paginated output only. |
| `grayscale` | `bool` | Post-process the PDF to grayscale. |
| `header` | `array` | `['content' => '<html>', 'height' => 15]`. Height in millimetres. |
| `footer` | `array` | Same shape as `header`. |

Header and footer content supports the `{{page}}`, `{{total_pages}}`, `{{date}}`, `{{title}}`, and `{{url}}` template variables. The full parameter reference lives in [parameters and options](/docs/parameters-options).

---

## Error handling

Everything the SDK throws descends from `Enconvert\Exception\EnconvertException`, so one catch block can bound the whole surface.

```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());
}
```

| Class | Raised on | Status code |
|-------|-----------|-------------|
| `AuthenticationException` | Invalid, missing, or revoked API key | `401` and `403` responses |
| `QuotaException` | HTTP 402 | `402` |
| `RateLimitException` | Too many requests | `429` |
| `ApiException` | Any other 4xx or 5xx response | the actual code |
| `EnconvertException` | Base class, and every client-side failure | none |

`ApiException::getStatusCode()` returns the numeric status; the message is prefixed with `[code]`. A `403` response maps to `AuthenticationException`, whose `getStatusCode()` reports `401`, so branch on the class rather than the number when you need to distinguish the two.

`EnconvertException` is also thrown without any network round trip when the SDK can tell the request is doomed: an empty API key, a missing file path, an unrecognized file extension, an unimplemented conversion pair, a `distill` call without a schema or with both `urls` and `discoverFrom`, an `ingest` mode and payload mismatch, an empty `ingestFiles` list, a `perceiveDirect` call that does not name exactly one artifact output, an `updateWatcher` call with no fields, and any Guzzle transport failure (surfaced as `HTTP request failed: ...`).

The full message map is in the [error codes reference](/docs/error-codes).

---

## Timeout recovery

Large document conversions and slow pages can outlive a reverse-proxy timeout even when the conversion eventually succeeds on the server. The SDK recovers from that on its own:

1. Before each conversion request it generates a 32-character hex job id and sends it as `job_id` in the body or the multipart form.
2. If that request comes back 5xx, the SDK stops raising and starts polling `GET /v1/convert/status/{job_id}` every 3 seconds.
3. The moment the job reads `success`, the SDK returns the result. If it reads `failed`, it throws `ApiException` carrying the server's error message.
4. A `404` during polling means "not recorded yet" and the poll continues. The deadline is 5 minutes, after which it throws `ApiException(504, 'Conversion timed out')`.

There is no code to write for this. Two deliberate exceptions: the website batch submissions (`convertWebsiteToPdf`, `convertWebsiteToScreenshot`) have no per-job row, so a 5xx there surfaces immediately, and V2 methods do not use job polling at all.

Successful responses that omit `job_id` are backfilled with the client-generated id, so `$result->jobId` is always usable with `getJobStatus()`.

---

## Configuration

```php
use Enconvert\Client;

$client = new Client(getenv('ENCONVERT_API_KEY'), [
    'timeout' => 300,                            // seconds
    'base_url' => 'https://api.enconvert.com',   // override for a self-hosted gateway
]);
```

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `$apiKey` (first argument) | `string` | required | Private API key (`sk_live_...`). An empty string throws `EnconvertException`. |
| `timeout` | `int\|float` | `300` | Request timeout in **seconds**, the idiomatic Guzzle unit. Applies to every HTTP request the client makes. The job-polling deadline is separate and fixed at 300 seconds. |
| `base_url` | `string` | `https://api.enconvert.com` | API host. Trailing slashes are stripped. |

Note the option key is snake_case `base_url` while every request option elsewhere in the SDK is camelCase. Requests are authenticated with an `X-API-Key` header; `saveTo` downloads go straight to storage and deliberately send no key, because the URL is already signed.

<div class="alert alert-warning">
<strong>Never hardcode the API key.</strong> Read it from an environment variable or your secret manager, and keep it out of version control and out of anything that ships to a browser. Generate and rotate keys in the <a href="/dashboard">dashboard</a>, and see the <a href="/docs/authentication">authentication guide</a> for key types.
</div>

---

## Result shape

Every single-file conversion returns an `Enconvert\Model\ConversionResult` with readonly public properties:

```php
final class ConversionResult
{
    public readonly string $presignedUrl;              // signed download URL
    public readonly string $objectKey;                 // storage object key
    public readonly string $filename;                  // server-side filename
    public readonly int|float|null $fileSize;          // bytes
    public readonly int|float|null $conversionTimeSeconds;
    public readonly ?string $jobId;                    // the id used for timeout recovery
}
```

Presigned URLs expire after 15 minutes and can be fetched more than once before they do. For permanent access, download the bytes (pass `saveTo`, or fetch the URL yourself) and store them in your own bucket.

The other result types follow the same pattern: `JobStatus`, `BatchSubmission`, `BatchStatus` with its `BatchItem[]`, and, under `Enconvert\Model\V2`, `PerceiveResult`, `PerceiveDirectResult`, `PerceiveBatchResult`, `OutputArtifact`, `DiscoverResult`, `LookupResult`, `LookupItem`, `DistillResult`, `DistillItem`, `IngestJob`, `IngestJobList`, `IngestJobSummary`, `Watcher`, `WatcherList`, `WatcherSummary`, `WatcherSnapshot`, `WatcherSnapshotList`, `WebhookSecret`, `WebhookRetryResult`, and `Tokens`. Wire fields arrive in snake_case and are mapped to camelCase properties; user-supplied payloads such as schemas, extracted data, tracked fields, and diff entries pass through untouched.

---

## Source and issues

- **Packagist:** [enconvert/enconvert-php](https://packagist.org/packages/enconvert/enconvert-php)
- **GitHub:** [conversionapi/php-sdk](https://github.com/conversionapi/php-sdk) · [open an issue](https://github.com/conversionapi/php-sdk/issues)
- **License:** MIT
- **Other clients:** [all SDKs](/docs/sdks) · [REST endpoints](/docs/endpoints-overview)

---

## Frequently asked questions

### How do I convert files in PHP with Composer?

Run `composer require enconvert/enconvert-php`, construct `new Enconvert\Client($apiKey)`, and call a method such as `convertUrlToPdf`, `convertImage`, `convertDocument`, `convertToPdf`, or `convertToMarkdown`. Add `saveTo` to any of them and the SDK streams the converted bytes to that local path for you, creating parent directories along the way.

### How do I convert DOCX to PDF in PHP?

Call `$client->convertDocument('report.docx', ['saveTo' => 'report.pdf'])`. The output format defaults to `pdf`, so no `outputFormat` is needed. The input format is read from the file extension, and the SDK checks the `doc-to-pdf` pair against its table of 43 implemented conversions before sending anything, so an unsupported pair fails instantly with a message listing what is valid for that input.

### How do I convert a URL to PDF in PHP?

Call `$client->convertUrlToPdf('https://example.com', ['saveTo' => 'page.pdf'])`. By default the page renders as one continuous page at a 1920x1080 viewport with media loading and scrolling enabled. Set `singlePage` to `false` and pass `pdfOptions` when you want real pagination with a page size, margins, headers, and footers.

### How do I scrape a web page in PHP and get clean Markdown?

Use the V2 namespace: `$client->v2->perceive($url, ['outputs' => ['markdown', 'structured']])`. It renders the page in a real browser, so JavaScript-heavy sites work, and returns a signed Markdown URL plus inline structured data. Check `renderQuality` on the result before trusting the content; a low score with populated `deductions` means the render hit an anti-bot page, a login wall, or an empty shell. For a lighter one-shot path with no V2 features, `convertUrlToMarkdown` also works.

### How do I convert HEIC to WebP in PHP?

Call `$client->convertImage('photo.heic', ['outputFormat' => 'webp', 'saveTo' => 'photo.webp'])`. Any pair among `jpeg`, `png`, `svg`, `heic`, and `webp` is supported, plus `pdf` to `jpeg` rasterization. You can also pass raw bytes as `['data' => $bytes, 'filename' => 'photo.heic']`, where the filename is what resolves the input format and MIME type.

### Does the EnConvert PHP SDK work with Laravel or Symfony?

Yes. The package is a plain PSR-4 library with Guzzle 7 as its only dependency and no framework coupling, so it drops into Laravel, Symfony, WordPress, or a bare script unchanged. Bind `Enconvert\Client` in your container with the key from your environment configuration and inject it wherever you need it.

### What happens when a conversion takes longer than the HTTP timeout?

The SDK recovers on its own. It sends a client-generated `job_id` with every conversion request, and if the request returns 5xx it silently polls `GET /v1/convert/status/{job_id}` every 3 seconds for up to 5 minutes, returning the result as soon as the job records `success`. Past the deadline it throws `ApiException(504, 'Conversion timed out')`. Website batch submissions opt out of this, since they have no per-job row.

### How long is the presigned download URL valid?

Presigned download URLs expire after 15 minutes and can be used more than once before they do. V2 artifact URLs carry an `expiresIn` of 900 seconds for the same reason, and `getPerceiveOperation($operationId)` re-signs them from the stored object keys without re-rendering the page. For anything permanent, download the bytes and keep them in your own storage.

### Which PHP version does the SDK require?

PHP 8.1 or newer. The SDK uses readonly properties, enum-style union types, named arguments, and `match` expressions throughout, so 8.0 and earlier are not supported. Its only runtime dependency is `guzzlehttp/guzzle ^7.8`.
