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.
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. |
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.
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.
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:
- Before each conversion request it generates a 32-character hex job id and sends it as
job_idin the body or the multipart form. - If that request comes back 5xx, the SDK stops raising and starts polling
GET /v1/convert/status/{job_id}every 3 seconds. - The moment the job reads
success, the SDK returns the result. If it readsfailed, it throwsApiExceptioncarrying the server's error message. - A
404during polling means "not recorded yet" and the poll continues. The deadline is 5 minutes, after which it throwsApiException(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.
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#
- Packagist: enconvert/enconvert-php
- GitHub: conversionapi/php-sdk · open an issue
- License: MIT
- Other clients: all SDKs · REST endpoints
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.