Bannerify
All posts

Generate dynamic images in PHP with an API

Design one template, then render banners, product images, and certificates from PHP. Works with plain cURL or the bannerify/bannerify SDK, no GD or Imagick.

· Duc An

Most PHP image code is layout code. With GD or Imagick, every design decision turns into a function call:

$image = imagecreatetruecolor(1200, 630);
$blue = imagecolorallocate($image, 37, 99, 235);
imagefilledrectangle($image, 0, 0, 1200, 630, $blue);
imagettftext($image, 48, 0, 80, 300, $white, 'Inter-Bold.ttf', 'Summer sale: 30% off');

It works, until someone wants the headline in two lines, a shadow behind the price, or a QR code in the corner. Now the design lives inside your PHP, and every tweak is a code change and a deploy.

There is a third option between “draw it yourself” and “run a headless browser fleet”: keep the layout in a template, and let PHP send only the data.

How the API version works

  1. Design the banner once in the editor, or duplicate a template from the gallery.
  2. Name the layers you want to change later, for example headline, price, photo.
  3. Send the template ID plus your values to the API. The response is the finished image.

Fonts, spacing, gradients, shadows, tables, and QR codes stay in the template. A designer can change all of it without touching your PHP, and your PHP keeps doing what it is good at: moving data around.

What you need

  • An API key. Create one in Dashboard → API Keys and keep it on the server. Treat it like a database password.
  • A template ID. Open the template you designed and copy its ID. It looks like tpl_kd93mz.
  • Layer names. The name you give a layer in the editor is the name you use in the request, matched exactly.

Option 1: plain cURL

No dependencies, works on any PHP host, including the ones where installing extensions is a fight.

<?php

$payload = [
    'apiKey' => getenv('BANNERIFY_API_KEY'),
    'templateId' => 'tpl_kd93mz',
    'format' => 'png',
    'modifications' => [
        ['name' => 'headline', 'text' => 'Summer sale: 30% off'],
        ['name' => 'price', 'text' => '$149.00'],
        ['name' => 'photo', 'src' => 'https://example.com/sneaker.jpg'],
    ],
];

$ch = curl_init('https://api.bannerify.co/v1/templates/createImage');

curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
    CURLOPT_POSTFIELDS => json_encode($payload),
    CURLOPT_TIMEOUT => 30,
]);

$image = curl_exec($ch);

if ($image === false) {
    throw new RuntimeException('Transport error: ' . curl_error($ch));
}

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

if ($status !== 200) {
    throw new RuntimeException("Bannerify returned {$status}: " . substr($image, 0, 200));
}

file_put_contents(__DIR__ . '/banner.png', $image);

Two details that trip people up:

  • The response body is the image itself, not JSON with base64 inside. Write it straight to a file or stream it to the browser.
  • Errors are JSON with a 4xx status. That is why the code above checks the status code before saving anything.

Option 2: the official PHP SDK

If you would rather not hand-roll requests, install the SDK:

composer require bannerify/bannerify

The SDK returns either a result or an error, never both, so the failure path is impossible to forget:

<?php

use Bannerify\Bannerify\BannerifyClient;

$client = new BannerifyClient(getenv('BANNERIFY_API_KEY'));

$result = $client->createImage('tpl_kd93mz', [
    'modifications' => [
        ['name' => 'headline', 'text' => $product['name']],
        ['name' => 'price', 'text' => '$' . number_format($product['price'], 2)],
        ['name' => 'photo', 'src' => $product['image_url']],
    ],
]);

if (isset($result['error'])) {
    error_log('Render failed: ' . $result['error']['message']);

    return null;
}

$path = __DIR__ . "/banners/{$product['sku']}.png";
file_put_contents($path, $result['result']);

Full method list and options are in the PHP SDK docs.

Sending real data

You do not have to build the modification array by hand. The API also accepts an object shorthand where the key is the layer name:

'modifications' => [
    'headline' => $product['name'],
    'price' => '$' . number_format($product['price'], 2),
    'photo' => ['src' => $product['image_url']],
    'qr' => ['qrcode' => $product['checkout_url']],
],

Plain values become text. Nested arrays keep their own fields, which is what you need for images, QR codes, barcodes, charts, and tables.

Per-layer fields you can send:

  • text replaces the content of a text layer
  • color overrides the layer color, for example #FF0000
  • src swaps an image layer, using any publicly reachable HTTPS URL
  • qrcode and barcode encode a link, SKU, or tracking number
  • rows and columns fill charts and tables
  • visible hides a layer for one render only
  • star sets a rating value

One field per layer, sent by name. If you need the same data on two layers, send it twice.

Batching and caching

Generating a thousand images is a loop, not a special case:

foreach ($products as $product) {
    $payload = buildPayload($product);
    $cacheKey = hash('xxh128', json_encode($payload));
    $path = __DIR__ . "/cache/{$cacheKey}.png";

    if (file_exists($path)) {
        continue;
    }

    file_put_contents($path, renderBanner($payload));
}

Keep the loop bounded (a few concurrent requests at a time), and key the cache on the payload so a repeated request never pays twice. Rendering is synchronous, so you get the file in the same request instead of polling a job queue.

If you do not want to store files at all, use createStoredImage instead. It uploads the render and returns a hosted URL:

{ "url": "https://cdn.bannerify.co/..." }

That is the convenient path for Open Graph images, email headers, and anywhere a URL is easier to hand around than a file.

Formats and sizes

  • format accepts png (default), jpeg, or webp.
  • The canvas size comes from the template, not from the request. Keep one template per size you need: 1200x630 for link previews, 1080x1080 for a feed, 1080x1920 for stories.
  • Sending src for a photo? The renderer fetches that URL, so it has to be public. A local file or a private bucket will fail.

Common mistakes

  • Putting the API key in browser JavaScript or a mobile app. It belongs on the server.
  • Rebuilding the image when nothing changed. Cache on the payload hash.
  • Rendering thousands of images inside one web request. It works for tens, use a queue for thousands.
  • Assuming the response is JSON. It is the file itself, unless you use the stored endpoint.

What it costs

The free plan includes 100 generations a month (10 a day), one template, and 1GB of stored images, which is plenty to wire this into a real app first. PRO is $29 a month for 10,000 generations and unlimited templates. Details on pricing.

If you are generating images from PHP today, start with the free plan and move one template off GD. The first render that comes back with the layout intact is usually the moment people stop writing positioning code.

Your first 100 images are on us.

Make a template, grab an API key, and render your first image in a few minutes.