How to Implement Batch Image Generation with the GPT Image API: A Complete Guide

Batch image generation is implemented by orchestrating multiple calls to the /api/generate-image endpoint, either through client-side parallel requests or a server-side wrapper that loops through the existing reservation and submission logic while reusing the core utilities in /api/_lib.

The freestylefly/awesome-gpt-image-2 repository provides a robust single-image generation pipeline that you can extend to process multiple prompts efficiently. By understanding how the core generate-image.js endpoint handles reservations and Apimart submissions, you can build batch workflows that maintain credit accounting, error isolation, and rate-limit compliance across multiple images.

Understanding the Single-Image Architecture

The foundation for batch processing lies in the /api/generate-image endpoint defined in [api/generate-image.js](https://github.com/freestylefly/awesome-gpt-image-2/blob/main/api/generate-image.js). This endpoint follows a strict four-step flow:

  1. Authentication and validation via getAuthContext to verify user credentials.
  2. Credit reservation through reserveGeneration (lines 30-49 in [api/_lib/generation.js](https://github.com/freestylefly/awesome-gpt-image-2/blob/main/api/_lib/generation.js)), which creates a database row with a unique reservation_id and ensures sufficient credits.
  3. Task submission via submitApimartGeneration (lines 41-48 in [api/_lib/apimart.js](https://github.com/freestylefly/awesome-gpt-image-2/blob/main/api/_lib/apimart.js)), which returns a taskId from the Apimart provider.
  4. Immediate response with HTTP 202 status, returning the taskId and initial status payload.

Final image retrieval happens through a separate GET request to /api/generation ([api/generation/status.js](https://github.com/freestylefly/awesome-gpt-image-2/blob/main/api/generation/status.js)), which polls the Apimart task and settles the reservation upon completion.

Because the system processes one prompt per reservation, batch generation requires orchestrating multiple instances of this flow while respecting the existing credit checks and error-handling constraints.

Two Approaches to Batch Image Generation

You can implement batch processing using either client-side parallelism or a dedicated server-side endpoint. Both strategies reuse the existing utilities in /api/_lib and inherit the same validation logic.

Client-Side Parallel Requests

The simplest approach fires multiple POST requests to /api/generate-image concurrently from your frontend or Node.js client. This method requires no server modifications and isolates failures per image.

// batchGenerate.js
const API_BASE = '/api/generate-image';

async function generateOne(prompt, caseId, language = 'en') {
  const res = await fetch(API_BASE, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ prompt, caseId, language })
  });
  return res.json();
}

export async function batchGenerate(requests) {
  // Execute all requests simultaneously using Promise.allSettled
  const promises = requests.map(req => 
    generateOne(req.prompt, req.caseId, req.language)
  );
  
  const results = await Promise.allSettled(promises);
  
  return results.map(r => 
    r.status === 'fulfilled' ? r.value : { ok: false, error: r.reason }
  );
}

// Usage example
const batch = [
  { prompt: 'A futuristic cityscape', caseId: 101 },
  { prompt: 'A medieval knight portrait', caseId: 102 },
  { prompt: 'A vibrant tropical beach', caseId: 103 }
];

const responses = await batchGenerate(batch);

Why this works: Each call independently executes the full reservation → submission flow in generate-image.js. If one prompt triggers a CREDITS_REQUIRED error, other requests continue unaffected. The Promise.allSettled pattern ensures you receive results for all prompts, including partial failures.

Server-Side Batch Wrapper

For reduced network overhead and atomic-like behavior, create a new endpoint at /api/batch-generate-image.js that accepts an array of prompts and loops through the reservation logic internally.

// /api/batch-generate-image.js
import { getAuthContext, isSupabaseServerConfigured } from './_lib/supabase.js';
import { getApimartConfig, submitApimartGeneration } from './_lib/apimart.js';
import { reserveGeneration, releaseReservation, getGenerationResponseUser } from './_lib/generation.js';
import { APIMART_MAX_PROMPT_LENGTH } from '../shared/apimart.js';

export default async function handler(req, res) {
  // Standard validation from generate-image.js
  if (req.method !== 'POST') {
    return res.status(405).json({ ok: false, error: 'METHOD_NOT_ALLOWED' });
  }
  
  if (!getApimartConfig().configured || !isSupabaseServerConfigured()) {
    return res.status(500).json({ ok: false, error: 'SERVER_NOT_CONFIGURED' });
  }

  const auth = await getAuthContext(req);
  if (auth.error) return res.status(401).json({ ok: false, error: auth.error });

  const { requests: batch } = req.body;
  const results = [];
  const appUrl = process.env.APP_URL?.replace(/\/$/, '');

  for (const item of batch) {
    const { prompt, caseId, language = 'en' } = item;
    
    // Validate prompt length using shared constant
    if (!prompt?.trim() || prompt.length > APIMART_MAX_PROMPT_LENGTH) {
      results.push({ ok: false, error: 'INVALID_PROMPT' });
      continue;
    }

    try {
      // Reserve credits for this specific prompt
      const reservation = await reserveGeneration(
        auth.client,
        auth.profile,
        auth.user.id,
        Number(caseId),
        prompt
      );

      // Submit to Apimart
      const submitted = await submitApimartGeneration({
        apiKey: getApimartConfig().apiKey,
        prompt,
        language,
        webhook: appUrl ? `${appUrl}/api/generation` : ''
      });

      // Link reservation to provider task
      await auth.client
        .from('generation_reservations')
        .update({ 
          provider: 'apimart', 
          provider_task_id: submitted.taskId 
        })
        .eq('id', reservation.reservationId)
        .eq('user_id', auth.user.id);

      results.push({ 
        ok: true, 
        taskId: submitted.taskId, 
        status: submitted.status 
      });
      
    } catch (err) {
      const code = err?.code === 'CREDITS_REQUIRED' 
        ? 'CREDITS_REQUIRED' 
        : 'GENERATION_FAILED';
        
      results.push({ ok: false, error: code });
      
      // Critical: Release reservation on failure to refund credits
      if (err.reservationId) {
        await releaseReservation(auth.client, err.reservationId, code);
      }
    }
  }

  return res.status(200).json({ 
    ok: true, 
    batch: results,
    user: await getGenerationResponseUser(auth.client, auth.user.id)
  });
}

Key advantages of this approach:

  • Single HTTP round-trip reduces mobile client latency.
  • Continued processing ensures one invalid prompt doesn't abort the entire batch.
  • Automatic cleanup calls releaseReservation when submitApimartGeneration fails, preventing credit lock-up.
  • Consistent credit accounting maintains the one-credit-per-image business rule.

Polling Results for the Complete Batch

After obtaining an array of taskId values from either batch method, retrieve the final images using the existing /api/generation endpoint ([api/generation/status.js](https://github.com/freestylefly/awesome-gpt-image-2/blob/main/api/generation/status.js)).

// pollBatchResults.js
export async function pollBatch(taskIds, interval = 3000) {
  const pending = new Set(taskIds);
  const results = {};

  while (pending.size) {
    await Promise.all([...pending].map(async taskId => {
      const res = await fetch(`/api/generation?taskId=${taskId}`);
      const data = await res.json();
      
      if (data.ok && ['completed', 'failed'].includes(data.status)) {
        results[taskId] = data;
        pending.delete(taskId);
      }
    }));
    
    if (pending.size) await new Promise(r => setTimeout(r, interval));
  }
  
  return results;
}

This utility polls every task ID simultaneously, removing completed tasks from the pending set until all images are either generated or failed.

Error Handling and Rate Limiting

The codebase defines specific error mappings in generate-image.js (lines 65-78) that you must handle during batch operations:

Always check the ok boolean in responses rather than HTTP status codes alone, as the API returns 200 OK even for generation failures to indicate successful transmission of the error state.

Summary

  • The single-image pipeline in /api/generate-image handles validation, credit reservation via reserveGeneration, and Apimart submission via submitApimartGeneration.
  • Client-side batching uses Promise.allSettled to fire parallel requests without server modifications, isolating failures per image.
  • Server-side batching creates a wrapper endpoint that loops through the same reservation logic, providing cleanup via releaseReservation and reducing network hops.
  • Result retrieval requires polling /api/generation for each taskId returned by the batch process.
  • Error handling must account for APIMART_RATE_LIMITED and CREDITS_REQUIRED codes while ensuring reservations are released on failure to maintain credit integrity.

Frequently Asked Questions

How does the API handle credit deductions for batch requests?

Each individual prompt in a batch requires a separate call to reserveGeneration (found in [api/_lib/generation.js](https://github.com/freestylefly/awesome-gpt-image-2/blob/main/api/_lib/generation.js)), which checks available credits and creates a database reservation before submitting to Apimart. Credits are deducted per image, not per batch, ensuring accurate accounting even when partial failures occur.

What is the maximum number of images I can generate in one batch?

The repository does not enforce a hard limit on batch size, but you must respect the Apimart rate limits signaled by the APIMART_RATE_LIMITED error code. For large batches, implement client-side throttling or server-side queues to avoid overwhelming the upstream provider.

Can I mix different languages in a single batch request?

Yes. The language parameter is passed directly to submitApimartGeneration in [api/_lib/apimart.js](https://github.com/freestylefly/awesome-gpt-image-2/blob/main/api/_lib/apimart.js). When using the server-side wrapper approach, you can specify different languages for each object in the requests array, and the system will submit each prompt with its corresponding language code.

How do I handle failed generations within a batch?

Use Promise.allSettled for client-side batches to capture both successes and failures without stopping the entire operation. For server-side implementations, wrap each iteration in a try-catch block that calls releaseReservation (passing the reservationId) if submitApimartGeneration throws an error. This refunds credits for failed attempts and allows the loop to continue processing remaining prompts.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →