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/guzzleHere'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.
Auto generate featured images on publishAuto-Generate Featured Images on Publish
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 haswp-config.php. Raw PHP can usegetenv()with a.envfile 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.