---
title: "Image API Webhook vs Direct Response Explained"
description: "Image API webhook or a direct response? What async really costs in code, the failure modes each adds, and when a callback is genuinely the right design."
url: "https://www.imejis.io/blogs/comparisons/image-api-webhook-vs-direct"
published: "2026-10-04"
---

# Image API Webhook vs Direct Response Explained

An image API webhook and a direct response solve the same problem in two places. Either the API holds the connection and hands you the finished image, or it takes the job, answers immediately, and calls you back later at a URL you operate.

The choice looks like a preference and isn't. It decides how much infrastructure sits between your code and a picture, and for static images the honest answer is that most of that infrastructure exists to solve a problem static images don't have.

## Image API webhook or direct response, side by side

|                                   | Direct response             | Webhook callback                         |
| --------------------------------- | --------------------------- | ---------------------------------------- |
| Calls to get an image             | 1                           | 2, plus an inbound request               |
| Public endpoint needed            | No                          | Yes                                      |
| Correlating a result to a request | Nothing to do               | Job id to record, and store              |
| Local testing                     | A terminal                  | A tunnel to your laptop                  |
| Failure mode                      | The call fails, you retry   | The callback never arrives, and now what |
| Who holds the wait                | Your caller, for the render | Your queue, indefinitely                 |

Neither column is universally right. The second column exists because some renders genuinely take minutes.

## What async actually costs

A webhook integration is never just a webhook. It's five pieces, and each one is code somebody maintains:

1. **A public endpoint.** Reachable from the vendor, which means it's reachable from the internet, which means it needs its own hardening.
2. **Signature verification.** Otherwise anyone who learns the URL can tell you a render finished.
3. **Correlation.** The callback carries a job id. Something has to remember which user, order, or article that job was for.
4. **In-flight storage.** A row per pending render, and a definition of how long is too long.
5. **A retry or reconciliation path.** For the callbacks that don't arrive, which is not a hypothetical.

None of that is hard. All of it is real, and it's why an "image generation" ticket that should take an hour takes a day. It also has to exist before you can render your first image, so the cost lands entirely up front.

## What a direct response costs

Honesty demands the other column too, because it isn't free.

**Your caller waits.** The render happens while the connection is open, typically under 2 seconds. Inside a background job that's irrelevant. Inside a web request that a user is watching, 2 seconds is a real budget you're spending.

**Bulk work needs concurrency, not a queue.** Generating 5,000 images means running calls in parallel from a worker. That's straightforward. It is a different mental model from firing 5,000 jobs and waiting for callbacks to trickle back in over the next few minutes, and worth knowing before you start.

**A timeout is your problem.** If the connection drops, you retry the call. With a callback the vendor's queue would have retried for you.

The trade is that all three of those are handled where you already handle everything else: your retry policy, your worker pool, your timeout. There's no second system with its own state.

## The failure modes are different, not equal

This is the part that decides it for most teams.

**A direct call fails loudly.** The request returns an error or times out, in the code path that asked for the image, where the context still exists. You retry, or you fall back, and the person who wrote that line is the person who reads the log.

Retrying is safer here than the HTTP method suggests. A render is a POST, which [RFC 9110](ref:rfc-editor/rfc/rfc9110.html#name-idempotent-methods) does not classify as idempotent, but the render itself is deterministic: the same design and the same data produce the same image. So a repeat is safe in effect. It does cost a second render against your quota, which is the one thing to watch in a retry loop.

**A missing callback fails silently.** Nothing errors. A row sits in your in-flight table forever, and the user never gets their image. You find out days later, from a support ticket, and the debugging starts with "did they send it, or did we drop it".

Silent failures across a trust boundary are the expensive kind. That's the real argument here, more than the line count.

## When a callback genuinely is the right design

Async exists for good reasons, and pretending otherwise would be marketing rather than advice.

- **Video.** Rendering a clip takes minutes. Holding a connection for that is wrong, and every video API is async for this reason. We don't generate video at all, so if that's your workload the whole comparison is moot.
- **Multi-page documents.** A long PDF is closer to video than to an image in render time.
- **Very large batches with a vendor-side queue.** If the provider will absorb 100,000 jobs and drip results back, that's genuinely less work than operating your own worker pool.
- **Serverless with tight execution limits.** If your platform kills a function at 1 second, holding a connection for a render doesn't fit and a callback is the honest answer.

For a static image that finishes in about a second, none of these apply. That's the case where the callback is protecting you from a wait that isn't happening.

## A URL is a separate question from async

Worth untangling, because they get conflated. "I want a URL, not bytes" doesn't require a callback.

The same synchronous call can return a stored URL instead of the image, by asking for hosted or signed delivery:

```bash
curl -X POST 'https://render.imejis.io/v1/YOUR_DESIGN_ID?delivery=hosted' \
  -H 'dma-api-key: YOUR_KEY' \
  -H 'Content-Type: application/json' \
  -d '{ "headline": { "text": "Q3 results" } }'
```

```json
{
  "success": true,
  "delivery": "hosted",
  "url": "https://storage.googleapis.com/...png",
  "format": "png"
}
```

One request, a URL back, no endpoint to operate. [Delivery modes](/blogs/tutorials/image-api-delivery-modes) covers the differences between stream, hosted and signed, including the expiry bounds on signed URLs.

## Choosing, in practice

- **Static images, any volume.** Direct response. Add concurrency when volume needs it.
- **Video or long documents.** Async, and pick a vendor built for it.
- **Hard execution limits in your runtime.** Async, or move the render to a worker without one.
- **You want a URL.** That's a delivery setting, not an architecture.

If you're weighing specific vendors on this, [Bannerbear vs Imejis](/blogs/comparisons/bannerbear-vs-imejis) covers the two models head to head, and [migrating from Bannerbear](/blogs/comparisons/migrate-from-bannerbear) shows exactly which code disappears when you move from one to the other. The [API docs](/apis) have synchronous examples in 33 languages if you'd rather just try it.
