Skip to main content

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 CaseSingle APIBatch 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 asynchronousrenderBatch 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

ErrorCauseSolution
INVALID_VARIABLEMissing required variableCheck data completeness
EXPRESSION_ERRORInvalid expression syntaxFix template expressions
IMAGE_FETCH_FAILEDCan’t load external imageVerify image URLs
RENDER_TIMEOUTComplex templateSimplify template

Performance Tips

Optimize Template

  1. Simplify CSS - Avoid complex gradients and shadows
  2. Preload fonts - Use web-safe fonts or inline font data
  3. Optimize images - Use appropriately sized source images
  4. Reduce elements - Fewer DOM elements = faster render

Batch Size

A single batch accepts up to 100 variable sets. Split larger datasets across multiple batches.
ItemsRecommendation
≤ 100Single batch
> 100Split 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
    });
  }
}