PHP Image Generation API: Laravel & WordPress Guide

PHP Image Generation API: Laravel & WordPress Guide

PHP powers roughly 77% of all websites with a known server-side language. If you're building with Laravel, WordPress, or plain PHP and need to generate images programmatically, you don't need to switch languages. A single POST request is all it takes.

I've put together working examples for every common PHP setup, from raw cURL to Laravel queues to WordPress hooks. Every code block here is something you can copy into a project and run. Let's get into it.

The basics calling the api with curlThe Basics: Calling the API with cURL

No frameworks, no dependencies. Just PHP and cURL. This works anywhere PHP runs:

<?php
 
$designId = 'YOUR_DESIGN_ID';
$apiKey = 'YOUR_API_KEY';
 
$payload = json_encode([
    'headline' => ['text' => 'Hello from PHP'],
    'subtitle' => ['text' => 'Generated with cURL'],
]);
 
$ch = curl_init("https://render.imejis.io/v1/{$designId}");
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_POSTFIELDS => $payload,
    CURLOPT_HTTPHEADER => [
        'Content-Type: application/json',
        "dma-api-key: {$apiKey}",
    ],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT => 30,
]);
 
$imageData = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
 
if ($httpCode === 200) {
    file_put_contents('output.png', $imageData);
    echo "Image saved!\n";
} else {
    echo "Error: HTTP {$httpCode}\n";
}

The API returns image binary directly, not JSON, not a URL. You get the raw PNG bytes back. Write them to a file, store them in S3, or stream them to a browser.

But raw cURL gets messy fast. For anything beyond a one-off script, you'll want a proper HTTP client.

Using guzzleUsing Guzzle

Guzzle is the standard HTTP client for modern PHP. Laravel ships with it. Composer makes it a one-liner to install:

composer require guzzlehttp/guzzle

Here's the same API call with Guzzle:

<?php
 
use GuzzleHttp\Client;
 
$client = new Client([
    'base_uri' => 'https://render.imejis.io/v1/',
    'timeout' => 30,
    'headers' => [
        'Content-Type' => 'application/json',
        'dma-api-key' => getenv('IMEJIS_API_KEY'),
    ],
]);
 
$response = $client->post('YOUR_DESIGN_ID', [
    'json' => [
        'headline' => ['text' => 'Built with Guzzle'],
        'subtitle' => ['text' => 'Much cleaner'],
    ],
]);
 
$imageData = $response->getBody()->getContents();
file_put_contents('output.png', $imageData);

Cleaner. The base URI, timeout, and headers are set once. Every request reuses them. And Guzzle gives you proper exception handling, which matters a lot in production.

Laravel integrationLaravel Integration

Laravel is where this gets really fun. You can wire up image generation as a service, expose it through controllers, run it from the CLI, or push it to a background queue. Here's how I'd set it up.

Service classService Class

First, create a dedicated service. This keeps your image generation logic in one place:

<?php
 
namespace App\Services;
 
use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;
use Illuminate\Support\Facades\Log;
 
class ImageService
{
    private Client $client;
 
    public function __construct()
    {
        $this->client = new Client([
            'base_uri' => 'https://render.imejis.io/v1/',
            'timeout' => 30,
            'headers' => [
                'Content-Type' => 'application/json',
                'dma-api-key' => config('services.imejis.api_key'),
            ],
        ]);
    }
 
    public function generate(string $designId, array $data): ?string
    {
        try {
            $response = $this->client->post($designId, [
                'json' => $data,
            ]);
 
            return $response->getBody()->getContents();
        } catch (GuzzleException $e) {
            Log::error('Image generation failed', [
                'design_id' => $designId,
                'error' => $e->getMessage(),
            ]);
            return null;
        }
    }
 
    public function generateAndStore(
        string $designId,
        array $data,
        string $path
    ): ?string {
        $imageData = $this->generate($designId, $data);
 
        if ($imageData === null) {
            return null;
        }
 
        $storagePath = "images/{$path}";
        \Storage::put($storagePath, $imageData);
 
        return $storagePath;
    }
}

Add your API key to config/services.php:

'imejis' => [
    'api_key' => env('IMEJIS_API_KEY'),
],

And your .env:

IMEJIS_API_KEY=your_api_key_here

Controller exampleController Example

Now expose it through a controller. This generates an image on demand and returns it as a download:

<?php
 
namespace App\Http\Controllers;
 
use App\Services\ImageService;
use Illuminate\Http\Request;
 
class ImageController extends Controller
{
    public function __construct(
        private ImageService $imageService
    ) {}
 
    public function generate(Request $request)
    {
        $validated = $request->validate([
            'design_id' => 'required|string',
            'headline' => 'required|string',
            'subtitle' => 'nullable|string',
        ]);
 
        $imageData = $this->imageService->generate(
            $validated['design_id'],
            [
                'headline' => ['text' => $validated['headline']],
                'subtitle' => ['text' => $validated['subtitle'] ?? ''],
            ]
        );
 
        if ($imageData === null) {
            return response()->json(['error' => 'Generation failed'], 500);
        }
 
        return response($imageData, 200)
            ->header('Content-Type', 'image/png')
            ->header('Content-Disposition', 'attachment; filename="generated.png"');
    }
}

Register the route in routes/api.php:

Route::post('/images/generate', [ImageController::class, 'generate']);

Artisan commandArtisan Command

For batch generation from the command line, an Artisan command works great. I use this when I need to generate images for a bunch of products or blog posts at once:

<?php
 
namespace App\Console\Commands;
 
use App\Services\ImageService;
use Illuminate\Console\Command;
 
class GenerateImages extends Command
{
    protected $signature = 'images:generate
        {design_id : The template design ID}
        {--source=products : Source model to generate from}';
 
    protected $description = 'Batch generate images for a model';
 
    public function handle(ImageService $imageService): int
    {
        $designId = $this->argument('design_id');
        $source = $this->option('source');
 
        $items = match ($source) {
            'products' => \App\Models\Product::all(),
            'posts' => \App\Models\Post::whereNull('featured_image')->get(),
            default => collect(),
        };
 
        $bar = $this->output->createProgressBar($items->count());
        $bar->start();
 
        $failed = 0;
 
        foreach ($items as $item) {
            $path = $imageService->generateAndStore(
                $designId,
                [
                    'headline' => ['text' => $item->title],
                    'subtitle' => ['text' => $item->description ?? ''],
                ],
                "{$source}/{$item->id}.png"
            );
 
            if ($path === null) {
                $failed++;
                $this->error(" Failed: {$item->title}");
            } else {
                $item->update(['featured_image' => $path]);
            }
 
            $bar->advance();
        }
 
        $bar->finish();
        $this->newLine();
        $this->info("Done. {$failed} failures out of {$items->count()} total.");
 
        return $failed > 0 ? self::FAILURE : self::SUCCESS;
    }
}

Run it: php artisan images:generate abc123 --source=products

Queue jobQueue Job

Generating images synchronously blocks your request. For anything user-facing, push it to a queue. The user gets an instant response, and the image generates in the background:

<?php
 
namespace App\Jobs;
 
use App\Services\ImageService;
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\Log;
 
class GenerateImageJob implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
 
    public int $tries = 3;
    public int $backoff = 10;
 
    public function __construct(
        private string $designId,
        private array $data,
        private string $storagePath,
        private ?int $modelId = null,
        private ?string $modelClass = null,
    ) {}
 
    public function handle(ImageService $imageService): void
    {
        $path = $imageService->generateAndStore(
            $this->designId,
            $this->data,
            $this->storagePath,
        );
 
        if ($path && $this->modelId && $this->modelClass) {
            $model = ($this->modelClass)::find($this->modelId);
            $model?->update(['featured_image' => $path]);
        }
    }
 
    public function failed(\Throwable $exception): void
    {
        Log::error('Image generation job failed', [
            'design_id' => $this->designId,
            'storage_path' => $this->storagePath,
            'error' => $exception->getMessage(),
        ]);
    }
}

Dispatch it from anywhere:

GenerateImageJob::dispatch(
    designId: 'YOUR_DESIGN_ID',
    data: ['headline' => ['text' => $post->title]],
    storagePath: "posts/{$post->id}.png",
    modelId: $post->id,
    modelClass: Post::class,
);

Laravel handles retries automatically. Three attempts with a 10-second backoff. If all three fail, the failed() method logs the details so you can investigate.

Wordpress integrationWordPress Integration

WordPress doesn't use Composer by default (though it can). The built-in wp_remote_post function handles HTTP requests without any extra dependencies. For a broader look at WordPress image automation (including no-code options), see our WordPress image automation guide.

This is the one most people want. When you publish a post, WordPress calls the image API and sets the result as the featured image. No manual work:

<?php
// Add this to your theme's functions.php or a custom plugin
 
add_action('save_post', 'imejis_generate_featured_image', 10, 3);
 
function imejis_generate_featured_image($post_id, $post, $update) {
    // Only run on publish, not drafts or updates
    if ($post->post_status !== 'publish' || $post->post_type !== 'post') {
        return;
    }
 
    // Skip if already has a featured image
    if (has_post_thumbnail($post_id)) {
        return;
    }
 
    $api_key = defined('IMEJIS_API_KEY') ? IMEJIS_API_KEY : '';
    $design_id = defined('IMEJIS_DESIGN_ID') ? IMEJIS_DESIGN_ID : '';
 
    if (empty($api_key) || empty($design_id)) {
        return;
    }
 
    $response = wp_remote_post(
        "https://render.imejis.io/v1/{$design_id}",
        [
            'timeout' => 30,
            'headers' => [
                'Content-Type' => 'application/json',
                'dma-api-key' => $api_key,
            ],
            'body' => wp_json_encode([
                'headline' => ['text' => $post->post_title],
                'subtitle' => ['text' => wp_trim_words($post->post_excerpt, 15)],
            ]),
        ]
    );
 
    if (is_wp_error($response) || wp_remote_retrieve_response_code($response) !== 200) {
        error_log('Imejis image generation failed for post ' . $post_id);
        return;
    }
 
    $image_data = wp_remote_retrieve_body($response);
    $filename = "imejis-{$post_id}.png";
 
    // Save to uploads directory
    $upload = wp_upload_bits($filename, null, $image_data);
 
    if ($upload['error']) {
        error_log('Failed to save image: ' . $upload['error']);
        return;
    }
 
    // Create attachment
    $attachment_id = wp_insert_attachment([
        'post_mime_type' => 'image/png',
        'post_title' => sanitize_file_name($filename),
        'post_content' => '',
        'post_status' => 'inherit',
    ], $upload['file'], $post_id);
 
    require_once ABSPATH . 'wp-admin/includes/image.php';
    $metadata = wp_generate_attachment_metadata($attachment_id, $upload['file']);
    wp_update_attachment_metadata($attachment_id, $metadata);
 
    set_post_thumbnail($post_id, $attachment_id);
}

Add your credentials to wp-config.php:

define('IMEJIS_API_KEY', 'your_api_key_here');
define('IMEJIS_DESIGN_ID', 'your_design_id_here');

Every new post gets a branded featured image the moment you hit Publish. No plugin needed.

Shortcode for on demand generationShortcode for On-Demand Generation

Sometimes you want to generate images inline within a post. A shortcode makes this easy:

<?php
 
add_shortcode('imejis_image', 'imejis_shortcode_handler');
 
function imejis_shortcode_handler($atts) {
    $atts = shortcode_atts([
        'design' => defined('IMEJIS_DESIGN_ID') ? IMEJIS_DESIGN_ID : '',
        'headline' => '',
        'subtitle' => '',
        'alt' => 'Generated image',
    ], $atts);
 
    // Cache with transient to avoid regenerating on every page load
    $cache_key = 'imejis_' . md5(serialize($atts));
    $cached_url = get_transient($cache_key);
 
    if ($cached_url) {
        return sprintf('<img src="%s" alt="%s" loading="lazy" />', esc_url($cached_url), esc_attr($atts['alt']));
    }
 
    $response = wp_remote_post(
        "https://render.imejis.io/v1/{$atts['design']}",
        [
            'timeout' => 30,
            'headers' => [
                'Content-Type' => 'application/json',
                'dma-api-key' => defined('IMEJIS_API_KEY') ? IMEJIS_API_KEY : '',
            ],
            'body' => wp_json_encode([
                'headline' => ['text' => $atts['headline']],
                'subtitle' => ['text' => $atts['subtitle']],
            ]),
        ]
    );
 
    if (is_wp_error($response) || wp_remote_retrieve_response_code($response) !== 200) {
        return '<!-- Imejis image generation failed -->';
    }
 
    $upload = wp_upload_bits("imejis-" . md5($cache_key) . ".png", null, wp_remote_retrieve_body($response));
 
    if ($upload['error']) {
        return '<!-- Image upload failed -->';
    }
 
    $url = $upload['url'];
    set_transient($cache_key, $url, DAY_IN_SECONDS);
 
    return sprintf('<img src="%s" alt="%s" loading="lazy" />', esc_url($url), esc_attr($atts['alt']));
}

Use it in any post or page:

[imejis_image headline="Spring Sale" subtitle="50% off everything" alt="Spring sale promotional banner"]

The transient cache prevents regeneration on every page load. The image generates once, gets cached for 24 hours, and serves from your uploads directory after that.

Error handling retriesError Handling & Retries

API calls fail. Networks time out, servers return 500s, rate limits kick in. Here's a retry wrapper with exponential backoff that I use across PHP projects:

<?php
 
use GuzzleHttp\Client;
use GuzzleHttp\Exception\ConnectException;
use GuzzleHttp\Exception\ServerException;
use Psr\Log\LoggerInterface;
 
class ImageApiClient
{
    private Client $client;
    private LoggerInterface $logger;
    private int $maxRetries;
 
    public function __construct(
        string $apiKey,
        LoggerInterface $logger,
        int $maxRetries = 3
    ) {
        $this->client = new Client([
            'base_uri' => 'https://render.imejis.io/v1/',
            'timeout' => 30,
            'headers' => [
                'Content-Type' => 'application/json',
                'dma-api-key' => $apiKey,
            ],
        ]);
        $this->logger = $logger;
        $this->maxRetries = $maxRetries;
    }
 
    public function generate(string $designId, array $data): string
    {
        $lastException = null;
 
        for ($attempt = 1; $attempt <= $this->maxRetries; $attempt++) {
            try {
                $response = $this->client->post($designId, [
                    'json' => $data,
                ]);
 
                return $response->getBody()->getContents();
 
            } catch (ServerException $e) {
                // 5xx errors — retry
                $lastException = $e;
                $this->logger->warning('API server error, retrying', [
                    'attempt' => $attempt,
                    'status' => $e->getResponse()->getStatusCode(),
                    'design_id' => $designId,
                ]);
            } catch (ConnectException $e) {
                // Network errors — retry
                $lastException = $e;
                $this->logger->warning('Connection failed, retrying', [
                    'attempt' => $attempt,
                    'design_id' => $designId,
                ]);
            }
 
            // Exponential backoff: 1s, 2s, 4s
            if ($attempt < $this->maxRetries) {
                usleep((int)(pow(2, $attempt - 1) * 1_000_000));
            }
        }
 
        $this->logger->error('Image generation failed after all retries', [
            'design_id' => $designId,
            'error' => $lastException?->getMessage(),
        ]);
 
        throw $lastException;
    }
}

Don't retry on 4xx errors. Those mean your request is wrong (bad design ID, invalid payload, missing API key). Retrying won't help. Only retry on 5xx (server issues) and connection failures.

Batch generationBatch Generation

Need to generate 100+ images? Guzzle's concurrent request support makes this fast. Instead of waiting for each image sequentially, you fire off multiple requests at once:

<?php
 
use GuzzleHttp\Client;
use GuzzleHttp\Pool;
use GuzzleHttp\Psr7\Request;
use GuzzleHttp\Psr7\Response;
 
function batchGenerate(array $items, string $designId, string $apiKey): array
{
    $client = new Client([
        'timeout' => 30,
    ]);
 
    $requests = function () use ($items, $designId, $apiKey) {
        foreach ($items as $index => $item) {
            $body = json_encode([
                'headline' => ['text' => $item['title']],
                'subtitle' => ['text' => $item['description'] ?? ''],
            ]);
 
            yield $index => new Request(
                'POST',
                "https://render.imejis.io/v1/{$designId}",
                [
                    'Content-Type' => 'application/json',
                    'dma-api-key' => $apiKey,
                ],
                $body
            );
        }
    };
 
    $results = [];
 
    $pool = new Pool($client, $requests(), [
        'concurrency' => 5,
        'fulfilled' => function (Response $response, int $index) use (&$results, $items) {
            $filename = "output/{$items[$index]['id']}.png";
            file_put_contents($filename, $response->getBody()->getContents());
            $results[$index] = ['status' => 'ok', 'file' => $filename];
        },
        'rejected' => function (\Exception $e, int $index) use (&$results) {
            $results[$index] = ['status' => 'error', 'message' => $e->getMessage()];
        },
    ]);
 
    $pool->promise()->wait();
 
    return $results;
}
 
// Usage
$items = [
    ['id' => 1, 'title' => 'Product A', 'description' => 'Best seller'],
    ['id' => 2, 'title' => 'Product B', 'description' => 'New arrival'],
    ['id' => 3, 'title' => 'Product C', 'description' => 'Limited edition'],
    // ... hundreds more
];
 
$results = batchGenerate($items, 'YOUR_DESIGN_ID', getenv('IMEJIS_API_KEY'));

The concurrency setting controls how many requests run in parallel. Five is a safe starting point. I've pushed it to 10 without issues, but check the API docs for current rate limits.

With 5 concurrent requests and ~1.5 seconds per generation, you can process 200 images per minute. That's a full product catalog in a few minutes.

Tips for productionTips for Production

A few things I've learned running image generation in production PHP apps:

  • Cache design IDs. Don't hardcode them everywhere. Store them in a config file or database. When you update a template on Imejis.io, the design ID stays the same, but having them centralized makes it easy to swap templates.

  • Use environment variables for API keys. Never commit keys to version control. Laravel has .env. WordPress has wp-config.php. Raw PHP can use getenv() with a .env file loaded by vlucas/phpdotenv.

  • Monitor response times. Log how long each generation takes. If response times suddenly spike, you want to know before your users do. A simple microtime(true) before and after the request gives you what you need.

  • Set up health checks. Generate a test image every 5 minutes with a cron job. If it fails, alert your team. This catches issues before they affect real users.

  • Size your timeouts correctly. The API typically responds in 1-2 seconds. A 30-second timeout handles edge cases without making users wait forever. Don't set it to 5 seconds, as complex templates can take longer.

  • Store generated images. Don't call the API every time you need the same image. Generate once, store in S3 or your local filesystem, and serve from there. The batch image generation from CSV guide covers this pattern in detail.

If you're coming from another language, the how to generate images with an API guide covers the fundamentals. We also have specific guides for Node.js and Python.

FaqFAQ

Which http client should i use in phpWhich HTTP client should I use in PHP?

Guzzle is the standard for modern PHP. Laravel includes it by default. For WordPress, wp_remote_post works without extra dependencies. Both handle the API well.

Can i generate images in wordpress without a pluginCan I generate images in WordPress without a plugin?

Yes. Use wp_remote_post in a custom function hooked to save_post. When a post is published, it calls the API and sets the featured image automatically. The example above does exactly this. Just add it to your theme's functions.php.

How do i handle api errors in phpHow do I handle API errors in PHP?

Wrap API calls in try-catch blocks. Check the HTTP status code first. Log failures with context (design ID, payload size). Retry on 5xx errors with exponential backoff. The ImageApiClient class above shows this pattern.

Does the api work with php 7Does the API work with PHP 7?

Yes. The API is a standard REST endpoint. Any PHP version that supports HTTP requests works fine. The code examples here use PHP 8.1+ syntax (named arguments, constructor promotion), but they're easy to adapt for older versions.

Can i generate images asynchronously in phpCan I generate images asynchronously in PHP?

With Guzzle, use promise-based requests to generate multiple images concurrently. Laravel queues are another option: dispatch GenerateImageJob jobs that process in the background while your app stays responsive.

Start buildingStart Building

PHP image generation comes down to one HTTP POST. Whether you're using raw cURL, Guzzle, Laravel's full stack, or WordPress hooks, the pattern is the same: send JSON, get image bytes back.

Pick the integration that fits your stack. If you're on Laravel, start with the service class and queue job. If you're on WordPress, the save_post hook gives you automatic featured images with zero manual effort.

Get started with Imejis.io. Design your first template, grab your API key, and you'll have images generating from PHP in under 10 minutes.