Personalized Email Banners at Scale — No More Generic Headers

6 min read Image Generation

Email HTML Is Broken

Every email developer knows the pain. You design a layout in Figma, convert it to HTML tables (because email clients still live in 2004), and then watch it render differently in Outlook, Gmail, Apple Mail, and Yahoo. Padding breaks. Fonts fall back. Background images don’t show. Media queries work in some clients but not others.

The one thing that renders consistently across every email client? Images. A JPEG looks the same in Outlook 2016 and Apple Mail. No CSS quirks, no rendering engine differences, no “works everywhere except Gmail” surprises.

That’s why the most reliable way to deliver a visually consistent email header is to generate it as an image. And if you’re generating images anyway, you might as well make them personalized.

One Banner Per Recipient

Generic email banners talk to everyone and connect with no one. “Spring Sale — 20% Off” is the same banner for every subscriber. But “Sarah, your 20% off code expires Friday” with the recipient’s name rendered directly into the image — that’s a different level of engagement.

The Image Generation API lets you generate a unique banner image per recipient with a single API call. Define the layout once as a set of layers, swap the dynamic fields per email, and get back a ready-to-embed image.

Here’s a promotional banner template:

import { IterationLayer } from "iterationlayer";
const client = new IterationLayer({ apiKey: "YOUR_API_KEY" });

const generateBanner = async (recipientName: string, discountCode: string) => {
  const { data: { buffer } } = await client.generateImage({
    dimensions: { width: 600, height: 200 },
    output_format: "jpeg",
    layers: [
      {
        index: 0,
        type: "solid-color-background",
        hex_color: "#0f172a",
      },
      {
        index: 1,
        type: "rectangle",
        hex_color: "#3b82f6",
        position: { x: 0, y: 0 },
        dimensions: { width: 8, height: 200 },
      },
      {
        index: 2,
        type: "static-image",
        file: { type: "url", name: "logo-white.png", url: "https://example.com/logo-white.png" },
        position: { x: 30, y: 20 },
        dimensions: { width: 120, height: 40 },
      },
      {
        index: 3,
        type: "text",
        text: `${recipientName}, your exclusive offer`,
        font_name: "Inter",
        font_weight: "Bold",
        font_size_in_px: 24,
        text_color: "#ffffff",
        text_align: "left",
        vertical_align: "top",
        position: { x: 30, y: 80 },
        dimensions: { width: 540, height: 40 },
      },
      {
        index: 4,
        type: "text",
        text: `Use code ${discountCode} for 20% off`,
        font_name: "Inter",
        font_weight: "Regular",
        font_size_in_px: 18,
        text_color: "#94a3b8",
        text_align: "left",
        vertical_align: "top",
        position: { x: 30, y: 130 },
        dimensions: { width: 540, height: 30 },
      },
      {
        index: 5,
        type: "text",
        text: "Expires Friday",
        font_name: "Inter",
        font_weight: "Regular",
        font_size_in_px: 14,
        text_color: "#64748b",
        text_align: "left",
        vertical_align: "bottom",
        position: { x: 30, y: 165 },
        dimensions: { width: 540, height: 20 },
      },
    ],
  });

  return Buffer.from(buffer, "base64");
};

Call it once per recipient. The only things that change are recipientName and discountCode. Everything else — layout, colors, logo, typography — stays the same.

Why JPEG for Email

The example uses output_format: "jpeg" deliberately. Email clients handle JPEG reliably. PNG works too but produces larger files — fine for graphics with transparency, overkill for a banner with a solid background. WebP and AVIF have patchy email client support. Stick with JPEG for banners unless you need transparency.

Image size matters in email. Large attachments get clipped by Gmail (at ~102KB of HTML), slow down loading, and can trigger spam filters. A 600x200 JPEG banner at decent quality is typically 15-30KB. That’s small enough to embed as a hosted image URL without worrying about load times.

Building the Template

The banner above uses four layer types — here’s the thinking behind each:

solid-color-background fills the entire canvas. This is your base. Dark backgrounds (#0f172a) work well for promotional emails because they contrast with the mostly-white email body.

rectangle adds the accent bar on the left edge — 8px wide, full height, brand blue. Small design touches like this make a generated image look designed, not generated.

static-image places the logo. The file field accepts a URL, so you can reference your logo from a CDN. Position and dimensions are explicit — no CSS alignment needed.

text layers handle the copy. Each text element is a separate layer with its own font size, weight, color, and position. This is more explicit than CSS, but it means there are no surprises. What you specify is what renders.

Scaling to Thousands of Recipients

Generating one image per recipient sounds expensive. It’s not. Each API call produces one image. There’s no browser to start, no template to render through a DOM. The composition is fast.

For a campaign with 10,000 recipients, the pattern looks like this:

const generateBannersForCampaign = async (
  recipients: { name: string; code: string; email: string }[],
) => {
  const banners = await Promise.all(
    recipients.map(async (recipient) => {
      const imageBuffer = await generateBanner(recipient.name, recipient.code);

      return { email: recipient.email, imageBuffer };
    }),
  );

  return banners;
};

In practice, you’d want to batch these with a concurrency limit rather than firing all 10,000 at once. A simple approach with a concurrency pool of 20-50 keeps throughput high without overwhelming your network.

You can also generate banners on-demand. Instead of pre-generating all banners before sending, generate each banner when the email is assembled. This works well with email service providers that support dynamic content — generate the image, upload it to your CDN or storage, and insert the URL into the email template.

Beyond Names: What You Can Personalize

Recipient name is the obvious dynamic field. But the template is just layers — anything can change per recipient:

  • Product recommendations — swap the static-image layer with a product photo specific to the recipient’s browsing history
  • Location-based offers — change the text to reference the recipient’s city or nearest store
  • Usage milestones — “You’ve processed 1,247 documents this month” with the actual count rendered into the image
  • Account-specific data — subscription tier, renewal date, credit balance

Each variation is just a different set of values passed to the same layer structure. The layout stays fixed. The content changes.

Integrating With Your Email Pipeline

Most email workflows follow the same pattern: build the recipient list, assemble the email content, send through an ESP (Mailchimp, SendGrid, Postmark, etc.). Image generation fits between “assemble” and “send.”

// 1. Get recipients from your database
const recipients = await getActiveSubscribers();

// 2. Generate personalized banners
const banners = await Promise.all(
  recipients.map(async (recipient) => {
    const imageBuffer = await generateBanner(recipient.name, recipient.discountCode);
    const imageUrl = await uploadToStorage(imageBuffer, `banners/${recipient.id}.jpg`);

    return { ...recipient, bannerUrl: imageUrl };
  }),
);

// 3. Send emails with personalized banner URLs
for (const recipient of banners) {
  await sendEmail({
    to: recipient.email,
    subject: `${recipient.name}, your exclusive offer inside`,
    html: `<img src="${recipient.bannerUrl}" width="600" height="200" alt="Your exclusive offer" />`,
  });
}

The generated image is hosted at a URL. The email HTML just references it with an <img> tag. No complex HTML layout, no CSS compatibility issues, no rendering differences. The image is the layout.

Beyond Banners

The same approach works for any image embedded in email:

  • Event tickets with the attendee’s name and QR code
  • Monthly reports with a chart or key metric rendered as an image
  • Onboarding sequences with the user’s company logo incorporated into the welcome graphic
  • Re-engagement emails with specific product images the user viewed

Any time you need a visual element in an email that varies per recipient, generating it as an image sidesteps every email HTML rendering problem.

What’s Next

Combine with Document Extraction to pull personalization data directly from CRM exports or spreadsheets — same auth, same credit pool.

Get Started

Check out the Image Generation API docs for the full layer specification. Start with one banner template, generate a test image with your data, and embed it in a test email. Once you see it render identically across Outlook, Gmail, and Apple Mail, you’ll wonder why you ever fought with email HTML.

Sign up for a free account — no credit card required.

Start building in minutes

Free trial included. No credit card required.