---
title: "How to Generate Quote Cards from an API for Testimonials, Pull Quotes and Podcast Clips"
description: "Generate quote cards from an API: send the quote and attribution as JSON, get a branded PNG back, then loop it over every testimonial and pull quote."
url: "https://html2img.com/articles/quote-card-api/"
section: "Tutorials"
published: "2026-09-12T16:01:12.630Z"
updated: "2026-09-12T16:03:53.748Z"
---

# How to Generate Quote Cards from an API for Testimonials, Pull Quotes and Podcast Clips

By Mike Griffiths. https://html2img.com/articles/quote-card-api/

![How to Generate Quote Cards from an API for Testimonials, Pull Quotes and Podcast Clips](https://a.storyblok.com/f/320619/1200x630/2ca2726746/og.png)

A SaaS with forty customer testimonials has forty quote cards to make. A publication that shares two pull quotes per article needs a card every time a piece goes out. A podcast that clips the best line from each episode needs one a week for as long as the show runs. Each card is the same layout with the words swapped, and it is still being opened in a design tool, typed out, exported and uploaded by hand.

This article shows how to render quote cards from an API instead: one call per quote, a hosted PNG back, and a loop that runs the whole backlog while you do something else. Every image below is the actual output of the request above it.

## What a quote card has to get right

A quote card looks simple and hides three problems.

The first is length. A testimonial can be eleven words or fifty, and the card has to look deliberate at both ends. A fixed font size either leaves a short quote floating in empty space or pushes a long one off the bottom. The type has to scale with the text.

The second is attribution. Name, role, company and sometimes a face or a logo, laid out so the quote stays the hero and the credit reads as a caption. This is the part that drifts when different people make the cards, and drift is what makes a feed look like three brands instead of one.

The third is format. A 1200x1200 square posts to Instagram, LinkedIn and X without recropping, which is why it is the default. But a card destined for a link preview has to be 1200x630, and a card for Stories has to be 1080x1920 with the payload kept out of the top and bottom where the platform draws its own chrome. If you have built [Open Graph images with a safe zone](https://html2img.com/articles/og-image-safe-zone/) the discipline carries over exactly.

The template route below handles the first two problems for you. The custom HTML route handles all three, at the cost of writing the markup once.

## Route 1: render from JSON with the template endpoint

The [Quote Card template](https://html2img.com/templates/quote-card/) takes two required inputs, `quote` and `attribution_name`, and six optional ones for the role, an avatar, a brand name, a logo and the two colours:

```
curl -X POST https://app.html2img.com/api/v1/templates/quote-card \
  -H "X-API-Key: $HTML2IMG_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "quote": "We replaced a designer-in-the-loop process with one API call. The cards went out the same afternoon the quotes came in, and nobody could tell they were not hand made.",
    "attribution_name": "Priya Raman",
    "attribution_role": "Head of Product, Fieldline",
    "brand_name": "Fieldline",
    "background_color": "#FAF7F0",
    "accent_color": "#2563EB"
  }'
```

The response is the standard envelope with a CDN URL:

```
{
  "success": true,
  "id": "152d3ad1-5fb3-44e0-b7c3-328c45332aeb",
  "template": "quote-card",
  "expires_at": null,
  "credits_remaining": 100,
  "url": "https://i.html2img.com/image-1789228709438-533456.png"
}
```

And this is the file at that URL:

![Rendered quote card: a square off-white card with a 30-word testimonial set in large dark type, a short blue rule beneath it, the attribution Priya Raman, Head of Product, Fieldline, faint decorative quotation marks in the corners and the brand name FIELDLINE in small blue capitals at the foot](https://i.html2img.com/image-1789228709438-533456.png)

A few things the render does without being asked. The quote is 30 words and the type has been sized so it fills the middle of the card in six lines with even margins; send a ten-word quote and it comes back larger, not lonelier. The `accent_color` shows up as the rule under the quote and the brand name at the bottom, and nowhere else, so a single hex value keeps the card on-brand without letting the colour take over. The decorative quotation marks are tinted from the background, not the accent, which is why they read as texture rather than competing with the words.

The file came back at 2400x2400, the template rendering at 2x, and at 1.5 MB. That is comfortably inside what every social platform accepts, and it is sharp on a retina feed. The template always renders at 2x, so if you need a lighter file for a page or an email, the HTML route below is the one to use: it renders at 1x unless you ask for more.

Two inputs are left out above. `avatar_url` takes a photo of the person and masks it into a circle beside the attribution, which is the right call for testimonials and podcast guests and the wrong one for an anonymous pull quote, so leave it out and the slot collapses. `logo_url` replaces the text brand name with your mark. Both are fetched at render time, so they need to be publicly reachable URLs. The [template reference](https://html2img.com/docs/templates/quote-card/) lists every input with its default, and the [Quote Card tool](https://html2img.com/tools/quote-card/) runs the same template in the browser if you want to settle on colours before writing the request.

## Route 2: your own layout through the HTML endpoint

The template is one design. A brand with an established testimonial style, a dark card with a serif face and a monogram avatar, say, needs its own markup. That is the HTML endpoint, and because you set the dimensions it is also how you get the 1200x630 version for a link preview:

```
curl -X POST https://app.html2img.com/api/html \
  -H "X-API-Key: $HTML2IMG_API_KEY" \
  -H "Content-Type: application/json" \
  -d @- <<'JSON'
{
  "width": 1200,
  "height": 630,
  "html": "<!doctype html><html><head><meta charset=\"utf-8\"><style>@import url('https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,600&family=Manrope:wght@500;700&display=swap');*{box-sizing:border-box;margin:0}body{width:1200px;height:630px;overflow:hidden;position:relative;background:#0B1220;color:#fff;font-family:Manrope,sans-serif}.glow{position:absolute;inset:0;background:radial-gradient(800px 500px at 90% 10%,rgba(37,99,235,.45),transparent 65%)}.wrap{position:absolute;inset:64px 72px;display:flex;flex-direction:column;justify-content:space-between}.mark{font-family:Fraunces,serif;font-size:160px;line-height:.6;color:#2563EB;height:70px}blockquote{font-family:Fraunces,serif;font-weight:600;font-size:44px;line-height:1.25;letter-spacing:-.01em;max-width:1000px}.who{display:flex;align-items:center;gap:20px}.avatar{width:64px;height:64px;border-radius:50%;background:#2563EB;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:24px}.name{font-weight:700;font-size:22px}.role{font-size:18px;color:rgba(255,255,255,.6);margin-top:4px}.brand{margin-left:auto;font-weight:700;font-size:16px;letter-spacing:.2em;text-transform:uppercase;color:rgba(255,255,255,.45)}</style></head><body><div class=\"glow\"></div><div class=\"wrap\"><div class=\"mark\">&#8220;</div><blockquote>We replaced a designer-in-the-loop process with one API call. The cards went out the same afternoon the quotes came in.</blockquote><div class=\"who\"><div class=\"avatar\">PR</div><div><div class=\"name\">Priya Raman</div><div class=\"role\">Head of Product, Fieldline</div></div><div class=\"brand\">Fieldline · Customer stories</div></div></div></body></html>"
}
JSON
```

That produced this, a native 1200x630 PNG at 192 KB:

![Rendered custom quote card: a dark navy 1200x630 card with a blue radial glow top-right, a large blue opening quotation mark, a two-sentence testimonial in white serif type, and at the foot a blue circular monogram reading PR beside the name Priya Raman, Head of Product, Fieldline, with FIELDLINE · CUSTOMER STORIES in spaced capitals on the right](https://i.html2img.com/image-1789228729622-346975.png)

The markup is ordinary CSS. Google Fonts load through `@import`, so a serif for the quote and a sans for the attribution costs one line. The `.wrap` is a flex column with `justify-content: space-between`, which pins the quotation mark to the top and the attribution to the bottom whatever length the quote in the middle turns out to be. The monogram avatar is a `div` with initials, which sidesteps fetching a headshot and is what many brands prefer for customer quotes anyway.

Two things to carry over from the template route. First, `overflow: hidden` on the body and an explicit `width` and `height` matching the request, so a long quote cannot push the attribution below the canvas. Second, the rendered image is a real browser screenshot, so anything you can express in CSS you can put on the card: a background photo with a dark overlay, a gradient border, a second column for a product screenshot.

## Sizing the type to the quote

The template scales the font for you. In your own markup you have to do it, and the simplest way is to pick the size on the server before you send the HTML rather than trying to do it in CSS:

```
function quoteFontSize(text) {
  const words = text.trim().split(/\s+/).length;
  if (words <= 12) return 60;
  if (words <= 25) return 48;
  if (words <= 40) return 40;
  return 34;
}
```

Drop the result into an inline style on the `blockquote` and a 12-word quote renders at 60px while a 40-word one renders at 40px, both filling the middle of the card. Four bands are enough; the eye does not notice the steps, only whether the card looks full. If you would rather not maintain the bands, a small inline `<script>` that shrinks the font until the `blockquote` fits its box also works, because the renderer executes inline JavaScript before capture. Pair it with a short `ms_delay` so the screenshot waits for the loop to finish.

## Rendering the whole backlog

One card is a curl. Forty is a loop, and the loop is where the time saving actually lives. Given a `testimonials.json` with a quote, a name and a role per entry:

```
import { readFile, writeFile } from 'node:fs/promises';

const testimonials = JSON.parse(await readFile('testimonials.json', 'utf8'));

for (const t of testimonials) {
  const response = await fetch('https://app.html2img.com/api/v1/templates/quote-card', {
    method: 'POST',
    headers: {
      'X-API-Key': process.env.HTML2IMG_API_KEY,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      quote: t.quote,
      attribution_name: t.name,
      attribution_role: t.role,
      brand_name: 'Fieldline',
      background_color: '#FAF7F0',
      accent_color: '#2563EB',
    }),
  });

  const { url } = await response.json();
  t.card_url = url;
  console.log(`${t.name}: ${url}`);
}

await writeFile('testimonials.json', JSON.stringify(testimonials, null, 2));
```

The script writes each card's URL back onto the testimonial record, so the JSON file becomes the source of truth for the marketing site, the case study PDF and the social scheduler at once. The same loop in PHP, Python or Ruby is a dozen lines through the [SDKs and integration guides](https://html2img.com/integrations/); if the quotes already live in a Laravel app, the [Laravel integration](https://html2img.com/integrations/laravel/) is a facade call inside the same `foreach`.

A few notes for a batch of any size:

- Each render is one credit, and `credits_remaining` comes back in every response so the loop can stop cleanly before a 402. `GET /api/me` reports the balance without spending one.
- For a batch in the hundreds, pass a `webhook_url` with each request and let the API post the URL back to you rather than holding forty HTTP connections open in sequence.
- The CDN URL is stable for as long as the render exists, so it is safe to write into a CMS field or a newsletter. For cards going into an email, keep the file light by rendering through the HTML endpoint at the default `dpi` of 1; the [images-in-email guide](https://html2img.com/articles/images-in-email-render-everywhere/) covers what each client does with a hosted PNG.

## One quote, three formats

Most teams need the square for the feed and the 1200x630 for the link preview, and occasionally the 1080x1920 for a Story. Rather than three templates, treat the format as a parameter: render the square through the template endpoint, and send the same quote and attribution through the HTML endpoint with `width` and `height` set for the other two, using the markup above with the `body` dimensions changed. For the Story format, add top and bottom padding of around 250px so the quote and attribution sit in the middle band where neither the progress bar nor the reply box will cover them.

The [tweet-to-image](https://html2img.com/articles/tweet-to-image/) workflow follows the same shape when the quote is a post rather than a testimonial, and the [LinkedIn post template](https://html2img.com/templates/linkedin-post/) is the companion when the card is the post itself rather than a quote inside it.

---

Need testimonial cards, pull quotes or podcast highlights rendered from JSON without opening a design tool? [Browse the templates gallery](https://html2img.com/templates/) or [read the docs](https://html2img.com/docs/) to get started.
