Invalid Date

When generating PDFs in PHP, DomPDF (laravel-dompdf) and wkhtmltopdf (barryvdh/laravel-snappy) have long been the go-to options. But each comes with frustrating limitations. DomPDF has narrow CSS support — flexbox and grid simply don't work. Snappy requires installing the wkhtmltopdf binary, making it awkward in Docker images and impossible in serverless environments. And running Chromium on your own servers carries a non-trivial resource cost.

FUNBREW PDF API lets you send HTML with Laravel's Http Facade and get back a high-quality PDF. Chromium-equivalent rendering, full CSS support, Japanese fonts included — and zero binaries on your server.

This guide walks through integrating FUNBREW PDF API into a Laravel project: service class design, Blade template rendering, Queue-based async processing, and testing with Http::fake(). For other languages and frameworks, see the language quickstart guide. For error handling patterns, check the error handling guide.

Why Use an API Instead of a Library?

DomPDF's Limitations

DomPDF runs natively in PHP with minimal dependencies, which is why it's popular. But as of 2024, it still has these constraints:

Feature DomPDF FUNBREW PDF API
Flexbox / Grid Not supported Fully supported
Modern CSS (custom properties, etc.) Partial Fully supported
Japanese fonts Manual setup required Noto Sans JP built-in
Complex tables Layout issues common Faithfully rendered
Rendering engine PHP internal implementation Chromium-equivalent

If your design uses flex or grid layouts, DomPDF will require a significant rewrite of your templates.

Snappy's (wkhtmltopdf) Operational Cost

Snappy depends on the wkhtmltopdf binary, which introduces several problems:

  • Bloated Docker images: Adding wkhtmltopdf and its dependencies (libX11, etc.) adds 100+ MB to your image
  • Alpine Linux incompatibility: wkhtmltopdf requires glibc, making Alpine setups complex
  • Unusable in serverless environments: Lambda, Cloud Run, and similar platforms restrict binary executables
  • Project is abandoned: wkhtmltopdf was archived in 2023 and is no longer maintained

With an API approach, you eliminate binaries from your application server entirely. Your Docker image stays lean, and serverless environments work without any special configuration.

Lower Server Resource Usage

DomPDF and Snappy both execute PDF generation on your application server. For complex HTML or high-volume generation, CPU and memory consumption becomes noticeable and can affect other requests.

With an API approach, the heavy lifting happens on the external service. Your Laravel application only sends an HTTP request, letting you keep instance sizes small and making scaling more predictable.

Setting Up Your Laravel Project

Get Your API Key

Create a free account and generate an API key from the dashboard.

# Add to your .env file
FUNBREW_PDF_API_KEY="sk-your-api-key"
FUNBREW_PDF_API_URL="https://api.pdf.funbrew.cloud/v1/pdf/from-html"

For secure key management best practices, see the security guide.

Add Configuration to config/services.php

Following Laravel conventions, external service settings belong in config/services.php.

// config/services.php
return [
    // existing config...

    'funbrew_pdf' => [
        'api_key' => env('FUNBREW_PDF_API_KEY'),
        'api_url' => env('FUNBREW_PDF_API_URL', 'https://api.pdf.funbrew.cloud/v1/pdf/from-html'),
        'timeout'  => env('FUNBREW_PDF_TIMEOUT', 60),
    ],
];

No Additional Packages Required

Laravel ships with an HTTP client (Http Facade) built on Guzzle, so there's nothing extra to install. Laravel 7+ is all you need.

# Already available in Laravel 7+
# php artisan --version

Service Class Design

Keep PDF generation logic out of your controllers by centralizing it in a dedicated service class. This improves testability and makes it easy to swap API providers in the future.

app/Services/PdfService.php

<?php

namespace App\Services;

use Illuminate\Http\Client\RequestException;
use Illuminate\Support\Facades\Http;
use RuntimeException;

class PdfService
{
    private string $apiKey;
    private string $apiUrl;
    private int $timeout;

    public function __construct()
    {
        $this->apiKey = config('services.funbrew_pdf.api_key');
        $this->apiUrl = config('services.funbrew_pdf.api_url');
        $this->timeout = (int) config('services.funbrew_pdf.timeout', 60);
    }

    /**
     * Generate a PDF from an HTML string and return the binary content.
     *
     * @param  string               $html    HTML content to convert
     * @param  array<string, mixed> $options API options (format, engine, etc.)
     * @return string               PDF binary
     *
     * @throws RuntimeException When PDF generation fails
     */
    public function generateFromHtml(string $html, array $options = []): string
    {
        $payload = array_merge([
            'html'   => $html,
            'format' => 'A4',
            'engine' => 'quality',
        ], $options);

        $response = Http::withToken($this->apiKey)
            ->timeout($this->timeout)
            ->post($this->apiUrl, $payload);

        if ($response->failed()) {
            throw new RuntimeException(
                "PDF generation failed: HTTP {$response->status()} — {$response->body()}"
            );
        }

        return $response->body();
    }
}

Register with the Service Container

Register as a singleton to avoid re-instantiation on every request.

// app/Providers/AppServiceProvider.php
use App\Services\PdfService;

public function register(): void
{
    $this->app->singleton(PdfService::class, function () {
        return new PdfService();
    });
}

PDF Generation from Blade Templates

Laravel's Blade template engine integrates naturally with the PDF service. Render an existing Blade view to an HTML string, then pass it to the API.

Create the Blade Template

Create a dedicated Blade template for PDF output. Unlike web pages, load external fonts via CDN.

{{-- resources/views/pdf/invoice.blade.php --}}
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <link
    href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap"
    rel="stylesheet"
  >
  <style>
    * { box-sizing: border-box; margin: 0; padding: 0; }
    body {
      font-family: 'Inter', sans-serif;
      font-size: 14px;
      color: #1a1a1a;
      padding: 40px;
      line-height: 1.6;
    }
    .header {
      display: flex;
      justify-content: space-between;
      align-items: flex-start;
      border-bottom: 3px solid #1a56db;
      padding-bottom: 20px;
      margin-bottom: 32px;
    }
    .header h1 {
      font-size: 28px;
      color: #1a56db;
    }
    .meta { text-align: right; color: #4b5563; font-size: 13px; }
    .meta strong { color: #1a1a1a; font-size: 15px; }
    .bill-to {
      background: #f9fafb;
      border-radius: 8px;
      padding: 16px 20px;
      margin-bottom: 32px;
    }
    .bill-to h2 {
      font-size: 12px;
      color: #6b7280;
      margin-bottom: 8px;
      letter-spacing: 0.05em;
      text-transform: uppercase;
    }
    table {
      width: 100%;
      border-collapse: collapse;
      margin-bottom: 24px;
    }
    th {
      background: #1a56db;
      color: #fff;
      padding: 10px 12px;
      text-align: left;
      font-weight: 600;
    }
    td { padding: 10px 12px; border-bottom: 1px solid #e5e7eb; }
    tr:last-child td { border-bottom: none; }
    .amount { text-align: right; }
    .total-row {
      display: flex;
      justify-content: flex-end;
      gap: 40px;
      font-size: 16px;
      font-weight: 700;
      border-top: 2px solid #1a1a1a;
      padding-top: 16px;
    }
    .footer { margin-top: 48px; font-size: 12px; color: #9ca3af; }
  </style>
</head>
<body>
  <div class="header">
    <div>
      <h1>Invoice</h1>
    </div>
    <div class="meta">
      <strong>#{{ $invoice['number'] }}</strong><br>
      Issued: {{ $invoice['issued_at'] }}<br>
      Due: {{ $invoice['due_at'] }}
    </div>
  </div>

  <div class="bill-to">
    <h2>Bill To</h2>
    <p style="font-weight:600">{{ $customer['name'] }}</p>
    <p>{{ $customer['address'] }}</p>
    <p>{{ $customer['email'] }}</p>
  </div>

  <table>
    <thead>
      <tr>
        <th>Description</th>
        <th>Qty</th>
        <th>Unit Price</th>
        <th class="amount">Amount</th>
      </tr>
    </thead>
    <tbody>
      @foreach ($invoice['items'] as $item)
      <tr>
        <td>{{ $item['name'] }}</td>
        <td>{{ $item['quantity'] }}</td>
        <td>${{ number_format($item['unit_price'], 2) }}</td>
        <td class="amount">${{ number_format($item['quantity'] * $item['unit_price'], 2) }}</td>
      </tr>
      @endforeach
    </tbody>
  </table>

  <div class="total-row">
    <span>Total</span>
    <span>${{ number_format($invoice['total'], 2) }}</span>
  </div>

  <div class="footer">
    <p>{{ $company['name'] }} — {{ $company['address'] }} — {{ $company['email'] }}</p>
  </div>
</body>
</html>

Controller Implementation

<?php

namespace App\Http\Controllers;

use App\Services\PdfService;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\View;
use RuntimeException;

class InvoiceController extends Controller
{
    public function __construct(private readonly PdfService $pdfService)
    {
    }

    /**
     * Download invoice as a PDF
     */
    public function download(int $invoiceId): Response
    {
        // In practice, fetch this from your database
        $data = [
            'invoice' => [
                'number'    => "INV-{$invoiceId}",
                'issued_at' => now()->format('F j, Y'),
                'due_at'    => now()->addDays(30)->format('F j, Y'),
                'items'     => [
                    ['name' => 'FUNBREW PDF Pro Plan', 'quantity' => 1, 'unit_price' => 49.00],
                    ['name' => 'Additional API Calls (500)', 'quantity' => 2, 'unit_price' => 20.00],
                ],
                'total' => 89.00,
            ],
            'customer' => [
                'name'    => 'Acme Corp',
                'address' => '123 Main St, San Francisco, CA 94105',
                'email'   => 'billing@acme.com',
            ],
            'company' => [
                'name'    => 'Your Company',
                'address' => '456 Market St, San Francisco, CA 94105',
                'email'   => 'hello@yourcompany.com',
            ],
        ];

        // Render the Blade template to an HTML string
        $html = View::make('pdf.invoice', $data)->render();

        try {
            $pdfBytes = $this->pdfService->generateFromHtml($html);
        } catch (RuntimeException $e) {
            abort(502, 'PDF generation failed. Please try again shortly.');
        }

        return response($pdfBytes, 200, [
            'Content-Type'        => 'application/pdf',
            'Content-Disposition' => "attachment; filename=\"invoice-{$invoiceId}.pdf\"",
        ]);
    }
}

Routes

// routes/web.php
use App\Http\Controllers\InvoiceController;

Route::middleware('auth')->group(function () {
    Route::get('/invoices/{invoice}/pdf', [InvoiceController::class, 'download'])
        ->name('invoices.pdf');
});

For tips on template design, see the template engine guide.

Extending the Service Class

In production, different PDF types often need different settings. Add shortcut methods to the service class to keep callsites clean.

<?php

namespace App\Services;

use Illuminate\Support\Facades\Http;
use RuntimeException;

class PdfService
{
    private string $apiKey;
    private string $apiUrl;
    private int $timeout;

    public function __construct()
    {
        $this->apiKey = config('services.funbrew_pdf.api_key');
        $this->apiUrl = config('services.funbrew_pdf.api_url');
        $this->timeout = (int) config('services.funbrew_pdf.timeout', 60);
    }

    /**
     * Generic: generate PDF from HTML
     */
    public function generateFromHtml(string $html, array $options = []): string
    {
        $payload = array_merge([
            'html'   => $html,
            'format' => 'A4',
            'engine' => 'quality',
        ], $options);

        $response = Http::withToken($this->apiKey)
            ->timeout($this->timeout)
            ->post($this->apiUrl, $payload);

        if ($response->failed()) {
            throw new RuntimeException(
                "PDF generation failed: HTTP {$response->status()} — {$response->body()}"
            );
        }

        return $response->body();
    }

    /**
     * Invoice PDF (A4 portrait, quality engine)
     */
    public function generateInvoice(string $html): string
    {
        return $this->generateFromHtml($html, [
            'format'      => 'A4',
            'orientation' => 'portrait',
            'engine'      => 'quality',
        ]);
    }

    /**
     * Report PDF (A4 landscape, speed engine)
     */
    public function generateReport(string $html): string
    {
        return $this->generateFromHtml($html, [
            'format'      => 'A4',
            'orientation' => 'landscape',
            'engine'      => 'speed',
        ]);
    }

    /**
     * Certificate PDF (Letter landscape)
     */
    public function generateCertificate(string $html): string
    {
        return $this->generateFromHtml($html, [
            'format'      => 'Letter',
            'orientation' => 'landscape',
            'engine'      => 'quality',
        ]);
    }
}

For a broader look at use cases like invoices and certificates, see the use cases page.

Async PDF Generation with Laravel Queue

Making users wait for HTTP responses during PDF generation hurts UX, especially at scale. Use Laravel Queue to push generation to the background.

Create the Job Class

php artisan make:job GeneratePdfJob
<?php

namespace App\Jobs;

use App\Services\PdfService;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\View;
use RuntimeException;

class GeneratePdfJob implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    /**
     * Maximum number of retries
     */
    public int $tries = 3;

    /**
     * Timeout in seconds
     */
    public int $timeout = 90;

    /**
     * Retry backoff in seconds
     */
    public array $backoff = [10, 30, 60];

    public function __construct(
        private readonly string $template,
        private readonly array  $data,
        private readonly string $storagePath,
        private readonly array  $options = [],
    ) {
    }

    public function handle(PdfService $pdfService): void
    {
        $html = View::make($this->template, $this->data)->render();

        $pdfBytes = $pdfService->generateFromHtml($html, $this->options);

        Storage::put($this->storagePath, $pdfBytes);
    }

    public function failed(\Throwable $e): void
    {
        \Log::error('PDF generation job failed', [
            'template'    => $this->template,
            'storagePath' => $this->storagePath,
            'error'       => $e->getMessage(),
        ]);
    }
}

Dispatching the Job

<?php

namespace App\Http\Controllers;

use App\Jobs\GeneratePdfJob;
use Illuminate\Http\JsonResponse;

class ReportController extends Controller
{
    public function generateAsync(int $reportId): JsonResponse
    {
        $data = [
            'report_id' => $reportId,
            'title'     => 'Monthly Report',
            'metrics'   => $this->getMetrics($reportId),
        ];

        $storagePath = "reports/report-{$reportId}-" . now()->format('Ymd') . '.pdf';

        GeneratePdfJob::dispatch(
            template:    'pdf.report',
            data:        $data,
            storagePath: $storagePath,
        )->onQueue('pdf');

        return response()->json([
            'status'       => 'processing',
            'storage_path' => $storagePath,
            'message'      => 'PDF generation started. Check back shortly to download.',
        ], 202);
    }

    private function getMetrics(int $reportId): array
    {
        return [
            ['name' => 'PDFs Generated', 'value' => '1,234'],
            ['name' => 'Avg Response Time', 'value' => '0.8s'],
            ['name' => 'Success Rate', 'value' => '99.7%'],
        ];
    }
}

Queue Configuration

Redis is recommended for production.

// config/queue.php (Redis section)
'redis' => [
    'driver'      => 'redis',
    'connection'  => 'default',
    'queue'       => env('REDIS_QUEUE', 'default'),
    'retry_after' => 120,
    'block_for'   => null,
],
# .env (production)
QUEUE_CONNECTION=redis

# Start a dedicated worker for the PDF queue
php artisan queue:work redis --queue=pdf --tries=3 --timeout=90

For high-volume generation patterns, see the batch processing guide.

Downloading the Generated PDF

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Response;
use Illuminate\Support\Facades\Storage;

class ReportController extends Controller
{
    public function download(int $reportId): Response
    {
        $storagePath = "reports/report-{$reportId}-" . now()->format('Ymd') . '.pdf';

        if (! Storage::exists($storagePath)) {
            abort(404, 'PDF not ready yet. Please try again shortly.');
        }

        return response(Storage::get($storagePath), 200, [
            'Content-Type'        => 'application/pdf',
            'Content-Disposition' => "attachment; filename=\"report-{$reportId}.pdf\"",
        ]);
    }
}

Testing with Http::fake()

Laravel's Http::fake() lets you mock external API calls so tests run fast and never hit the real API.

Unit Test for the Service Class

<?php

namespace Tests\Unit\Services;

use App\Services\PdfService;
use Illuminate\Http\Client\Request;
use Illuminate\Support\Facades\Http;
use RuntimeException;
use Tests\TestCase;

class PdfServiceTest extends TestCase
{
    protected function setUp(): void
    {
        parent::setUp();

        config([
            'services.funbrew_pdf.api_key' => 'test-api-key',
            'services.funbrew_pdf.api_url' => 'https://api.pdf.funbrew.cloud/v1/pdf/from-html',
            'services.funbrew_pdf.timeout' => 60,
        ]);
    }

    public function test_generate_from_html_returns_pdf_binary(): void
    {
        $fakePdf = '%PDF-1.4 fake content';

        Http::fake([
            'api.pdf.funbrew.cloud/*' => Http::response($fakePdf, 200, [
                'Content-Type' => 'application/pdf',
            ]),
        ]);

        $service = new PdfService();
        $result = $service->generateFromHtml('<h1>Test</h1>');

        $this->assertSame($fakePdf, $result);
    }

    public function test_generate_from_html_sends_correct_payload(): void
    {
        Http::fake([
            'api.pdf.funbrew.cloud/*' => Http::response('%PDF-1.4', 200),
        ]);

        $service = new PdfService();
        $service->generateFromHtml('<h1>Test</h1>', ['format' => 'Letter']);

        Http::assertSent(function (Request $request) {
            return $request->url() === 'https://api.pdf.funbrew.cloud/v1/pdf/from-html'
                && $request['html'] === '<h1>Test</h1>'
                && $request['format'] === 'Letter'
                && $request->hasHeader('Authorization', 'Bearer test-api-key');
        });
    }

    public function test_generate_from_html_throws_on_api_error(): void
    {
        Http::fake([
            'api.pdf.funbrew.cloud/*' => Http::response('Internal Server Error', 500),
        ]);

        $this->expectException(RuntimeException::class);
        $this->expectExceptionMessageMatches('/PDF generation failed/');

        $service = new PdfService();
        $service->generateFromHtml('<h1>Test</h1>');
    }

    public function test_generate_invoice_uses_portrait_orientation(): void
    {
        Http::fake([
            'api.pdf.funbrew.cloud/*' => Http::response('%PDF-1.4', 200),
        ]);

        $service = new PdfService();
        $service->generateInvoice('<h1>Invoice</h1>');

        Http::assertSent(function (Request $request) {
            return $request['orientation'] === 'portrait'
                && $request['engine'] === 'quality';
        });
    }
}

Feature Test for the Controller

<?php

namespace Tests\Feature;

use Illuminate\Support\Facades\Http;
use Tests\TestCase;

class InvoiceControllerTest extends TestCase
{
    public function test_download_returns_pdf_response(): void
    {
        $fakePdf = '%PDF-1.4 fake invoice content';

        Http::fake([
            'api.pdf.funbrew.cloud/*' => Http::response($fakePdf, 200, [
                'Content-Type' => 'application/pdf',
            ]),
        ]);

        $this->actingAs($this->createUser())
            ->get('/invoices/1/pdf')
            ->assertStatus(200)
            ->assertHeader('Content-Type', 'application/pdf')
            ->assertHeader('Content-Disposition', 'attachment; filename="invoice-1.pdf"');
    }

    public function test_download_returns_502_on_api_failure(): void
    {
        Http::fake([
            'api.pdf.funbrew.cloud/*' => Http::response('Service Unavailable', 503),
        ]);

        $this->actingAs($this->createUser())
            ->get('/invoices/1/pdf')
            ->assertStatus(502);
    }

    public function test_download_requires_authentication(): void
    {
        $this->get('/invoices/1/pdf')
            ->assertRedirect('/login');
    }
}

Job Test

<?php

namespace Tests\Unit\Jobs;

use App\Jobs\GeneratePdfJob;
use App\Services\PdfService;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Storage;
use Tests\TestCase;

class GeneratePdfJobTest extends TestCase
{
    public function test_handle_stores_pdf_to_storage(): void
    {
        Storage::fake('local');

        Http::fake([
            'api.pdf.funbrew.cloud/*' => Http::response('%PDF-1.4 fake', 200),
        ]);

        $job = new GeneratePdfJob(
            template:    'pdf.invoice',
            data:        ['invoice' => ['number' => 'INV-001', 'items' => [], 'total' => 0]],
            storagePath: 'reports/test.pdf',
        );

        $job->handle(app(PdfService::class));

        Storage::assertExists('reports/test.pdf');
    }
}

Production Tips

Retries and Timeouts

HTTP APIs can fail transiently. Use Laravel's Http::retry() to handle temporary failures automatically.

// app/Services/PdfService.php

public function generateFromHtml(string $html, array $options = []): string
{
    $payload = array_merge([
        'html'   => $html,
        'format' => 'A4',
        'engine' => 'quality',
    ], $options);

    $response = Http::withToken($this->apiKey)
        ->timeout($this->timeout)
        ->retry(
            times: 3,
            sleepMilliseconds: 500,
            when: fn (\Throwable $e) => ! ($e instanceof \Illuminate\Http\Client\ConnectionException),
        )
        ->post($this->apiUrl, $payload);

    if ($response->failed()) {
        throw new RuntimeException(
            "PDF generation failed: HTTP {$response->status()} — {$response->body()}"
        );
    }

    return $response->body();
}

PDF Caching

If you generate the same PDF repeatedly from unchanged data, cache the result to reduce API calls.

use Illuminate\Support\Facades\Cache;

public function getCachedPdf(string $cacheKey, string $html, array $options = []): string
{
    return Cache::remember(
        "pdf:{$cacheKey}",
        now()->addHours(24),
        fn () => $this->generateFromHtml($html, $options)
    );
}

Monitoring with Laravel Horizon

In production, use Laravel Horizon to visualize and monitor your PDF queue.

composer require laravel/horizon
php artisan horizon:install
// config/horizon.php (dedicated PDF supervisor)
'environments' => [
    'production' => [
        'supervisor-pdf' => [
            'maxProcesses'  => 5,
            'queue'         => ['pdf'],
            'balance'       => 'auto',
            'minProcesses'  => 1,
            'memory'        => 128,
            'tries'         => 3,
            'timeout'       => 90,
        ],
    ],
],

Logging and Observability

Log PDF generation performance to catch slowdowns early.

use Illuminate\Support\Facades\Log;

public function generateFromHtml(string $html, array $options = []): string
{
    $startedAt = microtime(true);

    $response = Http::withToken($this->apiKey)
        ->timeout($this->timeout)
        ->post($this->apiUrl, array_merge([
            'html'   => $html,
            'format' => 'A4',
            'engine' => 'quality',
        ], $options));

    $elapsed = round((microtime(true) - $startedAt) * 1000);

    if ($response->failed()) {
        Log::error('PDF generation failed', [
            'status'  => $response->status(),
            'elapsed' => "{$elapsed}ms",
        ]);

        throw new RuntimeException(
            "PDF generation failed: HTTP {$response->status()} — {$response->body()}"
        );
    }

    Log::info('PDF generated', [
        'size'    => strlen($response->body()),
        'elapsed' => "{$elapsed}ms",
    ]);

    return $response->body();
}

For comprehensive production guidance, see the production operations guide.

DomPDF vs Snappy vs FUNBREW PDF API

Aspect DomPDF (laravel-dompdf) Snappy (wkhtmltopdf) FUNBREW PDF API
Setup Composer only Binary install required API key only
CSS Support No flexbox/grid, CSS2-level CSS2.1, partial CSS3 Full modern CSS, Chromium-equivalent
Japanese/CJK fonts Manual config required Manual config required Noto Sans JP built-in
Docker Easy Image bloat Easy (external HTTP call only)
Serverless Works Difficult Fully supported
Server load Runs on your server Runs on your server Offloaded to external service
Maintenance Active Abandoned (2023) Active
Cost Free Free Monthly subscription (pay-as-you-go available)

When to choose what:

  • DomPDF: Simple internal PDFs where CSS layout doesn't matter much
  • Snappy: Existing projects already using it, non-Docker environments (but consider migrating)
  • FUNBREW PDF API: Customer-facing high-quality PDFs, Docker environments, serverless, or CJK content

Summary

Connecting Laravel to FUNBREW PDF API takes only a few dozen lines using the Http Facade. Apply the design patterns in this guide to build PDF generation that's easy to test and reliable in production.

  1. Centralize in a service class — Encapsulate PDF logic in PdfService, keep controllers thin
  2. Leverage Blade templates — Use View::make()->render() to generate HTML and reuse existing views
  3. Go async with Queue — Dispatch GeneratePdfJob to keep users from waiting on HTTP responses
  4. Test with Http::fake() — Mock external API calls for fast, stable tests
  5. Add retries and logging — Handle transient failures gracefully and make performance visible

Try the Playground to see what your HTML looks like as a PDF. When you're ready to integrate, the documentation covers all available API options. Check the pricing page for the right plan.

Related Guides

Powered by FUNBREW PDF