ScreenshotNeo

BlogEngineering

How to Create Webhooks for Automated Image Generation

Build a secure image-generation webhook: configure events, verify signatures, acknowledge quickly, deduplicate deliveries, and process results reliably.

By the ScreenshotNeo team29 September 202611 min read

How to Create Webhooks for Automated Image Generation

Direct answer: create a public HTTPS endpoint that accepts the image provider’s POST request, verify the signature against the untouched request body, record the event ID, return a 2xx response quickly, and let a worker fetch and process the generated image. Configure the provider to send completion and failure events, then test valid, invalid, duplicate, delayed, and failed deliveries before production.

A webhook is a provider-initiated HTTP request to a URL you control. It lets an image-generation workflow continue after the original API call returns. Instead of polling every few seconds, your application receives a notification when a job starts, produces output, completes, or fails. The exact event names, signature scheme, retry policy, and output retention rules depend on the provider.

1. Webhook architecture for image generation

A dependable integration separates notification from processing:

A webhook acknowledges the event quickly and leaves image processing to a separate worker.
A webhook acknowledges the event quickly and leaves image processing to a separate worker.
  1. Your application creates an image-generation job and stores the provider job or response ID with your own request ID.
  2. The provider sends an HTTPS POST to your webhook when a subscribed event occurs.
  3. Your endpoint validates method, size, payload shape, timestamp, and signature using the raw body.
  4. The endpoint writes an idempotency record and enqueues durable work.
  5. It returns a successful 2xx response immediately.
  6. A worker retrieves the result using the stored provider ID, downloads or transforms it, and publishes it.

Do not make the webhook request wait for image downloads, resizing, moderation, storage, or downstream API calls. Slow or unsuccessful responses can trigger retries, which create duplicate work unless your handler is idempotent.

2. Choose the provider and event model

Decide whether you need every intermediate output or only terminal states. A simple production subscription usually includes completion and failure events. Progress events are useful for a user interface but can create high traffic.

Provider Configuration Events and delivery Verification and retrieval
OpenAI Project-level webhook endpoint with event subscriptions and an HTTPS URL A background response can emit response.completed. Failed or slow deliveries are retried for up to 72 hours with exponential backoff. Redirects are not followed, and duplicate deliveries can occur. Use the signing secret and the provider’s SDK helper or documented verification method. Retrieve the response by the ID in the completion event.
Replicate Include a webhook URL when creating a prediction Filters include start, output, logs, and completed. Output and log notifications are throttled to at most once every 500ms; start and completed events are sent regardless of that throttling. Verify webhook-id, webhook-timestamp, and webhook-signature with the documented HMAC-SHA256 procedure and a timestamp tolerance. Fetch the prediction output using the prediction ID.
Stability AI The reviewed API reference documents image-generation endpoints and API-key authentication. The reviewed reference does not establish an equivalent native webhook workflow. Confirm current capabilities before designing around callbacks. You may need polling or an orchestration layer if no callback is available.

Read the current provider documentation before deployment because event names, payloads, retry behavior, signing formats, and output retention can change. The OpenAI webhook guide and Replicate setup guide are the authoritative starting points for those providers.

3. Create a public HTTPS receiver

Your endpoint must be reachable by the provider, use HTTPS in production, and accept POST requests at a stable route such as POST /webhooks/image-generation. During local development, use a public tunnel such as ngrok or a cloud development environment, then configure the final production URL directly. OpenAI does not follow redirects for webhook delivery.

Minimal receiver contract

  • Accept only the expected HTTP method and path.
  • Set a conservative body-size limit.
  • Preserve the exact raw bytes for signature verification.
  • Reject malformed JSON and unexpected event types.
  • Return a 2xx only after validation and durable enqueueing.
  • Return a non-2xx for invalid signatures or temporary inability to persist the event so the provider can retry where supported.

Node.js and Express receiver

Use the raw-body parser on this route. Do not parse and then re-serialize the JSON before verification.

import express from "express";
import crypto from "node:crypto";

const app = express();
const port = process.env.PORT || 3000;
const replicateSecret = process.env.REPLICATE_WEBHOOK_SECRET;

// Capture raw bytes only for the webhook route.
app.post("/webhooks/image-generation",
  express.raw({ type: "application/json", limit: "256kb" }),
  async (req, res) => {
    try {
      const rawBody = req.body; // Buffer: do not transform before verification
      const event = JSON.parse(rawBody.toString("utf8"));

      if (!replicateSecret) return res.status(500).send("server configuration error");
      if (!verifyReplicate(req.headers, rawBody, replicateSecret)) {
        return res.status(401).send("invalid signature");
      }

      const eventId = String(req.header("webhook-id") || "");
      if (!eventId) return res.status(400).send("missing event id");

      // Replace these with a unique database insert and a durable queue publish.
      const alreadySeen = await eventAlreadyProcessed(eventId);
      if (!alreadySeen) {
        await recordEvent(eventId, event);
        await enqueueImageWork({ eventId, event });
      }

      return res.sendStatus(200);
    } catch (error) {
      console.error(error);
      return res.sendStatus(400);
    }
  }
);

function verifyReplicate(headers, rawBody, secret) {
  const id = headers["webhook-id"];
  const timestamp = headers["webhook-timestamp"];
  const signatures = headers["webhook-signature"];
  if (!id || !timestamp || !signatures) return false;

  const age = Math.abs(Date.now() / 1000 - Number(timestamp));
  if (!Number.isFinite(age) || age > 300) return false;

  // Replicate's signing key contains a base64 portion after its prefix.
  const keyPart = String(secret).split("_").pop();
  const key = Buffer.from(keyPart, "base64");
  const signed = `${id}.${timestamp}.${rawBody.toString("utf8")}`;
  const expected = crypto.createHmac("sha256", key).update(signed).digest("base64");

  return String(signatures).split(" ").some(value => {
    const supplied = value.replace(/^v\d+,/, "");
    const a = Buffer.from(supplied);
    const b = Buffer.from(expected);
    return a.length === b.length && crypto.timingSafeEqual(a, b);
  });
}

async function eventAlreadyProcessed(id) { return false; }
async function recordEvent(id, event) {}
async function enqueueImageWork(work) {}

app.listen(port, () => console.log(`listening on ${port}`));

For OpenAI, use its current SDK webhook helper rather than copying a Replicate verifier. The OpenAI guide specifically demonstrates retaining the raw text body in Express and verifying it with the SDK.

Python and Flask receiver

import hashlib
import hmac
import json
import os
import time
from flask import Flask, request, Response

app = Flask(__name__)
SECRET = os.environ["REPLICATE_WEBHOOK_SECRET"]

@app.post("/webhooks/image-generation")
def image_webhook():
    raw = request.get_data(cache=True)  # exact bytes received
    event_id = request.headers.get("webhook-id")
    timestamp = request.headers.get("webhook-timestamp")
    signatures = request.headers.get("webhook-signature", "")

    if not event_id or not timestamp:
        return Response("missing headers", status=400)
    try:
        if abs(time.time() - float(timestamp)) > 300:
            return Response("stale event", status=401)
    except ValueError:
        return Response("bad timestamp", status=401)

    key_part = SECRET.rsplit("_", 1)[-1]
    import base64
    key = base64.b64decode(key_part)
    signed = f"{event_id}.{timestamp}.".encode() + raw
    expected = base64.b64encode(hmac.new(key, signed, hashlib.sha256).digest()).decode()
    valid = any(hmac.compare_digest(value.split(",", 1)[-1], expected)
                for value in signatures.split())
    if not valid:
        return Response("invalid signature", status=401)

    try:
        event = json.loads(raw)
    except json.JSONDecodeError:
        return Response("invalid json", status=400)

    # Insert event_id with a unique constraint, then enqueue the event.
    if not event_already_seen(event_id):
        record_event(event_id, event)
        enqueue_image_work(event_id, event)
    return Response(status=200)

def event_already_seen(event_id):
    return False

def record_event(event_id, event):
    pass

def enqueue_image_work(event_id, event):
    pass

if __name__ == "__main__":
    app.run(port=3000)

In a real service, replace the placeholder functions with a database transaction and a durable queue. The unique event-ID constraint is what makes retries safe.

4. Start a job and preserve its identity

When you request generation, create your own request record before or atomically with the provider call. Store:

  • your internal request ID;
  • the provider job, prediction, or response ID;
  • the requested model and parameters;
  • the intended user, project, or storage destination;
  • the current state and timestamps.

Use the provider ID from the verified event to look up this record. Do not trust arbitrary client-supplied routing fields. This mapping is an implementation pattern inferred from the provider event identifiers and documented result retrieval flows.

Replicate request with cURL

curl -X POST "https://api.replicate.com/v1/predictions" \
  -H "Authorization: Bearer $REPLICATE_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "version": "MODEL_VERSION_ID",
    "input": {"prompt": "a red kite over a coastal cliff"},
    "webhook": "https://example.com/webhooks/image-generation",
    "webhook_events_filter": ["completed"]
  }'

Replace the model version, input schema, and endpoint with the values required by the model you selected. A completion event is a signal to retrieve the prediction; do not assume the callback contains a permanent image URL.

5. Verify signatures correctly

Signature verification must happen before any side effect. Keep secrets in server-side secret storage, never in browser JavaScript or source control. Preserve the raw request body. Parsing and re-serializing JSON can change whitespace or key ordering and invalidate a correct signature.

Replicate signs the concatenation of webhook ID, timestamp, and raw body with HMAC-SHA256 using the base64 key portion of its signing key. Compare signatures in constant time and reject timestamps outside a tolerance window to reduce replay risk. OpenAI provides SDK helpers and advises verification, especially when events trigger backend actions; rotate a signing secret if it is exposed.

Also validate the event type and payload shape after signature verification. A validly signed event can still be an event your route is not prepared to process.

6. Acknowledge, deduplicate, and queue

OpenAI recommends responding quickly with a successful 2xx status. Its documented delivery process can retry unsuccessful or slow deliveries for up to 72 hours with exponential backoff. A 3xx redirect counts as a failure. Since duplicate deliveries are possible, treat the provider’s event ID as an idempotency key.

Use this transaction order:

  1. Verify the signature and timestamp.
  2. Insert the event ID into an events table with a unique constraint.
  3. Insert an outbox record or publish to a durable queue.
  4. Commit the transaction.
  5. Return 200.

If the event already exists, return 200 without repeating publication, billing, or other irreversible actions. If enqueueing fails, return a non-2xx so a provider with retries can attempt delivery again.

7. Fetch and process the generated image

The webhook is a state notification. A worker should use the stored provider ID and documented result endpoint to retrieve the output, then download it to storage you control when appropriate. Verify that the job is in a terminal success state before publishing it. Handle failure and cancellation as first-class outcomes.

Do not assume every provider includes a durable image URL in the callback or retains output forever. Check current retention documentation, then choose whether to copy the file to object storage, apply transformations, run moderation, generate thumbnails, or notify your application.

8. Testing checklist

  • Send a valid signed completion event and confirm one queued job.
  • Send the same event twice and confirm one side effect.
  • Change one byte of the body and confirm a 401 response.
  • Use an old timestamp and confirm replay rejection.
  • Send malformed JSON and an unknown event type.
  • Exercise failed and canceled generation states.
  • Delay the worker while confirming the webhook still returns quickly.
  • Force a queue or database outage and confirm the endpoint returns a retryable error.
  • Use provider test events where available; OpenAI exposes webhook test events in dashboard settings.

9. Troubleshooting common webhook errors

Symptom Likely cause Fix
No delivery arrives Endpoint is private, DNS is wrong, TLS fails, or the provider has a different configured URL Open the exact URL from an external network, inspect TLS and server logs, and configure the final HTTPS URL without a redirect.
Every signature is invalid Body was parsed before verification, the wrong secret is used, or the signed string is incorrect Capture raw bytes, load the active secret from server configuration, and follow the provider’s current algorithm exactly.
Repeated events create duplicate images No unique event-ID record or the check and insert race each other Use a database unique constraint and make the insert plus enqueue operation idempotent.
Provider keeps retrying Non-2xx response, timeout, application exception, or a redirect Return 2xx after durable enqueueing, keep work out of the request path, and inspect response logs.
Images are missing later Provider output URLs expired or were not fetched Download successful outputs in a worker and store them according to your retention and access requirements.
Progress overwhelms the service Subscribed to output or log events that arrive frequently Subscribe only to terminal events, or throttle and aggregate progress updates.

10. Performance, reliability, and cost

Keep the receiver small and horizontally scalable. A fast signature check, database write, and queue publish are usually enough work for the request. Set connection and body limits, monitor latency and non-2xx rates, and alert on a growing queue or repeated delivery failures.

Retries can multiply downstream costs if your worker is not idempotent. Charge, publish, or notify only after the event ID has been accepted once. Cache provider results by provider ID where the provider’s terms permit it. For high-volume progress events, subscribe to completion only or aggregate updates.

Separate infrastructure cost from model cost: webhook requests consume server, database, queue, and storage resources, while image generation is billed by the provider according to its own plan. Retaining original images and derivatives can become a larger storage expense than the callback itself.

11. Or skip the browser setup

If your workflow needs screenshots of generated-image landing pages, review pages, or status dashboards, ScreenshotNeo provides a website screenshot API and MCP server. One GET request returns PNG, JPEG, WebP, or PDF. It removes cookie banners, newsletter popups, and chat widgets before capture, and failed loads, bot checks, blank pages, timeouts, and cache hits are not billed. Responses identify the result with X-Page-Verdict and X-Billed headers. Its MCP server exposes take_screenshot, get_page_info, and capture_pdf for Claude, Cursor, and other MCP clients.

Clean capture services can remove consent banners and overlays before saving a result.
Clean capture services can remove consent banners and overlays before saving a result.

See the ScreenshotNeo API documentation for all options, including signed webhooks for asynchronous jobs. A direct capture looks like this:

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

There is a free allowance of 1,000 screenshots per month with no card. Paid plans start at $5 for 3,000 shots, and every feature is available on every plan. Create a free ScreenshotNeo account to try it.

12. Frequently asked questions

Can a webhook endpoint be on localhost?

Not directly. The provider needs a publicly reachable URL. Use a public tunnel for development and deploy an HTTPS endpoint for production.

Should I poll as well as use webhooks?

Use polling as a recovery mechanism when the provider documents a result endpoint, not as the primary path. A scheduled reconciler can find jobs that have no terminal event after an expected interval.

What status should an invalid signature return?

Return a 4xx such as 401 and do not enqueue work. Log enough metadata to investigate without logging secrets or sensitive image payloads.

How long should webhook data be stored?

Keep event IDs and processing state long enough to cover the provider’s retry window and your own replay and audit needs. Store image files according to your privacy, retention, and provider requirements.

What if the provider has no webhooks?

Use a polling worker or an orchestration service with exponential backoff, then verify current provider documentation before committing to that design.