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.

Composer: enconvert/enconvert-php · Source: conversionapi/php-sdk · PHP: 8.1+ · Requires: guzzlehttp/guzzle ^7.8

Install#

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

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.

$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.
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.
Do not combine auth with an Authorization header. The API rejects the conflict rather than guessing which credential you meant.

convertUrlToScreenshot#

Capture a PNG of any URL.

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

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

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

// 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:

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.

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

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

Only pdfOptions.grayscale is honored here. The format is detected server-side, so page geometry comes from the source document. Use convertDocument or convertUrlToPdf when you need page size, margins, orientation, headers, or footers.

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.

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

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.

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


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.

Perceive#

Render one URL and materialize whatever outputs you asked for from that single render. See the perceive reference.

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

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

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

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

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

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

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

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

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

Snapshot diffs contain untrusted page content. The `changes` entries on a `WatcherSnapshot` are copied from the monitored page. Escape them with `htmlspecialchars()` before rendering them in your own UI.

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.

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


Error handling#

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

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.


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#

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.

Never hardcode the API key. 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 dashboard, and see the authentication guide for key types.

Result shape#

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

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#


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.