Batch Processing
Batch operations let you generate hundreds or thousands of images efficiently from a single API call. Perfect for bulk social cards, certificates, personalized content, and data-driven graphics.
When to Use Batch
| Use Case | Single API | Batch API |
|---|
| 1-10 images | ✅ Simple | ❌ Overkill |
| 10-100 images | ⚠️ Slow | ✅ Better |
| 100+ images | ❌ Rate limits | ✅ Required |
Basic Batch Workflow
1. Prepare Your Data
Structure your data as an array of variable objects:
const posts = [
{ title: 'Getting Started with APIs', author: 'Alice', date: '2026-01-15' },
{ title: 'Advanced JavaScript', author: 'Bob', date: '2026-01-20' },
{ title: 'Cloud Architecture', author: 'Charlie', date: '2026-01-25' },
// ... hundreds more
];
2. Start the Batch Job
Batch rendering is asynchronous — renderBatch returns immediately (HTTP 202) with a batchId.
import { Pictify } from '@pictify/sdk';
const pictify = new Pictify({ apiKey: process.env.PICTIFY_API_KEY! });
const job = await pictify.renderBatch({
templateId: 'tmpl_blog_card',
variableSets: posts.map(post => ({
title: post.title,
author: post.author,
publishDate: post.date
})), // max 100 per batch
format: 'png',
});
console.log(`Batch started: ${job.batchId}`);
console.log(`Processing ${job.totalItems} images`);
3. Monitor Progress
// Poll for status
const checkStatus = async (batchId: string) => {
const status = await pictify.getBatchResults(batchId);
console.log(`Progress: ${status.completedItems}/${status.totalItems}`);
console.log(`Failed: ${status.failedItems}`);
return status.status;
};
// Check every 5 seconds
while (true) {
const status = await checkStatus(job.batchId);
if (['completed', 'partial', 'failed', 'cancelled'].includes(status)) break;
await sleep(5000);
}
4. Collect Results
The poll endpoint does not return rendered URLs. getBatchResults reports per-item { index, success, variables } (plus error on failures). Final image URLs are delivered via the render.completed webhook — subscribe to webhooks to collect batch output (see below).
const status = await pictify.getBatchResults(job.batchId);
for (const item of status.results) {
if (item.success) {
console.log(`Item ${item.index} rendered (vars: ${item.variables.join(', ')})`);
} else {
console.error(`Item ${item.index} failed: ${item.error}`);
}
}
Webhook Integration
Webhooks are how you collect the rendered image URLs from a batch. Configure a webhook subscription in the dashboard (Settings > Webhooks) or via the webhook API, then handle two event types:
render.completed — fired once per rendered image, and carries the url. This is where you collect output.
batch.completed — fired once when the whole job finishes, with success/failure counts.
Setup Webhook Handler
// routes/webhooks.ts — verify the signature first (see /security/webhook-verification)
app.post('/webhooks/pictify', express.raw({ type: 'application/json' }), async (req, res) => {
const event = JSON.parse(req.body.toString());
switch (event.event) {
case 'render.completed':
// Each completed render carries its URL — persist it.
await saveImageUrl(event.data.variables?.title, event.data.url);
break;
case 'batch.completed': {
const { batchId, completedCount, failedCount } = event.data;
console.log(`Batch ${batchId} done — success: ${completedCount}, failed: ${failedCount}`);
break;
}
}
res.sendStatus(200);
});
render.completed Payload
The render events deliver the URLs (the batch poll does not):
{
"event": "render.completed",
"timestamp": "2026-01-29T10:30:00Z",
"data": {
"type": "image",
"source": "api",
"imageId": "img_abc123",
"url": "https://cdn.pictify.io/renders/abc123.png",
"templateId": "tmpl_xyz789",
"variables": { "title": "Getting Started with APIs" },
"renderedAt": "2026-01-29T10:30:00Z"
}
}
batch.completed Payload
{
"event": "batch.completed",
"timestamp": "2026-01-29T10:30:00Z",
"data": {
"batchId": "batch_abc123",
"templateUid": "tmpl_xyz789",
"status": "completed",
"totalCount": 500,
"completedCount": 498,
"failedCount": 2,
"duration": 45000
}
}
Real-World Examples
Social Cards for Blog Posts
// Fetch all posts from your CMS
const posts = await cms.getPosts({ limit: 1000 });
// Prepare variable sets
const variableSets = posts.map(post => ({
title: post.title,
excerpt: post.excerpt.substring(0, 120),
author: post.author.name,
authorAvatar: post.author.avatarUrl,
category: post.category.name,
readTime: `${post.readTime} min read`,
publishDate: formatDate(post.publishedAt)
}));
// Start batch (URLs arrive later via the render.completed webhook)
const job = await pictify.renderBatch({
templateId: 'tmpl_social_card',
variableSets,
format: 'png',
});
// Store batch ID for tracking
await db.batches.create({
batchId: job.batchId,
type: 'social-cards',
totalCount: posts.length,
status: 'processing'
});
Event Certificates
const attendees = [
{ name: 'Alice Johnson', email: 'alice@example.com', ticketId: 'TK001' },
{ name: 'Bob Smith', email: 'bob@example.com', ticketId: 'TK002' },
// ... more attendees
];
const job = await pictify.renderBatch({
templateId: 'tmpl_certificate',
variableSets: attendees.map(a => ({
recipientName: a.name,
eventName: 'Tech Conference 2026',
eventDate: 'January 29, 2026',
certificateId: `CERT-${a.ticketId}`
})),
format: 'png',
});
// Send each certificate as its render.completed webhook arrives —
// that event carries the rendered URL (the batch poll does not).
async function onRenderCompleted(data: { url: string; variables?: Record<string, string> }) {
const certificateId = data.variables?.certificateId;
const attendee = attendees.find(a => `CERT-${a.ticketId}` === certificateId);
if (!attendee) return;
await sendEmail({
to: attendee.email,
subject: 'Your Conference Certificate',
body: `Download your certificate: ${data.url}`
});
}
Product Images
const products = await db.products.findAll();
const job = await pictify.renderBatch({
templateId: 'tmpl_product_card',
variableSets: products.map(p => ({
productName: p.name,
price: formatCurrency(p.price),
originalPrice: p.salePrice ? formatCurrency(p.originalPrice) : null,
discount: p.salePrice ? `${p.discountPercent}% OFF` : null,
imageUrl: p.imageUrl,
rating: p.rating,
reviewCount: p.reviewCount,
badge: p.isNew ? 'NEW' : p.isBestseller ? 'BESTSELLER' : null
})),
format: 'jpeg',
quality: 0.9 // template render quality: 0.1–1.0
});
Handling Failures
Identify Failed Items
const status = await pictify.getBatchResults(batchId);
const failed = status.results.filter(item => !item.success);
console.log(`${failed.length} items failed:`);
for (const item of failed) {
console.log(` Index ${item.index}: ${item.error}`);
console.log(` Variables: ${JSON.stringify(item.variables)}`);
}
Retry Failed Items
async function retryFailedItems(templateId: string, batchId: string) {
const status = await pictify.getBatchResults(batchId);
const failed = status.results.filter(item => !item.success);
if (failed.length === 0) {
console.log('No failed items to retry');
return;
}
// The poll response reports the variable names per item, not the original
// values — look the failed sets back up from your own records.
const failedVariableSets = failed.map(item => originalVariableSets[item.index]);
// Start a new batch with just the failed items
const retryJob = await pictify.renderBatch({
templateId,
variableSets: failedVariableSets,
});
console.log(`Retry batch started: ${retryJob.batchId}`);
}
Common Failure Reasons
| Error | Cause | Solution |
|---|
INVALID_VARIABLE | Missing required variable | Check data completeness |
EXPRESSION_ERROR | Invalid expression syntax | Fix template expressions |
IMAGE_FETCH_FAILED | Can’t load external image | Verify image URLs |
RENDER_TIMEOUT | Complex template | Simplify template |
Optimize Template
- Simplify CSS - Avoid complex gradients and shadows
- Preload fonts - Use web-safe fonts or inline font data
- Optimize images - Use appropriately sized source images
- Reduce elements - Fewer DOM elements = faster render
Batch Size
A single batch accepts up to 100 variable sets. Split larger datasets across multiple batches.
| Items | Recommendation |
|---|
| ≤ 100 | Single batch |
| > 100 | Split into batches of 100 |
// Split large datasets (max 100 variable sets per batch)
const BATCH_SIZE = 100;
for (let i = 0; i < items.length; i += BATCH_SIZE) {
const chunk = items.slice(i, i + BATCH_SIZE);
const job = await pictify.renderBatch({
templateId,
variableSets: chunk,
});
await db.batches.create({ batchId: job.batchId, chunkIndex: i / BATCH_SIZE });
}
Parallel Processing
Pictify processes batch items in parallel. Larger batches are more efficient than multiple small batches.
Monitoring
Track Batch Progress
interface BatchMetrics {
batchId: string;
startedAt: Date;
completedAt?: Date;
totalCount: number;
successCount: number;
failedCount: number;
avgRenderTime?: number;
}
async function trackBatch(batchId: string): Promise<BatchMetrics> {
const status = await pictify.getBatchResults(batchId);
return {
batchId,
startedAt: new Date(status.createdAt),
completedAt: status.completedAt ? new Date(status.completedAt) : undefined,
totalCount: status.totalItems,
successCount: status.completedItems,
failedCount: status.failedItems,
avgRenderTime: status.completedAt
? (new Date(status.completedAt).getTime() -
new Date(status.createdAt).getTime()) / status.totalItems
: undefined
};
}
Set Up Alerts
async function onBatchComplete(event: BatchCompleteEvent) {
const { completedCount, failedCount, totalCount } = event.data;
const failureRate = failedCount / totalCount;
if (failureRate > 0.05) { // > 5% failure rate
await sendAlert({
type: 'batch_high_failure_rate',
message: `Batch ${event.data.batchId} had ${(failureRate * 100).toFixed(1)}% failure rate`,
batchId: event.data.batchId
});
}
}