Blog

PHP document conversion to Markdown

14 Sep 2026 · 7 min read


PHP powers over 75% of the web. If you're running a CMS like WordPress, Drupal, or Laravel, you've almost certainly needed to convert uploaded documents — invoices, contracts, reports, user submissions — into Markdown for search indexing, LLM processing, or content migration.

This post shows you how to integrate AnyMD document conversion into PHP projects using cURL, with proper error handling, batch processing, and patterns that drop cleanly into any PHP framework.

Why AnyMD from PHP?

PHP's built-in document handling is minimal. PhpWord reads DOCX but outputs raw text that discards structure. PDF parsing in PHP requires shelling out to pdftotext. Tika integration adds a Java dependency. AnyMD gives you a single curl call that converts any format — DOCX, PDF, PPTX, XLSX, ODT, RTF, EPUB — into clean GitHub-Flavored Markdown.

In benchmarks, a PHP (cURL) client adds ~8.1 ms of client-side overhead per conversion — slightly more than Node.js (7.2 ms) but well within tolerance for typical web request cycles. With curl_multi_exec, PHP achieves ~310 docs/minute across 10 concurrent files.

Prerequisites

You need PHP 8.1+ with the curl and json extensions enabled (both are standard in most distributions). No external libraries required — we'll use PHP's built-in cURL functions.

Installation check:

php -m | grep -E 'curl|json'

If both appear, you're ready.

Single document conversion

Here's a minimal PHP function that converts any supported document to Markdown:

<?php

class AnyMDClient
{
    private string $apiUrl = 'https://anymd.net/api/convert';
    private string $apiKey;

    public function __construct(string $apiKey)
    {
        $this->apiKey = $apiKey;
    }

    public function convert(string $filePath): string
    {
        $ch = curl_init($this->apiUrl);

        curl_setopt_array($ch, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_POST => true,
            CURLOPT_HTTPHEADER => [
                'Authorization: Bearer ' . $this->apiKey,
            ],
            CURLOPT_POSTFIELDS => [
                'file' => new CURLFile($filePath),
            ],
            CURLOPT_TIMEOUT => 30,
        ]);

        $response = curl_exec($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        $error = curl_error($ch);
        curl_close($ch);

        if ($error) {
            throw new RuntimeException("cURL error: $error");
        }

        if ($httpCode !== 200) {
            throw new RuntimeException(
                "API error (HTTP $httpCode): $response"
            );
        }

        return $response;
    }
}

// Usage:
$client = new AnyMDClient('your-api-key');
$markdown = $client->convert('/path/to/report.docx');
file_put_contents('report.md', $markdown);

AnyMD auto-detects the input format from the file extension and returns GitHub-Flavored Markdown with headings, lists, tables, images, and links preserved.

Error handling and retries

Production code needs resilient error handling. Here's an enhanced version with exponential backoff:

<?php

class AnyMDClientWithRetry
{
    private string $apiKey;
    private int $maxRetries = 3;

    public function __construct(string $apiKey)
    {
        $this->apiKey = $apiKey;
    }

    public function convertWithRetry(string $filePath): ?string
    {
        $attempt = 0;

        while ($attempt < $this->maxRetries) {
            $ch = curl_init('https://anymd.net/api/convert');

            curl_setopt_array($ch, [
                CURLOPT_RETURNTRANSFER => true,
                CURLOPT_POST => true,
                CURLOPT_HTTPHEADER => [
                    'Authorization: Bearer ' . $this->apiKey,
                ],
                CURLOPT_POSTFIELDS => [
                    'file' => new CURLFile($filePath),
                ],
                CURLOPT_TIMEOUT => 60,
            ]);

            $response = curl_exec($ch);
            $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
            $error = curl_error($ch);
            curl_close($ch);

            if ($error) {
                $attempt++;
                if ($attempt >= $this->maxRetries) {
                    error_log("AnyMD: failed after $attempt attempts: $error");
                    return null;
                }
                sleep(pow(2, $attempt));
                continue;
            }

            // Rate-limited — wait and retry
            if ($httpCode === 429) {
                $retryAfter = 5;
                preg_match('/Retry-After: (\d+)/i', $response, $m);
                if (!empty($m[1])) {
                    $retryAfter = (int)$m[1];
                }
                sleep($retryAfter);
                $attempt++;
                continue;
            }

            if ($httpCode !== 200) {
                error_log("AnyMD: HTTP $httpCode on $filePath: $response");
                return null;
            }

            return $response;
        }

        return null;
    }
}

Batch processing with curl_multi

When you need to convert many files — migrating a CMS, processing uploads, or building a training dataset — sequential conversion is too slow. PHP's curl_multi_exec handles concurrent requests efficiently:

<?php

class AnyMDBatchConverter
{
    private string $apiKey;

    public function __construct(string $apiKey)
    {
        $this->apiKey = $apiKey;
    }

    /**
     * Convert multiple files concurrently.
     * @param string[] $files Map of output paths => input paths
     * @return array<string, string> Map of output paths => markdown
     */
    public function convertBatch(array $files): array
    {
        $mh = curl_multi_init();
        $handles = [];
        $results = [];

        foreach ($files as $outPath => $inPath) {
            $ch = curl_init('https://anymd.net/api/convert');
            curl_setopt_array($ch, [
                CURLOPT_RETURNTRANSFER => true,
                CURLOPT_POST => true,
                CURLOPT_HTTPHEADER => [
                    'Authorization: Bearer ' . $this->apiKey,
                ],
                CURLOPT_POSTFIELDS => [
                    'file' => new CURLFile($inPath),
                ],
                CURLOPT_TIMEOUT => 60,
            ]);
            curl_multi_add_handle($mh, $ch);
            $handles[(int)$ch] = ['ch' => $ch, 'out' => $outPath];
        }

        $running = 0;
        do {
            curl_multi_exec($mh, $running);
            curl_multi_select($mh);
        } while ($running > 0);

        foreach ($handles as $id => $info) {
            $response = curl_multi_getcontent($info['ch']);
            $httpCode = curl_getinfo($info['ch'], CURLINFO_HTTP_CODE);

            if ($httpCode === 200 && $response !== false) {
                $results[$info['out']] = $response;
            } else {
                error_log("AnyMD batch: $httpCode on {$info['out']}");
            }

            curl_multi_remove_handle($mh, $info['ch']);
            curl_close($info['ch']);
        }

        curl_multi_close($mh);
        return $results;
    }
}

// Convert 20 files at once:
$converter = new AnyMDBatchConverter('your-api-key');
$files = [];
foreach (glob('/path/to/documents/*.docx') as $i => $doc) {
    $outPath = "/path/to/output/doc_{$i}.md";
    $files[$outPath] = $doc;
}
$results = $converter->convertBatch($files);
echo "Converted " . count($results) . " files\n";

WordPress integration

If you're on WordPress, you can hook into upload processing to auto-convert documents:

<?php
/**
 * Auto-convert uploaded DOCX/PDF files to Markdown
 * and store the result as post meta.
 */
add_action('add_attachment', function (int $postId) {
    $file = get_attached_file($postId);
    $ext = strtolower(pathinfo($file, PATHINFO_EXTENSION));

    $supported = ['docx', 'pdf', 'pptx', 'xlsx', 'odt', 'rtf', 'epub'];
    if (!in_array($ext, $supported, true)) {
        return;
    }

    $apiKey = defined('ANYMD_API_KEY') ? ANYMD_API_KEY : '';
    if (empty($apiKey)) {
        return;
    }

    try {
        $client = new AnyMDClient($apiKey);
        $markdown = $client->convert($file);
        update_post_meta($postId, '_anymd_markdown', $markdown);
        update_post_meta($postId, '_anymd_converted', time());
    } catch (Throwable $e) {
        error_log("AnyMD WordPress: {$e->getMessage()}");
    }
});

This gives you searchable Markdown for every uploaded document — perfect for building a RAG knowledge base from your WordPress media library.

Laravel integration

For Laravel projects, here's a clean service class:

<?php

namespace App\Services;

use Illuminate\Http\Client\PendingRequest;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Storage;

class AnyMDService
{
    private PendingRequest $client;

    public function __construct()
    {
        $this->client = Http::baseUrl('https://anymd.net/api')
            ->withToken(config('services.anymd.api_key'))
            ->timeout(60)
            ->retry(3, 1000);
    }

    public function convert(string $disk, string $path): ?string
    {
        $fullPath = Storage::disk($disk)->path($path);
        $filename = basename($path);

        $response = $this->client->attach(
            'file', file_get_contents($fullPath), $filename
        )->post('/convert');

        if ($response->successful()) {
            return $response->body();
        }

        \Log::error('AnyMD conversion failed', [
            'path' => $path,
            'status' => $response->status(),
        ]);

        return null;
    }
}

Register the service in config/services.php:

'anymd' => [
    'api_key' => env('ANYMD_API_KEY'),
],

Format support

AnyMD converts these formats to clean Markdown:

FormatExtensionsNotes
DOCX.docxFull formatting, tables, images
PDF.pdfText extraction with layout preservation
PPTX.pptxSlide content as structured sections
XLSX.xlsxTables with merged cell support
ODT.odtFull OpenDocument support
RTF.rtfRich text to clean Markdown
EPUB.epubChapter structure preserved
HTML.html, .htmSemantic normalization
CSV.csvFormatted as Markdown tables

Pricing

PlanPages/monthMax file sizePrice
Free10010 MB$0
Starter50025 MB$19
Pro5,00050 MB$99
EnterpriseCustomCustomCustom

Get your free API key — 100 pages/month, no credit card required.


← Read more →