---
title: "HTML to Image API for PHP | Official PHP SDK"
description: "Render HTML to PNG, screenshot URLs and build PDFs from PHP with the official html2img SDK. Composer install, typed requests, typed exceptions, raw cURL too."
url: "https://html2img.com/integrations/php/"
---

# HTML to Image API for PHP

The official PHP SDK wraps the HTML to Image API in a small typed client built on Guzzle: render HTML you control, screenshot a live URL, or fill a named template, each returning one response object and throwing one exception hierarchy.

- **Install:** `composer require html2img/html2img-php`
- **Registry:** [Packagist](https://packagist.org/packages/html2img/html2img-php)
- **Source:** https://github.com/html2img/html2img-php
- **Licence:** MIT
- **Requires:** PHP 8.3 or newer

The API is a plain REST service, so any PHP application can call it with cURL. The
official SDK exists because the parts that are tedious to hand-roll (building the
request body, mapping eleven status codes onto something you can `catch`, and
keeping the option ranges honest) are the same in every project. Both routes are
covered below: the SDK first, raw cURL and Guzzle at the end.

## What you can build

- **Open Graph and social images** for every page, post or product, rendered from
  a PHP template and cached as a URL on the record.
- **Invoices, receipts, credit notes and statements** as PNGs for the account
  screen, or as vector PDFs for the emailed copy through the
  [HTML to PDF API](https://html2img.com/html-to-pdf/).
- **Certificates, tickets and passes** generated on completion or purchase.
- **Product and listing thumbnails** rendered once at import and re-served from
  your own CDN.
- **Website screenshots** through the [Screenshot API](https://html2img.com/screenshot-api/), for link
  previews, directory listings or a visual record of a page.

If your data already fits one of the [named templates](https://html2img.com/templates/) you can skip
writing markup entirely and post JSON instead.

## Requirements

| Requirement | Version |
| --- | --- |
| PHP | 8.3 or newer |
| ext-curl | Required by Guzzle, which the SDK is built on |
| Guzzle | `^7.0`, installed for you by Composer |
| API key | Free, from your [dashboard](https://app.html2img.com/register) |

The SDK is framework-agnostic. On Laravel, install the
[Laravel package](https://html2img.com/integrations/laravel/) instead: it wraps this client and adds a
facade, config file and storage helpers. Statamic, Craft CMS and WordPress each
have their own integration built on the same foundation.

## Installation

```bash
composer require html2img/html2img-php
```

Keep the key in the environment, never in the repository:

```dotenv
HTML2IMG_API_KEY=your-api-key
```

The key travels as an `X-API-Key` header on every request. Issuing and rotating
keys is covered in the [authentication docs](https://html2img.com/docs/authentication/).

## Quick start

Three lines to a rendered image:

```php
<?php

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

use Html2img\Html2imgClient;
use Html2img\Request\HtmlRequest;

$client = new Html2imgClient(getenv('HTML2IMG_API_KEY'));

$response = $client->html(new HtmlRequest(
    html: '<!doctype html><html><body><h1>Hello from PHP</h1></body></html>',
    width: 1200,
    height: 630,
));

echo $response->url; // https://i.html2img.com/abc123def456.png
```

The API returns a JSON envelope containing the CDN URL of the render, not the
image bytes. Store the URL against your record and serve it directly, or download
a copy (see [downloading and storing](#downloading-and-storing)).

> **Note: Nothing to install on the server**
>
> There is no Chrome binary, no shared memory tuning and no Puppeteer version to
> keep in step with your PHP version. The rendering happens on our infrastructure;
> your application makes one HTTP request.

## HTML to image

`POST /api/html`. Send a complete HTML document and get back an image of the
rendered result. Inline your CSS in a `<style>` block, or pull in remote
stylesheets and web fonts with `<link>` tags in the document head.

```php
use Html2img\Request\HtmlRequest;

$document = <<<HTML
<!doctype html>
<html>
  <head>
    <meta charset="utf-8">
    <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;800&display=swap">
    <style>
      body { margin: 0; width: 1200px; height: 630px; display: flex;
             align-items: center; padding: 80px; box-sizing: border-box;
             font-family: Inter, system-ui, sans-serif; background: #0f172a; color: #fff; }
      h1 { font-size: 68px; line-height: 1.1; margin: 0; font-weight: 800; }
    </style>
  </head>
  <body><h1>{$post->title}</h1></body>
</html>
HTML;

$response = $client->html(new HtmlRequest(
    html: $document,
    width: 1200,
    height: 630,
    dpi: 2, // retina: the file comes back 2400x1260
));
```

### Rendering from a PHP template

Most applications already have a templating layer. Capture its output and hand
the string to the SDK. With plain PHP includes:

```php
function renderToString(string $template, array $data = []): string
{
    extract($data, EXTR_SKIP);

    ob_start();
    require $template;

    return (string) ob_get_clean();
}

$response = $client->html(new HtmlRequest(
    html: renderToString(__DIR__ . '/templates/og-card.php', ['post' => $post]),
    width: 1200,
    height: 630,
    dpi: 2,
));
```

Twig, Latte, Plates and Blade all expose the same shape: a method that returns the
rendered string. Anything a browser can draw works, because a browser is what
draws it.

> **Warning: Every URL must be publicly reachable**
>
> The renderer fetches your fonts, images and stylesheets from its own servers, so
> `http://localhost/logo.png` resolves to nothing and comes out blank. Reference
> absolute public URLs, inline small images as data URIs, or expose your
> development site through a tunnel while you iterate.

### Injecting CSS after load

The [`css` parameter](https://html2img.com/docs/parameters/css/) is applied after the document loads,
on top of whatever the markup already carries. On the HTML endpoint it is mostly
useful for theming one document two ways:

```php
$dark = $client->html(new HtmlRequest(
    html: $document,
    css: 'body { background: #0f172a; color: #fff; }',
    width: 1200,
    height: 630,
));

$light = $client->html(new HtmlRequest(
    html: $document,
    css: 'body { background: #fff; color: #0f172a; }',
    width: 1200,
    height: 630,
));
```

## Website screenshots

`POST /api/screenshot` captures a live, publicly reachable URL. Use
[`selector`](https://html2img.com/docs/parameters/selector/) to crop to a single element and
[`css`](https://html2img.com/docs/parameters/css/) to hide anything you do not want in the frame.

```php
use Html2img\Request\ScreenshotRequest;

// Viewport capture at a fixed size
$response = $client->screenshot(new ScreenshotRequest(
    url: 'https://example.com',
    width: 1200,
    height: 630,
));

// The whole scroll length of the page
$response = $client->screenshot(new ScreenshotRequest(
    url: 'https://example.com/pricing',
    width: 1400,
    fullpage: true,
));

// One element, cropped to its own bounding box
$response = $client->screenshot(new ScreenshotRequest(
    url: 'https://example.com/pricing',
    selector: '#plans',
    width: 1400,
));

// Hide a cookie banner and a chat widget before capturing
$response = $client->screenshot(new ScreenshotRequest(
    url: 'https://example.com',
    css: '.cookie-banner, .intercom-launcher { display: none !important; }',
    width: 1440,
    height: 900,
    dpi: 2,
));
```

Page styles usually win on specificity, so injected rules generally need
`!important`. Two parameters control timing: `waitForSelector` returns as soon as
an element exists, `msDelay` always waits the full duration. Prefer the first
wherever you control the markup.

```php
$response = $client->screenshot(new ScreenshotRequest(
    url: 'https://your-app.example.com/reports/42',
    waitForSelector: '#chart-rendered',
    msDelay: 400,
    width: 1440,
    height: 900,
));
```

Full behaviour for each option lives in the parameter reference:
[`fullpage`](https://html2img.com/docs/parameters/fullpage/),
[`selector`](https://html2img.com/docs/parameters/selector/),
[`dimensions`](https://html2img.com/docs/parameters/dimensions/),
[`dpi`](https://html2img.com/docs/parameters/dpi/),
[`wait_for_selector`](https://html2img.com/docs/parameters/wait_for_selector/) and
[`ms_delay`](https://html2img.com/docs/parameters/ms_delay/).

## HTML to PDF

Set `format` to `Format::Pdf` on either request object and the same markup comes
back as an A4 portrait vector PDF: text stays selectable and searchable, web fonts
are embedded, and long content paginates automatically. One credit, the same as an
image.

```php
use Html2img\Enum\Format;
use Html2img\Request\HtmlRequest;

$response = $client->html(new HtmlRequest(
    html: renderToString(__DIR__ . '/templates/invoice.php', ['invoice' => $invoice]),
    format: Format::Pdf,
));

echo $response->url; // https://i.html2img.com/....pdf
```

`width`, `height`, `dpi`, `fullpage` and `selector` are ignored in PDF mode,
because the page size is fixed. The
[`format` parameter docs](https://html2img.com/docs/parameters/format/) cover the rest, and the
[HTML to PDF API](https://html2img.com/html-to-pdf/) page has the overview.

> **Note: scale_to_fit is a raw-request option**
>
> The API's [`scale_to_fit`](https://html2img.com/docs/parameters/scale-to-fit/) parameter, which shrinks
> a layout wider than the page rather than cropping it, is not exposed on the PHP
> SDK's request objects. If you need it, send that one request with
> [raw cURL or Guzzle](#without-the-sdk).

## Named templates

`POST /api/v1/templates/{slug}` renders one of the [named templates](https://html2img.com/templates/)
from a JSON payload, with no markup of your own. Data is validated server-side per
template, and templates output PNG only.

```php
$response = $client->template('invoice-image', [
    'invoice_number' => 'INV-2026-0042',
    'business_name'  => 'Coastline Coffee Co',
    'client_name'    => 'Riverside Bakery',
    'items'          => [
        ['description' => 'Wholesale beans', 'quantity' => '50', 'unit_price' => '£15.00', 'amount' => '£750.00'],
    ],
    'total'          => '£750.00',
]);

echo $response->template; // invoice-image
echo $response->url;
```

PHP applications most often reach for
[invoice](https://html2img.com/templates/invoice-image/),
[receipt](https://html2img.com/templates/receipt-image/),
[certificate](https://html2img.com/templates/certificate-of-completion/),
[business card](https://html2img.com/templates/business-card/),
[coupon](https://html2img.com/templates/coupon-voucher/) and
[event ticket](https://html2img.com/templates/event-ticket/) templates. Each one's inputs are
documented in the [template reference](https://html2img.com/docs/templates/).

## Downloading and storing

The response carries a hosted URL rather than bytes, which is usually what you
want: store the URL on the record and let the CDN serve it. When you would rather
keep your own copy, download it like any other file.

```php
// Simplest: straight to disk
file_put_contents(
    __DIR__ . "/storage/og/{$post->id}.png",
    file_get_contents($response->url),
);
```

With Guzzle, which the SDK already brings in, and streaming so a long PDF never
sits in memory:

```php
use GuzzleHttp\Client;

$http = new Client();

$http->get($response->url, ['sink' => __DIR__ . "/storage/og/{$post->id}.png"]);
```

Or hand the bytes to an object store:

```php
$s3->putObject([
    'Bucket'      => 'my-bucket',
    'Key'         => "og/{$post->id}.png",
    'Body'        => file_get_contents($response->url),
    'ContentType' => 'image/png',
]);
```

> **Tip: Free-tier renders expire**
>
> `$response->expiresAt` is an ISO 8601 timestamp on the free tier and `null` on
> paid plans, where renders are hosted permanently. If you are on the free tier and
> the image needs to outlive that window, download it. Upgrading makes everything
> you rendered earlier permanent too.

## Configuration

Everything beyond the API key is optional:

```php
use Html2img\Html2imgClient;

$client = new Html2imgClient(
    apiKey: getenv('HTML2IMG_API_KEY'),
    baseUri: 'https://app.html2img.com', // default
    timeout: 35.0,                       // seconds, default
);
```

The 35 second default sits just above the API's 30 second synchronous render
budget. For captures likely to exceed it, pass a `webhookUrl` rather than raising
the timeout (see [background rendering](#background-rendering)).

### Reusing your own Guzzle client

Pass a pre-configured `GuzzleHttp\ClientInterface` to reuse your own middleware,
retry strategy, proxy settings or logging. The SDK still sets the `X-API-Key`,
`Accept` and `Content-Type` headers on every request:

```php
use GuzzleHttp\Client;
use Html2img\Html2imgClient;

$guzzle = new Client([
    'base_uri' => 'https://app.html2img.com',
    'timeout'  => 60,
    // your own handler stack, middleware, proxy settings
]);

$client = new Html2imgClient(getenv('HTML2IMG_API_KEY'), httpClient: $guzzle);
```

### A service class

In a framework-free application, wrapping the client in a small service keeps the
API key resolution and your house defaults in one place:

```php
namespace App\Imaging;

use Html2img\Html2imgClient;
use Html2img\Request\HtmlRequest;
use Html2img\Response\RenderResponse;

final class SocialImages
{
    public function __construct(private readonly Html2imgClient $client) {}

    public static function fromEnvironment(): self
    {
        $key = getenv('HTML2IMG_API_KEY');

        if ($key === false || $key === '') {
            throw new \RuntimeException('HTML2IMG_API_KEY is not set.');
        }

        return new self(new Html2imgClient($key));
    }

    /** Every social card in the application is 1200x630 at 2x. */
    public function card(string $document): RenderResponse
    {
        return $this->client->html(new HtmlRequest(
            html: $document,
            width: 1200,
            height: 630,
            dpi: 2,
        ));
    }
}
```

## Background rendering

A synchronous request holds the connection open until the render finishes, within
a 30 second budget. Two ways to keep that off a web request:

**Render in a worker.** Whatever queue you already run (Symfony Messenger, a
database-backed job table, a cron-driven command) can call the client and write
the resulting URL back to the record. Nothing in the SDK is queue-specific.

**Let the API call you back.** Pass a `webhookUrl` and the API responds
immediately with `status: "processing"` and a null `url`, then POSTs the finished
URL to your endpoint:

```php
$response = $client->screenshot(new ScreenshotRequest(
    url: 'https://example.com/very-long-report',
    fullpage: true,
    webhookUrl: 'https://your-app.example.com/hooks/html2img',
));

if ($response->isProcessing()) {
    // The final URL arrives at your webhook, not on this response.
    $render->update(['status' => 'pending', 'render_id' => $response->id]);
}
```

The payload shape and delivery guarantees are in the
[`webhook_url` docs](https://html2img.com/docs/parameters/webhook-url/).

## Error handling

Every failure throws an `Html2img\Exception\Html2imgException`. Catch that one
type to handle anything, or catch a subclass to react per case. No raw Guzzle
exception escapes the client.

```php
use Html2img\Exception\Html2imgException;
use Html2img\Exception\InsufficientCreditsException;
use Html2img\Exception\TimeoutException;
use Html2img\Exception\ValidationException;

try {
    $response = $client->html(new HtmlRequest(html: $document, width: 1200, height: 630));
} catch (ValidationException $e) {
    // 400 or 422: the request was malformed. Retrying will not help.
    foreach ($e->details() as $field => $messages) {
        $logger->warning("html2img rejected {$field}: " . implode(', ', $messages));
    }
} catch (InsufficientCreditsException $e) {
    // 402: out of credits. Keep the old image and tell someone.
    $logger->error('html2img out of credits', ['remaining' => $e->creditsRemaining()]);
} catch (TimeoutException $e) {
    // 504: the render exceeded the sync budget. Re-send with a webhookUrl.
} catch (Html2imgException $e) {
    $logger->error('html2img failed', [
        'status' => $e->statusCode(),
        'code'   => $e->errorCode(),
        'body'   => $e->payload(),
    ]);
}
```

| Exception | Raised on |
| --- | --- |
| `AuthenticationException` | 401, missing or invalid API key |
| `InsufficientCreditsException` | 402, no credits remaining |
| `NotSubscribedException` | 403, no active subscription |
| `NotFoundException` | 404, for example an unknown template slug |
| `ValidationException` | 400 or 422, with `details()` per field |
| `TimeoutException` | 408 or 504, the render budget was exceeded |
| `ServerException` | 5xx, an unexpected renderer error |
| `ConnectionException` | The request never reached a response |
| `Html2imgException` | Base type for all of the above |

Out-of-range option values are caught locally, before a request leaves your
server: a `width` above 5000 or a `dpi` above 4 throws
`InvalidArgumentException` rather than spending a credit on a render the API would
reject. Retry 5xx and connection failures; do not retry a 4xx, since the same
request will fail the same way. The full status-code reference is in the
[getting started guide](https://html2img.com/docs/getting-started/).

## Testing

The client's only seam is the Guzzle instance, so a mocked handler keeps your test
suite off the network and off your credit balance:

```php
use GuzzleHttp\Client;
use GuzzleHttp\Handler\MockHandler;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Psr7\Response;
use Html2img\Html2imgClient;
use Html2img\Request\HtmlRequest;

$mock = new MockHandler([
    new Response(200, [], json_encode([
        'success' => true,
        'id' => 'test-render',
        'url' => 'https://i.html2img.com/test.png',
        'credits_remaining' => 49,
    ])),
]);

$client = new Html2imgClient(
    'test-key',
    httpClient: new Client(['handler' => HandlerStack::create($mock)]),
);

$response = $client->html(new HtmlRequest(html: '<h1>Hi</h1>'));

assert($response->url === 'https://i.html2img.com/test.png');
```

Queue a 402 response the same way to exercise your out-of-credits path. To check a
real key and balance without spending a credit, call
[`GET /api/me`](https://html2img.com/docs/account/); the [testing guide](https://html2img.com/docs/testing/) covers
reproducing each error condition against the live API.

## Without the SDK

If you would rather not add a dependency, the API is four fields and a header.

### Raw cURL

```php
function html2img(string $endpoint, array $payload): array
{
    $ch = curl_init("https://app.html2img.com/api/{$endpoint}");

    curl_setopt_array($ch, [
        CURLOPT_POST => true,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT => 35,
        CURLOPT_HTTPHEADER => [
            'Content-Type: application/json',
            'X-API-Key: ' . getenv('HTML2IMG_API_KEY'),
        ],
        CURLOPT_POSTFIELDS => json_encode($payload, JSON_THROW_ON_ERROR),
    ]);

    $body = curl_exec($ch);

    if ($body === false) {
        $error = curl_error($ch);
        curl_close($ch);

        throw new RuntimeException("html2img request failed: {$error}");
    }

    $status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    $result = json_decode((string) $body, true, 512, JSON_THROW_ON_ERROR);

    if ($status !== 200 || empty($result['success'])) {
        throw new RuntimeException(sprintf(
            'html2img returned %d: %s',
            $status,
            $result['message'] ?? $result['error'] ?? 'unknown error',
        ));
    }

    return $result;
}

// HTML to image
$render = html2img('html', [
    'html'   => $document,
    'width'  => 1200,
    'height' => 630,
    'dpi'    => 2,
]);

// Screenshot
$shot = html2img('screenshot', [
    'url'      => 'https://example.com',
    'fullpage' => true,
]);

// PDF, with the scale_to_fit option the SDK does not expose
$pdf = html2img('html', [
    'html'         => $document,
    'format'       => 'pdf',
    'scale_to_fit' => true,
]);

echo $render['url'];
```

Named templates use the same helper with a different path:

```php
$ch = curl_init('https://app.html2img.com/api/v1/templates/invoice-image');
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Content-Type: application/json',
        'X-API-Key: ' . getenv('HTML2IMG_API_KEY'),
    ],
    CURLOPT_POSTFIELDS => json_encode([
        'invoice_number' => 'INV-2026-0042',
        'business_name'  => 'Coastline Coffee Co',
        'client_name'    => 'Riverside Bakery',
        'total'          => '£750.00',
    ]),
]);
$url = json_decode(curl_exec($ch), true)['url'];
```

### Guzzle

```php
namespace App\Services;

use GuzzleHttp\Client;
use GuzzleHttp\Exception\BadResponseException;
use RuntimeException;

final class Html2imgClient
{
    private Client $http;

    public function __construct(string $apiKey)
    {
        $this->http = new Client([
            'base_uri' => 'https://app.html2img.com/api/',
            'timeout'  => 35,
            'headers'  => [
                'X-API-Key'    => $apiKey,
                'Content-Type' => 'application/json',
                'Accept'       => 'application/json',
            ],
        ]);
    }

    public function renderHtml(string $html, array $options = []): string
    {
        return $this->post('html', ['html' => $html] + $options);
    }

    public function screenshot(string $url, array $options = []): string
    {
        return $this->post('screenshot', ['url' => $url] + $options);
    }

    public function template(string $slug, array $data): string
    {
        return $this->post("v1/templates/{$slug}", $data);
    }

    private function post(string $path, array $payload): string
    {
        try {
            $response = $this->http->post($path, ['json' => $payload]);
        } catch (BadResponseException $e) {
            $body = json_decode((string) $e->getResponse()->getBody(), true) ?: [];

            throw new RuntimeException(sprintf(
                'html2img returned %d: %s',
                $e->getResponse()->getStatusCode(),
                $body['message'] ?? $body['error'] ?? 'unknown error',
            ), 0, $e);
        }

        return json_decode((string) $response->getBody(), true)['url'];
    }
}
```

This is the shape the SDK gives you for free, plus typed request objects, typed
exceptions and local range checks.

## Package reference

**`Html2img\Html2imgClient`**

| Member | Signature |
| --- | --- |
| Constructor | `__construct(string $apiKey, string $baseUri = 'https://app.html2img.com', float $timeout = 35.0, ?ClientInterface $httpClient = null)` |
| `html()` | `html(HtmlRequest $request): RenderResponse` |
| `screenshot()` | `screenshot(ScreenshotRequest $request): RenderResponse` |
| `template()` | `template(string $slug, array $data = []): RenderResponse` |

**Request objects.** `Html2img\Request\HtmlRequest` takes `html` plus `css`,
`width`, `height`, `fullpage`, `dpi`, `webhookUrl`, `msDelay`, `waitForSelector`
and `format`. `Html2img\Request\ScreenshotRequest` takes `url` plus the same
options and additionally `selector`. Every option except the first is nullable and
omitted from the request when null, so the server applies its own default. Full
ranges and behaviour are in the [parameter reference](https://html2img.com/docs/parameters/).

**`Html2img\Enum\Format`** is `Format::Png` or `Format::Pdf`.

**`Html2img\Response\RenderResponse`** is readonly:

| Property | Type | Meaning |
| --- | --- | --- |
| `success` | `bool` | Whether the API reported success |
| `id` | `string\|null` | The render id |
| `url` | `string\|null` | CDN URL of the render, null while an async job is pending |
| `expiresAt` | `string\|null` | ISO 8601 expiry on the free tier, null on paid plans |
| `creditsRemaining` | `int\|null` | Credits left after this call |
| `status` | `string\|null` | `"processing"` for accepted async jobs |
| `message` | `string\|null` | Human-readable message, when provided |
| `template` | `string\|null` | Template slug, on template renders |
| `isProcessing()` | `bool` | Whether the job is still rendering |
| `raw()` | `array` | The full decoded JSON payload |

## Troubleshooting

**`HTML2IMG_API_KEY` is empty at runtime.** `getenv()` reads the process
environment, which is not the same as a `.env` file. If you use `vlucas/phpdotenv`
or similar, make sure it is loaded before the client is constructed, and remember
that PHP-FPM does not inherit your shell's environment.

**Images and fonts are missing from the render.** The renderer fetches them from
the public internet. `localhost`, `.test`, `.ddev.site` and private network
addresses are invisible to it. Use absolute public URLs, inline small assets as
data URIs, or tunnel your development site while you iterate.

**The output is blank or half-drawn.** The capture happened before the content
did. Add `waitForSelector` pointing at something the finished page contains, or
`msDelay` as a fallback for anything inside an iframe, which selectors cannot see.

**`ValidationException` on the templates endpoint.** Each template validates its
own payload. `details()` names the field and the reason; check the template's
inputs in the [template reference](https://html2img.com/docs/templates/).

**`TimeoutException` on full-page captures.** A very long page can exceed the 30
second synchronous budget. Send the same request with a `webhookUrl` and handle
the callback rather than raising the client timeout.

**Injected CSS has no effect.** Page rules usually win on specificity. Add
`!important`, and confirm the selector matches on the live page rather than in
your local copy.

**A `ConnectionException` in a container.** Outbound HTTPS to
`app.html2img.com` needs to be allowed, and the container needs CA certificates.
An empty `/etc/ssl/certs` in a slim base image is a common cause.

## FAQ

**Should I use the SDK or plain cURL?**
Use the SDK unless you have a policy against adding dependencies. It is one small
package on top of Guzzle, and the parts it replaces (status-code mapping, request
building, range checks) are the parts most often got wrong by hand. Both routes
hit the same endpoints and cost the same.

**Does this work without Composer?**
The SDK requires Composer, because it requires Guzzle. The
[raw cURL helper](#without-the-sdk) above has no dependencies at all and works in
any PHP 8 codebase.

**Can I render a Blade, Twig or Latte view?**
Yes. The SDK takes a string, so anything that renders to a string works. On
Laravel, use the [Laravel package](https://html2img.com/integrations/laravel/), where
`view(...)->render()` drops straight into the request object.

**Do I get the image bytes back?**
No, you get a hosted URL. That is deliberate: most applications want to store a
URL rather than proxy bytes, and downloads from the CDN are free and unmetered.
See [downloading and storing](#downloading-and-storing) if you want a local copy.

**Is one PDF page one credit?**
No. One render is one credit, however many pages the PDF turns out to be, and a
PDF costs exactly the same as an image.

**How do I check my balance without spending a credit?**
Call [`GET /api/me`](https://html2img.com/docs/account/). It reports your plan and remaining credits
and never renders anything.

**Is there a synchronous size or time limit?**
A synchronous render has a 30 second budget. Anything likely to exceed it should
pass a `webhookUrl` and be handled asynchronously.
