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:
- Authentication and validation via
getAuthContextto verify user credentials. - 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 uniquereservation_idand ensures sufficient credits. - 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 ataskIdfrom the Apimart provider. - Immediate response with HTTP 202 status, returning the
taskIdand 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
releaseReservationwhensubmitApimartGenerationfails, 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:
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 exceedAPIMART_MAX_PROMPT_LENGTH(defined in [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-imagehandles validation, credit reservation viareserveGeneration, and Apimart submission viasubmitApimartGeneration. - Client-side batching uses
Promise.allSettledto 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
releaseReservationand reducing network hops. - Result retrieval requires polling
/api/generationfor eachtaskIdreturned by the batch process. - Error handling must account for
APIMART_RATE_LIMITEDandCREDITS_REQUIREDcodes 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →