How the Webhook System Handles Async Generation Results in Awesome GPT‑Image‑2
The webhook system forwards generation requests to APIMart with a callback URL, receives asynchronous completion notifications at /api/generation, reconciles the task status against the database, and settles reservations via Supabase stored procedures.
The Awesome GPT‑Image‑2 repository implements a robust webhook system for async generation results to handle compute‑intensive image creation from the APIMart service. Because the generation process runs asynchronously outside the HTTP request cycle, the server cannot return final images in the initial response. Instead, the architecture pushes task completion states to a dedicated callback endpoint, reconciles them against pending database reservations, and optionally exposes endpoints for client polling.
Submitting the Generation Request with a Webhook URL
When a client initiates an image generation, the server in api/generate-image.js constructs an absolute webhook URL from the environment configuration and includes it in the payload sent to APIMart.
The system reads the APP_URL environment variable, strips trailing slashes, and appends /api/generation to form the callback address:
const appUrl = String(process.env.APP_URL || '').replace(/\/$/, '');
const submitted = await submitApimartGeneration({
apiKey: config.apiKey,
prompt,
language: body.language,
webhook: appUrl ? `${appUrl}/api/generation` : '' // ← callback endpoint
});
The submitApimartGeneration function (located in api/_lib/apimart.js) transmits this URL to the APIMart API, which stores it and invokes a POST request upon task completion or failure.
Receiving and Validating the Callback
The webhook listener resides in api/generation/callback.js. When APIMart calls the endpoint, the handler first validates the HTTP method and parses the JSON body using readBody.
It then extracts the task identifier via extractApimartTaskId and validates its format with isValidApimartTaskId:
const taskId = extractApimartTaskId(payload);
if (!isValidApimartTaskId(taskId)) {
// Return early for malformed identifiers
}
Only properly formatted task IDs proceed to the reconciliation phase.
Reconciliation Logic and Database Settlement
The core processing occurs inside reconcileApimartCallback, an exported function that synchronizes external task states with internal database records. The logic follows four distinct steps:
1. Locate the Pending Reservation
The system queries the generation_reservations table via findPlatformGeneration, searching for a row where provider_task_id equals the incoming taskId and status equals pending.
2. Handle Missing or Duplicate Callbacks
If no reservation exists, the callback is ignored. If the reservation status is already settled, the request is treated as a duplicate:
const reservation = await findReservation(client, taskId);
if (!reservation) return { state: 'ignored' };
if (reservation.status !== 'pending') return { state: 'duplicate' };
3. Verify External Task Status
The function fetches the latest task metadata from APIMart using getApimartTask. It proceeds only when the external status is completed or failed, waiting otherwise.
const verifiedTask = await getApimartTask({ apiKey, taskId });
if (!['completed', 'failed'].includes(verifiedTask.status)) {
return { state: 'pending' };
}
4. Settle the Reservation
Upon verification, settlePlatformGeneration updates the reservation row with final provider fields (cost, result URL, expiration) and invokes Supabase stored procedures:
complete_generation_reservationfor successful generationsrelease_generation_reservationfor failures, passing the appropriate error code
Webhook Response Patterns
The endpoint returns specific HTTP status codes and JSON payloads to communicate processing outcomes to APIMart:
202 Acceptedwith{ ignored: true }— No matching reservation found in the database.200 OKwith{ duplicate: true }— The reservation was already settled; no action taken.202 Acceptedwith{ pending: true }— The external task remains in progress; APIMart should retry later.200 OKwith{ ok: true }— The reservation has been successfully settled.
If reconciliation throws an unhandled exception, the endpoint returns 503 Service Unavailable with the error code CALLBACK_RECONCILIATION_FAILED, signaling APIMart to retry the delivery.
Optional Client-Side Polling
Although the webhook updates the database automatically, the front‑end can query real‑time status via the polling endpoint at GET /api/generation/status (implemented in api/generation/status.js).
This endpoint reads the reservation row and returns stored results if already settled. If the status remains pending, it queries APIMart on‑the‑fly via getApimartTask to return fresh state without waiting for the webhook push.
Summary
- Submission:
api/generate-image.jsbuilds the webhook URL fromAPP_URLand registers it with APIMart viasubmitApimartGeneration. - Callback:
api/generation/callback.jsreceives POST requests, validates task IDs, and delegates toreconcileApimartCallback. - Reconciliation: The system matches callbacks to
generation_reservationsrows, verifies terminal states (completed/failed), and settles them viacomplete_generation_reservationorrelease_generation_reservation. - Responses: Structured JSON responses inform APIMart whether to retry (202), acknowledge (200), or error (503).
- Polling:
api/generation/status.jsprovides a fallback pull mechanism for client applications.
Frequently Asked Questions
What happens if the webhook callback fails or retries?
If the APIMart POST to /api/generation fails or returns a 503, APIMart will retry the delivery according to its own backoff policy. The reconciliation logic in reconcileApimartCallback is idempotent; duplicate callbacks for already‑settled reservations return { duplicate: true } with HTTP 200, preventing double‑processing.
How does the system prevent duplicate processing of the same generation task?
Before settling, reconcileApimartCallback checks the reservation status via findPlatformGeneration. If the row status is not pending, the function immediately returns { state: 'duplicate' }. This ensures that only the first successful callback triggers the stored procedure, while subsequent retries are acknowledged but ignored.
Can the webhook endpoint URL be customized to a different route?
The webhook URL is dynamically constructed in api/generate-image.js using the APP_URL environment variable hardcoded to the /api/generation path. To change the endpoint, you must modify the string interpolation ${appUrl}/api/generation in the submission logic and create a corresponding handler file at the new path.
What is the difference between the webhook callback and the status polling endpoint?
The webhook callback at /api/generation is a push‑mechanism invoked by APIMart when a task finishes; it updates the database proactively. The status polling endpoint at /api/generation/status is a pull‑mechanism that clients can query repeatedly; it reads from the database or queries APIMart directly if the result is still pending, enabling real‑time UI updates without WebSocket connections.
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 →