Turn Data into Shareable Visual Reports with an Image Generation API

6 min read Image Generation

The Reporting Sharing Problem

Your SaaS dashboard has beautiful charts. Your weekly metrics look great on screen. Then someone asks you to share the numbers in Slack. Or email a summary to a stakeholder who doesn’t have a login. Or embed key metrics in a Notion doc.

You screenshot the dashboard. The screenshot includes the sidebar, half of another chart, and your browser tabs. You crop it. The resolution is wrong for the Slack preview. The colors look different. The text is too small to read on mobile.

PDF reports solve the content problem but create new ones. They’re heavy, they don’t inline in Slack or email, and generating them programmatically means wrestling with PDF layout libraries.

There’s a simpler format for shareable data summaries: images. A PNG renders consistently everywhere — Slack, email, Notion, Google Docs, SMS. No formatting issues, no layout shifts, no missing fonts. What you render is what everyone sees.

Visual Report Cards

A report card is a single image that shows the key metrics someone needs. Revenue this month. Change versus last month. Top-performing segment. One glance, no login required.

The Image Generation API generates these report cards from structured data. Define the layout — title, metric values, comparison text, visual structure — and render it as a PNG.

import { IterationLayer } from "iterationlayer";

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

const { data: { buffer: reportCardBase64 } } = await client.generateImage({
  dimensions: { width: 800, height: 500 },
  output_format: "png",
  fonts: [
    {
      name: "Inter",
      weight: "Bold",
      style: "normal",
      file: { type: "url", name: "Inter-Bold.woff2", url: "https://cdn.example.com/fonts/Inter-Bold.woff2" },
    },
    {
      name: "Inter",
      weight: "Regular",
      style: "normal",
      file: { type: "url", name: "Inter-Regular.woff2", url: "https://cdn.example.com/fonts/Inter-Regular.woff2" },
    },
  ],
  layers: [
      {
        index: 0,
        type: "solid-color-background",
        hex_color: "#0F172A",
      },
      {
        index: 1,
        type: "rectangle",
        position: { x: 0, y: 0 },
        dimensions: { width: 800, height: 4 },
        hex_color: "#3B82F6",
      },
      {
        index: 2,
        type: "text",
        text: "**Weekly Revenue Report**",
        position: { x: 40, y: 30 },
        dimensions: { width: 720, height: 36 },
        font_name: "Inter",
        font_weight: "Bold",
        font_size_in_px: 16,
        text_color: "#94A3B8",
        text_align: "left",
        vertical_align: "top",
      },
      {
        index: 3,
        type: "text",
        text: "Feb 10 - Feb 16, 2026",
        position: { x: 40, y: 62 },
        dimensions: { width: 720, height: 24 },
        font_name: "Inter",
        font_weight: "Regular",
        font_size_in_px: 13,
        text_color: "#64748B",
        text_align: "left",
        vertical_align: "top",
      },
      {
        index: 4,
        type: "rectangle",
        position: { x: 40, y: 110 },
        dimensions: { width: 340, height: 160 },
        hex_color: "#1E293B",
      },
      {
        index: 5,
        type: "text",
        text: "Total Revenue",
        position: { x: 60, y: 130 },
        dimensions: { width: 300, height: 24 },
        font_name: "Inter",
        font_weight: "Regular",
        font_size_in_px: 13,
        text_color: "#94A3B8",
        text_align: "left",
        vertical_align: "top",
      },
      {
        index: 6,
        type: "text",
        text: "**$142,850**",
        position: { x: 60, y: 160 },
        dimensions: { width: 300, height: 56 },
        font_name: "Inter",
        font_weight: "Bold",
        font_size_in_px: 44,
        text_color: "#F8FAFC",
        text_align: "left",
        vertical_align: "top",
      },
      {
        index: 7,
        type: "text",
        text: "↑ 12.4% vs last week",
        position: { x: 60, y: 224 },
        dimensions: { width: 300, height: 24 },
        font_name: "Inter",
        font_weight: "Regular",
        font_size_in_px: 14,
        text_color: "#4ADE80",
        text_align: "left",
        vertical_align: "top",
      },
      {
        index: 8,
        type: "rectangle",
        position: { x: 420, y: 110 },
        dimensions: { width: 340, height: 160 },
        hex_color: "#1E293B",
      },
      {
        index: 9,
        type: "text",
        text: "New Customers",
        position: { x: 440, y: 130 },
        dimensions: { width: 300, height: 24 },
        font_name: "Inter",
        font_weight: "Regular",
        font_size_in_px: 13,
        text_color: "#94A3B8",
        text_align: "left",
        vertical_align: "top",
      },
      {
        index: 10,
        type: "text",
        text: "**847**",
        position: { x: 440, y: 160 },
        dimensions: { width: 300, height: 56 },
        font_name: "Inter",
        font_weight: "Bold",
        font_size_in_px: 44,
        text_color: "#F8FAFC",
        text_align: "left",
        vertical_align: "top",
      },
      {
        index: 11,
        type: "text",
        text: "↑ 8.2% vs last week",
        position: { x: 440, y: 224 },
        dimensions: { width: 300, height: 24 },
        font_name: "Inter",
        font_weight: "Regular",
        font_size_in_px: 14,
        text_color: "#4ADE80",
        text_align: "left",
        vertical_align: "top",
      },
      {
        index: 12,
        type: "rectangle",
        position: { x: 40, y: 300 },
        dimensions: { width: 720, height: 160 },
        hex_color: "#1E293B",
      },
      {
        index: 13,
        type: "text",
        text: "Top Segment",
        position: { x: 60, y: 320 },
        dimensions: { width: 680, height: 24 },
        font_name: "Inter",
        font_weight: "Regular",
        font_size_in_px: 13,
        text_color: "#94A3B8",
        text_align: "left",
        vertical_align: "top",
      },
      {
        index: 14,
        type: "text",
        text: "**Enterprise Plan**",
        position: { x: 60, y: 350 },
        dimensions: { width: 400, height: 40 },
        font_name: "Inter",
        font_weight: "Bold",
        font_size_in_px: 28,
        text_color: "#F8FAFC",
        text_align: "left",
        vertical_align: "top",
      },
      {
        index: 15,
        type: "text",
        text: "$89,200 revenue  |  142 customers  |  62.4% of total",
        position: { x: 60, y: 400 },
        dimensions: { width: 680, height: 24 },
        font_name: "Inter",
        font_weight: "Regular",
        font_size_in_px: 14,
        text_color: "#94A3B8",
        text_align: "left",
        vertical_align: "top",
      },
    ],
});

const reportCardBuffer = Buffer.from(reportCardBase64, "base64");

The layout uses rectangles as card containers on a dark background. Two metric cards side by side at the top — revenue and new customers. A full-width card at the bottom for the top segment breakdown. Each metric has a label, a large value, and a comparison line.

The green #4ADE80 color on the comparison text signals positive change. For negative changes, swap to red — #F87171 — and change the arrow to .

Dynamic Report Generation

The template is static. The data is dynamic. Pull your metrics from whatever source — a database query, an analytics API, a spreadsheet — and inject them into the template:

const metrics = await analyticsService.getWeeklyMetrics();

const comparisonColor = metrics.revenueChangePercent >= 0 ? "#4ADE80" : "#F87171";
const comparisonArrow = metrics.revenueChangePercent >= 0 ? "" : "";
const comparisonText = `${comparisonArrow} ${Math.abs(metrics.revenueChangePercent)}% vs last week`;

// Inject into the template layers...

The generation logic doesn’t care where the numbers come from. It takes values and renders pixels. Your data pipeline produces the metrics. The API produces the image.

Scheduling Weekly Reports

The most common pattern: a cron job runs every Monday morning, pulls last week’s metrics, generates a report card, and posts it to Slack.

// In your scheduled job
const metrics = await analyticsService.getWeeklyMetrics();
const reportImage = await generateReportCard(metrics);

// Post to Slack
await slackClient.files.uploadV2({
  channel_id: "C0123METRICS",
  file: reportImage,
  filename: `weekly-report-${metrics.weekLabel}.png`,
  initial_comment: `Weekly report for ${metrics.weekLabel}`,
});

The image inlines directly in the Slack message. No link to click, no dashboard to log into, no PDF to download. The key numbers are right there in the channel.

Email-Friendly Reports

Email clients are hostile to complex formatting. HTML emails break differently in Gmail, Outlook, and Apple Mail. CSS support is inconsistent. Web fonts don’t load. Tables render unpredictably.

Images bypass all of this. A PNG renders the same in every email client. Embed the report card as an inline image and the recipient sees exactly what you rendered — correct fonts, correct colors, correct layout.

For email, generate at 2x resolution (1600x1000 for an 800x500 display size) so the image looks sharp on high-DPI screens. The API handles any dimensions you specify.

Multiple Report Types

Different stakeholders need different views. The CEO wants revenue and growth. The product team wants feature adoption. The support team wants ticket volume and resolution time.

Build a template for each report type. The structure stays the same — metric cards with labels, values, and comparisons. The content changes:

  • Executive summary: Revenue, growth rate, customer count, churn
  • Product report: Daily active users, feature adoption rates, conversion funnel
  • Support report: Open tickets, average resolution time, satisfaction score
  • Sales report: Pipeline value, deals closed, average deal size

Each one is a function that takes the relevant metrics and returns a PNG. Same API, different data.

Why Images, Not Dashboards

Dashboards are great for exploration. You click around, filter by date range, drill into a segment. But dashboards require a login, a browser, and time.

Report card images are for communication. They answer one question: “How did we do this week?” The recipient glances at the image and has the answer. No login, no navigation, no context switching.

Images also create a record. Drop the weekly report image into a Notion page and you have a visual timeline of metrics over time. Try doing that with dashboard screenshots — you’ll end up with inconsistent crops, varying zoom levels, and a page that looks like a ransom note.

The constraint of a fixed canvas — 800x500 pixels, a handful of metrics — forces clarity. You can’t cram every chart onto a report card. You have to choose the three or four numbers that actually matter. That constraint is a feature.

What’s Next

Chain with Document Extraction to pull report data directly from source documents — same auth, same credit pool.

Get Started

Check the docs for the full layer reference and output format options. The TypeScript and Python SDKs handle authentication and response parsing.

Sign up for a free account — no credit card required. Pull your latest metrics, generate a report card, and share it in your team’s Slack channel.

Start building in minutes

Free trial included. No credit card required.