---
title: "Image API Delivery: Stream, Hosted or Signed"
description: "Image API delivery, decided properly: raw bytes, a permanent URL, or a link that expires. Response shapes, caching behaviour and the 7-day ceiling. Start free."
url: "https://www.imejis.io/blogs/tutorials/image-api-delivery-modes"
published: "2026-09-13"
---

# Image API Delivery: Stream, Hosted or Signed

Image API delivery is a per-request choice between three answers to the same render: hand back the bytes, hand back a permanent URL, or hand back a URL that stops working. The design doesn't change. Only the shape of the response does.

Most people never change it off the default, then hit a bill they didn't expect or an email that leaks a customer's invoice a year later. Both are the same mistake: picking the transport before knowing who reads the image and for how long.

## Image API delivery, in one table

|                    | `stream`                     | `hosted`                        | `signed`                         |
| ------------------ | ---------------------------- | ------------------------------- | -------------------------------- |
| Response body      | The image bytes              | JSON with a URL                 | JSON with a URL                  |
| URL lifetime       | None, there's no URL         | Permanent                       | 60 min default, 7 days max       |
| Serves from cache  | Yes, on an identical payload | No, always renders fresh        | No, always renders fresh         |
| Who should read it | Your own backend             | Anyone with the link            | One recipient, for a while       |
| Typical use        | Save to disk, pipe to a user | README cards, docs, blog images | Invoices, certificates, receipts |

Everything below is the reasoning behind those rows.

## Stream is the default, and the only one that can hit cache

Send nothing and you get bytes:

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

Two things are true of this path and not the other two.

**It can be served from cache.** The renderer keys a cache on the design plus the exact payload. Ask twice for the same thing and the second answer skips rendering. That's a latency win, not a billing one, which the cost section gets to.

**Advisory warnings ride in a header.** The body is the image, so there's nowhere to put a note in it. If a `fontFamily` fell back or an override key matched no dynamic component, the response carries an `x-dma-warnings` header. It's a URI-encoded JSON array and the render still succeeded. Nothing breaks if you ignore it, and reading it in staging catches the typo where an override silently did nothing.

Stream is right when your own code is the reader: writing to disk, piping to a response, pushing into object storage you already run.

## Hosted returns a URL, not a picture

Add `delivery=hosted` and the response changes shape completely:

```json
{
  "success": true,
  "delivery": "hosted",
  "url": "https://storage.googleapis.com/hosted-exports/...png",
  "format": "png",
  "file": { "path": "hosted-exports/DESIGN/1757040000000.png", "size": 184213 }
}
```

That's JSON, not bytes, so a client written to expect binary needs a branch. Worth checking before you flip a production call over.

The URL is permanent and public. Anyone holding it can open it, and nothing revokes it, so hosted is for images that were always going to be public: a card in a README, a diagram in documentation, an open graph image. It's the right default for anything with many readers, because those readers hit storage instead of the render endpoint.

## Signed expires, and 7 days is the ceiling

Same JSON, one extra field:

```json
{
  "success": true,
  "delivery": "signed",
  "url": "https://storage.googleapis.com/signed-exports/...pdf",
  "expiresAt": "2026-09-13T15:04:00.000Z",
  "format": "pdf"
}
```

The default lifetime is 60 minutes. You can raise it with `expiresIn`, in minutes, up to 10,080, which is 7 days. That maximum is a hard bound, not a suggestion, so a design that assumes a 30-day link needs re-architecting rather than a longer number.

Pick a lifetime from the reader's behaviour, not from caution:

- **60 minutes:** an in-app download the user clicked for just now.
- **24 hours:** an email they'll open this evening.
- **7 days:** an invoice or certificate you'd rather not host forever, with a re-render available from your own dashboard when they come back later.

The `expiresAt` timestamp is there so your code can decide. Store it next to the record and re-render on demand once it's passed, rather than emailing a link that quietly stops working.

## The cost model doesn't change

All three cost one render. There is no cheaper mode.

Two details decide the actual bill, and neither is about which mode you picked:

**A cached response still costs a render.** The quota is consumed when the API key is checked, and that happens before the cache lookup. Cache saves you time, not allowance.

**Hosted and signed never touch the cache at all.** They need the rendered buffer to upload it, so they always render fresh. We'd rather say that plainly than let you find it on an invoice. Calling hosted delivery in a loop with an identical payload is the most expensive thing you can do with this API, and it looks harmless in code.

So the saving comes from call frequency. A hosted URL in a page read 50,000 times is one render. A stream URL in the same `<img>` tag is 50,000. If you want the full version of that arithmetic, [cutting render cost at scale](/blogs/tutorials/cost-optimization-image-api-scale) works through it, and [render quota](/help/pricing-and-quota) covers the two counters and how they reset.

## Caching in front of it

A hosted URL is a normal HTTP resource, so ordinary caching applies: put a CDN in front of it and the rules in [RFC 9111](https://www.rfc-editor.org/rfc/rfc9111.html) do what they always do. A signed URL is a poor CDN citizen by design, since the thing making it private is the part that expires. Don't cache those at the edge.

## Picking, in one line each

- **Your backend is the reader.** Stream.
- **Many readers, public content.** Hosted, rendered once on a schedule.
- **One recipient, private content.** Signed, with a lifetime that matches how they'll actually read it.
- **Not sure yet.** Stream, and revisit when a page starts getting traffic.

As of September 2026 all three are on every plan, including a permanent free tier: 100 renders a month. The [API docs](/apis) have the exact parameters, and [generating images with the API](/help/generate-images-with-the-api) is the shorter version if you're setting up the first call today.
