"Just Listed" to "Just Sold" — Automate Real Estate Marketing Graphics

6 min read Image Generation

Every Listing Needs Five Graphics

A real estate agent lists a property. Within the first 48 hours, they need a “Just Listed” graphic for Instagram, a “Price Reduced” variant if the strategy changes, an “Open House” version with date and time, and eventually a “Just Sold” celebration post. Each one features the property photo, the price, the agent’s branding, and a status badge.

Most agents either do this manually in Canva — 15 minutes per graphic, multiplied by every listing — or hire a virtual assistant to handle it. Brokerages with hundreds of agents scale this with design teams that batch-process graphics weekly. By the time the graphic is ready, the listing is stale.

For real estate platforms and CRMs, the problem is worse. You’re not making graphics for one agent — you’re making them for thousands. Every listing, every status change, every agent’s branding.

The Image Generation API generates these graphics from a JSON template. Define the layout once — property photo, status badge, price, agent branding — and generate variants by swapping the status text and color. One API call per graphic.

The “Just Listed” Template

Here’s a complete real estate listing graphic at 1080x1080 — the standard size for Instagram and Facebook posts:

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

const result = await client.generateImage({
  dimensions: { width: 1080, height: 1080 },
  output_format: "png",
  fonts: [
    {
      name: "Montserrat",
      weight: "Bold",
      style: "normal",
      file: { type: "url", name: "Montserrat-Bold.woff2", url: "https://cdn.example.com/fonts/Montserrat-Bold.woff2" },
    },
    {
      name: "Montserrat",
      weight: "Regular",
      style: "normal",
      file: { type: "url", name: "Montserrat-Regular.woff2", url: "https://cdn.example.com/fonts/Montserrat-Regular.woff2" },
    },
  ],
  layers: [
      {
        index: 0,
        type: "solid-color-background",
        hex_color: "#FFFFFF",
      },
      {
        index: 1,
        type: "static-image",
        file: { type: "url", name: "property-123.jpg", url: "https://cdn.example.com/listings/property-123.jpg" },
        position: { x: 0, y: 0 },
        dimensions: { width: 1080, height: 720 },
        should_use_smart_cropping: true,
      },
      {
        index: 2,
        type: "rectangle",
        position: { x: 0, y: 0 },
        dimensions: { width: 320, height: 56 },
        hex_color: "#1E40AF",
      },
      {
        index: 3,
        type: "text",
        text: "**JUST LISTED**",
        position: { x: 0, y: 0 },
        dimensions: { width: 320, height: 56 },
        font_name: "Montserrat",
        font_weight: "Bold",
        font_size_in_px: 20,
        text_color: "#FFFFFF",
        text_align: "center",
        vertical_align: "center",
      },
      {
        index: 4,
        type: "text",
        text: "**$875,000**",
        position: { x: 40, y: 740 },
        dimensions: { width: 600, height: 60 },
        font_name: "Montserrat",
        font_weight: "Bold",
        font_size_in_px: 44,
        text_color: "#111827",
        text_align: "left",
        vertical_align: "center",
      },
      {
        index: 5,
        type: "text",
        text: "4 Bed  |  3 Bath  |  2,450 sqft",
        position: { x: 40, y: 800 },
        dimensions: { width: 600, height: 36 },
        font_name: "Montserrat",
        font_weight: "Regular",
        font_size_in_px: 18,
        text_color: "#6B7280",
        text_align: "left",
        vertical_align: "center",
      },
      {
        index: 6,
        type: "text",
        text: "742 Evergreen Terrace, Springfield",
        position: { x: 40, y: 840 },
        dimensions: { width: 600, height: 32 },
        font_name: "Montserrat",
        font_weight: "Regular",
        font_size_in_px: 16,
        text_color: "#9CA3AF",
        text_align: "left",
        vertical_align: "center",
      },
      {
        index: 7,
        type: "rectangle",
        position: { x: 0, y: 930 },
        dimensions: { width: 1080, height: 150 },
        hex_color: "#F3F4F6",
      },
      {
        index: 8,
        type: "static-image",
        file: { type: "url", name: "agent-headshot.jpg", url: "https://cdn.example.com/agents/agent-headshot.jpg" },
        position: { x: 40, y: 950 },
        dimensions: { width: 90, height: 90 },
        should_use_smart_cropping: true,
      },
      {
        index: 9,
        type: "text",
        text: "**Jane Rodriguez**",
        position: { x: 150, y: 955 },
        dimensions: { width: 400, height: 32 },
        font_name: "Montserrat",
        font_weight: "Bold",
        font_size_in_px: 18,
        text_color: "#111827",
        text_align: "left",
        vertical_align: "center",
      },
      {
        index: 10,
        type: "text",
        text: "Premier Realty Group  |  (555) 123-4567",
        position: { x: 150, y: 990 },
        dimensions: { width: 400, height: 28 },
        font_name: "Montserrat",
        font_weight: "Regular",
        font_size_in_px: 14,
        text_color: "#6B7280",
        text_align: "left",
        vertical_align: "center",
      },
      {
        index: 11,
        type: "static-image",
        file: { type: "url", name: "brokerage-logo.png", url: "https://cdn.example.com/agents/brokerage-logo.png" },
        position: { x: 860, y: 960 },
        dimensions: { width: 180, height: 80 },
      },
    ],
  });

const { data: { buffer: listingGraphicBase64 } } = result;
const listingGraphicBuffer = Buffer.from(listingGraphicBase64, "base64");

The layout has four zones. The property photo fills the top two-thirds, smart-cropped to frame the house correctly. The status badge sits in the top-left corner. The middle section shows price, specs, and address. The bottom strip is the agent’s branding — headshot, name, brokerage, and logo.

Swapping Status Variants

The same template generates every status variant. Only the badge text and color change:

const statusVariants = [
  { label: "JUST LISTED", hex_color: "#1E40AF" },
  { label: "OPEN HOUSE", hex_color: "#047857" },
  { label: "PRICE REDUCED", hex_color: "#DC2626" },
  { label: "UNDER CONTRACT", hex_color: "#7C3AED" },
  { label: "JUST SOLD", hex_color: "#B45309" },
];

For each variant, replace the badge rectangle’s hex_color and the badge text’s text. Everything else stays the same — same property photo, same price, same agent branding.

The “Open House” variant might also add a date and time line below the address:

{
  index: 7,
  type: "text",
  text: "**Open House:** Saturday, March 8  |  1:00 PM - 4:00 PM",
  position: { x: 40, y: 875 },
  dimensions: { width: 700, height: 30 },
  font_name: "Montserrat",
  font_weight: "Bold",
  font_size_in_px: 15,
  text_color: "#047857",
  text_align: "left",
  vertical_align: "center",
}

One extra layer. Same template. The “Price Reduced” variant can show both the original and new price:

{
  index: 4,
  type: "text",
  text: "**$875,000** (was $925,000)",
  position: { x: 40, y: 740 },
  dimensions: { width: 600, height: 60 },
  font_name: "Montserrat",
  font_weight: "Bold",
  font_size_in_px: 44,
  text_color: "#111827",
  text_align: "left",
  vertical_align: "center",
}

Batch Generation for a Listing Feed

Real estate platforms process listing feeds — MLS data, IDX feeds, or internal listing databases. Each listing has a property photo, address, price, and specs. Generating graphics for an entire feed is a loop:

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

const listings = await database.getActiveListings(agentId);

const graphics = await Promise.all(
  listings.map(async (listing) => {
    const result = await client.generateImage({
      dimensions: { width: 1080, height: 1080 },
      output_format: "png",
      fonts: [/* same font config */],
      layers: [
          { index: 0, type: "solid-color-background", hex_color: "#FFFFFF" },
          {
            index: 1,
            type: "static-image",
            file: { type: "url", name: "property.jpg", url: listing.primaryPhotoUrl },
            position: { x: 0, y: 0 },
            dimensions: { width: 1080, height: 720 },
            should_use_smart_cropping: true,
          },
          // Status badge
          {
            index: 2,
            type: "rectangle",
            position: { x: 0, y: 0 },
            dimensions: { width: 320, height: 56 },
            hex_color: listing.statusColor,
          },
          {
            index: 3,
            type: "text",
            text: `**${listing.statusLabel}**`,
            position: { x: 0, y: 0 },
            dimensions: { width: 320, height: 56 },
            font_name: "Montserrat",
            font_weight: "Bold",
            font_size_in_px: 20,
            text_color: "#FFFFFF",
            text_align: "center",
            vertical_align: "center",
          },
          // Price and details
          {
            index: 4,
            type: "text",
            text: `**${listing.formattedPrice}**`,
            position: { x: 40, y: 740 },
            dimensions: { width: 600, height: 60 },
            font_name: "Montserrat",
            font_weight: "Bold",
            font_size_in_px: 44,
            text_color: "#111827",
            text_align: "left",
            vertical_align: "center",
          },
          {
            index: 5,
            type: "text",
            text: `${listing.beds} Bed  |  ${listing.baths} Bath  |  ${listing.sqft} sqft`,
            position: { x: 40, y: 800 },
            dimensions: { width: 600, height: 36 },
            font_name: "Montserrat",
            font_weight: "Regular",
            font_size_in_px: 18,
            text_color: "#6B7280",
            text_align: "left",
            vertical_align: "center",
          },
          // Agent branding layers...
        ],
      });

    const { data: { buffer } } = result;

    return { listingId: listing.id, status: listing.statusLabel, buffer: Buffer.from(buffer, "base64") };
  })
);

Fifty listings, fifty graphics. Each one branded consistently. Each one with the property correctly framed thanks to smart cropping. No manual work, no design queue, no wait time.

Smart Cropping for Property Photos

Property photos are notoriously inconsistent. A front exterior shot might be wide with the house centered. An aerial drone photo might have the property in the lower third. An interior shot might focus on the kitchen with the living room visible through a doorway.

The should_use_smart_cropping flag handles all of these. For a 1080x720 container on a landscape exterior shot, it finds the house and centers it. For a vertical interior shot, it finds the most visually relevant area and crops to fill the container without distortion.

This is the difference between a professional-looking listing graphic and one where the property photo is awkwardly cut off at the roofline.

Agent-Specific Branding at Scale

Brokerages with hundreds of agents need consistent branding with per-agent customization. The template stays the same — only the agent’s headshot, name, phone number, and optionally their team logo change.

Store each agent’s branding assets (headshot URL, name, contact info, logo URL) in your database. When generating a listing graphic, pull the agent’s branding and inject it into the template. Every agent gets polished, on-brand graphics without a design team touching each one.

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. Generate a listing graphic from one of your properties and see how it compares to your current workflow.

Start building in minutes

Free trial included. No credit card required.