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

> Master batch image generation using the GPT Image API. This guide shows you how to orchestrate multiple API calls for efficient image creation. Learn client-side and server-side techniques.

- Repository: [苍何/awesome-gpt-image-2](https://github.com/freestylefly/awesome-gpt-image-2)
- Tags: how-to-guide
- Published: 2026-09-11

---

**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`](https://github.com/freestylefly/awesome-gpt-image-2/blob/main/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)](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)](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)](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)](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.

```javascript
// 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`](https://github.com/freestylefly/awesome-gpt-image-2/blob/main/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`](https://github.com/freestylefly/awesome-gpt-image-2/blob/main//api/batch-generate-image.js)** that accepts an array of prompts and loops through the reservation logic internally.

```javascript
// /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)](https://github.com/freestylefly/awesome-gpt-image-2/blob/main/api/generation/status.js)).

```javascript
// 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`](https://github.com/freestylefly/awesome-gpt-image-2/blob/main/generate-image.js) (lines 65-78) that you must handle during batch operations:

- **`APIMART_RATE_LIMITED`**: When Apimart returns this code, implement exponential backoff before retrying the specific failed item.
- **`CREDITS_REQUIRED`**: Insufficient credits for a specific reservation; continue processing other batch items.
- **`INVALID_PROMPT`**: Triggered when prompts exceed `APIMART_MAX_PROMPT_LENGTH` (defined in [[`shared/apimart.js`](https://github.com/freestylefly/awesome-gpt-image-2/blob/main/shared/apimart.js)](https://github.com/freestylefly/awesome-gpt-image-2/blob/main/shared/apimart.js)).

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)](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)](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.