Image API Delivery: Stream, Hosted or Signed

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 tableImage API delivery, in one table

streamhostedsigned
Response bodyThe image bytesJSON with a URLJSON with a URL
URL lifetimeNone, there's no URLPermanent60 min default, 7 days max
Serves from cacheYes, on an identical payloadNo, always renders freshNo, always renders fresh
Who should read itYour own backendAnyone with the linkOne recipient, for a while
Typical useSave to disk, pipe to a userREADME cards, docs, blog imagesInvoices, certificates, receipts

Everything below is the reasoning behind those rows.

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

Send nothing and you get bytes:

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 pictureHosted returns a URL, not a picture

Add delivery=hosted and the response changes shape completely:

{
  "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 ceilingSigned expires, and 7 days is the ceiling

Same JSON, one extra field:

{
  "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 doesnt changeThe 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 works through it, and render quota covers the two counters and how they reset.

Caching in front of itCaching 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 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 eachPicking, 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 have the exact parameters, and generating images with the API is the shorter version if you're setting up the first call today.

Frequently Asked Questions

Three. Stream returns the raw image bytes in the response body and is the default. Hosted stores the render and returns a permanent public URL as JSON. Signed stores it too but returns a time-limited URL with an expiry timestamp.

60 minutes by default. You can set any lifetime up to 7 days, which is 10,080 minutes and a hard ceiling. The response includes an expiresAt timestamp, so your code can decide whether to re-render rather than guessing.

No. Both return a small JSON body containing the URL, the format, and the stored file's path and size. Only stream delivery puts image bytes in the response, which matters if your client is written to expect binary.

They cost the same, one render each. The saving comes from how often you call. A hosted URL serves many readers from storage on a single render, while a stream call embedded in a page renders again for every view.

Yes. Delivery is a per-request choice, set with the delivery query parameter or the x-dma-delivery header. The same design can stream to your backend and answer with a hosted URL for an email in the same afternoon.