Use case · Personalized email images

A hero image with the recipient's name baked right in.

Personalized email images — name, stat, certificate number, milestone — rendered per recipient and served from a CDN. One URL per send, one render, open rates up. Drop into any <img src> in your transactional or lifecycle email.

The problem

Generic hero images get skimmed past. Personalized ones get attention — but you can't ship them with handlebars. Email clients render <img> URLs, not HTML templates. You need one URL per recipient that resolves to their personalized PNG.

Bannerbear does this but starts at $49/mo template-first; for devs who already have HTML, that workflow is upside-down. You shouldn't need to rebuild your design in someone else's drag-and-drop UI just to substitute a merge tag.

Three approaches, one API

1. At send time

For campaigns

Loop through your recipient list, render each variant, get the hosted URL back, inject it into a merge field on your ESP. One campaign, N personalized hero images, all cached at the edge.

2. Webhook on signup

For lifecycle

One-shot render the moment a user signs up. Store the hosted URL on the user record. Every future welcome, onboarding, and weekly summary email reuses the same hosted URL — zero send-time latency.

3. Trigger-based

For transactional

Event fires (order placed, milestone hit, certificate earned) → render with that event's data → output: "url" → queue the email with the URL in the body. Hero image matches the event, not a generic template.

Node example — personalize a campaign send

For each recipient, build the HTML with their data interpolated, POST to /v1/render with output: "url", then send the email with that URL in the body:

import { sendEmail } from "./esp";

const recipients = await db.recipients.findActive();

async function renderHero(user) {
  const html = `
    <div style="
      width: 1200px; height: 600px;
      display: flex; flex-direction: column; justify-content: center;
      padding: 80px;
      background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%);
      color: white; font-family: 'Inter', sans-serif;
    ">
      <div style="height: 6px; width: 80px; background: #fbbf24; margin-bottom: 32px;"></div>
      <h1 style="font-size: 56px; line-height: 1.1; font-weight: 700; margin: 0;">
        Here's your weekly summary, ${user.firstName}.
      </h1>
      <div style="font-size: 32px; opacity: 0.85; margin-top: 24px;">
        ${user.stats.shipped} shipments · ${user.stats.saved} hours saved
      </div>
    </div>`;

  const res = await fetch("https://api.codetoimage.app/v1/render", {
    method: "POST",
    headers: {
      "X-API-Key": process.env.CODETOIMAGE_API_KEY,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      html,
      width: 1200,
      height: 600,
      format: "png",
      output: "url",
    }),
  });

  const { url } = await res.json();
  return url;
}

for (const user of recipients) {
  const heroUrl = await renderHero(user);

  await sendEmail({
    to: user.email,
    subject: `Your weekly summary, ${user.firstName}`,
    html: `
      <img src="${heroUrl}" alt="Weekly summary for ${user.firstName}"
           width="600" style="display:block;width:100%;max-width:600px;" />
      <p>Hey ${user.firstName}, here's what happened this week...</p>`,
  });
}

The response shape from URL-output mode: { url: "https://img.codetoimage.app/r/..." }. That URL lives 24h on our CDN — plenty for an email send, since most opens happen in the first day.

vs Bannerbear

AspectBannerbearcodetoimage
ApproachTemplate-first (drag-drop UI)✓ Code-first (HTML you control)
Entry price$49/mo (1,000 images)✓ $7/mo (3,000 renders)
Best forMarketing teams without engineers✓ Devs with existing HTML / AI agents
SetupBuild template in their UI✓ Write your template once in code
Variable substitutionTheir merge-tag system✓ Inline string interpolation in your code
Hosted URL outputYes✓ Yes (CDN, 24h)
API + SDKsNode/Python/Ruby/PHP SDKsREST today (native SDKs not yet)
CLINo CLI@codetoimage/cli
MCP for agentsNo✓ Yes

What you get

Per-recipient personalization

Name, photo, stat, certificate number, milestone date — anything you can interpolate into HTML lands in the rendered PNG.

One render per send, cached 24h

Each personalized URL is cached on our CDN for 24h. Most email opens happen in the first day — your quota covers exactly what gets seen.

Works with any ESP

Postmark, SendGrid, Mailgun, Resend, Mailchimp — they all support <img src>. Drop the hosted URL anywhere a hero image goes.

Transactional triggers

Order confirmation hero with the order summary in the image. Receipt with the actual receipt rendered, not just attached.

Loyalty / milestone images

Anniversary card, badge unlock, level-up — render the event into the image at the moment it happens, email it inside an hour.

Pre-render at signup

One HTTP call at user creation, store the URL forever on the user record. Every future email already has a personalized hero ready.

FAQ

How fast does this run for bulk sends?

Render is ~600ms median. For 10k recipients, parallelize 20-50 concurrent requests and finish in 2-5 minutes. For huge sends, pre-render at signup so send-time is just a DB lookup — the URL is already on the user record.

Do email clients need to support HTML?

No — they just need <img>. Renders are PNGs (or JPEG/WebP), fetched by URL like any other image. Works in Gmail, Outlook, Apple Mail, every webmail client. The personalization is baked into the image itself, not into the HTML body.

Are the hosted URLs cacheable?

Yes — served with public Cache-Control headers. ESP image proxies (Gmail's image cache, Apple Mail Privacy Protection) will fetch once and cache. Recipients seeing the same image multiple times won't trigger re-renders, and your render quota only burns once per personalized variant.

What about GDPR / privacy?

We don't log recipient PII server-side — we render and return. If you're embedding personal data (name, email, stats) into the image, that data lives in the URL, the rendered PNG, and our short-lived render log only. Set output to binary if you want zero retention on our side — we stream the bytes and never persist the image.

Stop sending the same hero image to everyone.

Free Sandbox tier — 50 renders/month, no credit card. Wire up in an afternoon with the Node example above.