Features Full Page Screenshot Wait for Selector & Delay Block Cookie Banners Custom Viewport & Device Website to PDF HTML to Image Markdown to Image Dark Mode Image Format & Quality MCP Server Webhook Pricing Docs Blog Log In Sign Up

Markdown to Image API

Vitalii Holben Vitalii Holben

Developers write in markdown every day. README files, release notes, blog posts, documentation. At some point someone asks: "can I get this as an image?" Maybe for a tweet, a Slack message, or a slide. And then it starts — convert to HTML, add CSS, launch a browser, take a screenshot, clean up. That's a lot of steps for what should be a simple thing.

ScreenshotRun has a markdown parameter that handles all of this in one request. You send markdown text, the API renders it and returns an image. PNG, WebP, JPEG, or PDF. No browser on your side, no conversion steps, no cleanup.

What the markdown parameter actually does

When you send markdown instead of a URL, the API parses your text, wraps it in a clean HTML page with good typography, opens it in a real browser on the server, and takes a screenshot. You don't need to install anything on your side — no markdown library, no CSS, no browser.

Headings, bold, italic, links, images, code blocks, tables, blockquotes, horizontal rules, nested lists. If you've typed it in a README, it renders.

curl "https://api.screenshotrun.com/v1/screenshots/capture" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "markdown": "# Release v3.1.0\n\n## Changes\n\n- **Webhook retries** — failed deliveries now retry 3 times with exponential backoff\n- **WebP default** — new accounts default to WebP instead of PNG\n- Fixed timeout handling for pages over 60 seconds\n\n## Migration notes\n\nNo breaking changes. Existing webhooks continue to work as before.",
    "format": "png",
    "width": 800
  }'

That returns a clean, readable image of your release notes. No setup, no teardown.

Same request in Node.js:

const response = await fetch('https://api.screenshotrun.com/v1/screenshots/capture', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer YOUR_API_KEY',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    markdown: '# Release v3.1.0\n\n- **Webhook retries** with exponential backoff\n- WebP as default format for new accounts',
    format: 'webp',
    width: 800,
  }),
});

const imageBuffer = await response.arrayBuffer();
// Save to file, upload to S3, embed in Slack — your call
import requests

resp = requests.post(
    "https://api.screenshotrun.com/v1/screenshots/capture",
    headers={"Authorization": "Bearer YOUR_API_KEY"},
    json={
        "markdown": "# Release v3.1.0\n\n- **Webhook retries** with exponential backoff\n- WebP as default format for new accounts",
        "format": "webp",
        "width": 800,
    },
)

with open("release-notes.webp", "wb") as f:
    f.write(resp.content)

Styling and dark mode

The default look is clean: readable fonts, comfortable spacing, nice code blocks, neat table borders. Light mode gives you dark text on white. Add dark_mode: true and it flips — light text on a dark background, code blocks in a deeper shade.

{
  "markdown": "# API Status\n\n| Service | Status | Latency |\n|---------|--------|---------|\n| Renderer | Up | 1.2s |\n| Storage | Up | 45ms |\n| Webhooks | Degraded | 3.8s |",
  "dark_mode": true,
  "format": "webp",
  "width": 600
}

If the defaults don't match your brand, pass your own CSS with the css parameter. It overrides the built-in styles, so you can change fonts, colors, spacing — whatever you need — without touching the markdown itself.

{
  "markdown": "# Weekly Report\n\nConversion rate: **4.2%** (up from 3.8%)\n\nTop referrer: `producthunt.com`",
  "css": "body { font-family: 'Inter', sans-serif; background: #f8f9fa; color: #1a1a1a; } code { background: #e9ecef; padding: 2px 6px; border-radius: 3px; }",
  "format": "png",
  "width": 600
}

Full control over the output. No intermediate HTML step.

Where this shows up in production

We expected markdown-to-image to be a niche feature when we added it. Turns out, it gets used more than HTML-to-image on some accounts.

Release notes and changelogs

Every GitHub release has a markdown body. Pull that text via the GitHub API, send it to ScreenshotRun, and you have an image for Slack, Discord, or a tweet. A CI job does this in three lines of bash. No Puppeteer, no Docker image with Chrome, no fonts to install.

# Grab the latest release body from GitHub and screenshot it
RELEASE_BODY=$(curl -s "https://api.github.com/repos/your-org/your-repo/releases/latest" \
  | jq -r '.body')

curl "https://api.screenshotrun.com/v1/screenshots/capture" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d "{
    \"markdown\": $(echo "$RELEASE_BODY" | jq -Rs .),
    \"format\": \"webp\",
    \"width\": 800
  }" --output release-notes.webp

Social cards and documentation previews

If your blog posts are markdown files (Hugo, Jekyll, Astro, Next.js), you can generate a unique social card for each post with one API call. Pull the title and summary, format as markdown, add your brand colors via css. No template engine, no canvas library. The same trick works for README files and internal docs when someone needs a visual preview without building the full site, or a quick screenshot for a presentation.

Slack, Discord, and messaging bots

Both platforms understand markdown, but they render it differently, and neither handles tables well. Convert a weekly report or metrics summary to an image and post it as an attachment. Same look on every device and every client.

This also comes up with AI-generated text. ChatGPT, Claude, and most models return markdown. If you need to share an AI response in Slack, on Twitter, or in a report, just copying it loses all the formatting. Tables and comparisons especially turn into a mess when pasted as plain text. Send the response as the markdown parameter and the image keeps headings, lists, and code blocks looking the way they should.

Email is another place where this pays off. Outlook strips half your CSS, Gmail ignores the other half. If you have a formatted report, a code snippet, or a table that needs to look exactly right in every inbox, render it as an image and embed it. Identical everywhere.

Combining markdown with other parameters

markdown works with everything else in the API, just like url and html. A few combinations worth knowing:

ParameterEffect with markdownUse case
dark_modeSwitches to dark color schemeDev tool dashboards, terminal-style reports
cssOverride default stylingBrand-consistent social cards, custom fonts
widthControls viewport widthNarrow cards (400px) or wide reports (1200px)
formatPNG, WebP, JPEG, or PDFWebP for web, PNG for Slack, PDF for archiving
qualityCompression level for JPEG/WebPBalance file size vs clarity
full_pageCaptures entire document heightLong changelogs, multi-section reports
retina2x pixel densityHigh-DPI displays, print-quality output
omit_backgroundTransparent background (PNG only)Overlay on custom backgrounds

One thing to watch: full_page: true with long markdown documents produces tall images. A 3,000-word changelog renders to roughly 8,000 pixels tall. That's fine for a PDF, but too large for a social card. Set width and let the height auto-calculate, or use resize_width to scale the output down after capture.

Building it yourself vs one API call

If you'd rather own the whole stack, the pieces are: a markdown parser (marked, markdown-it, or remark), an HTML template with your CSS, Playwright or Puppeteer to render and capture. Locally, that's maybe 40 lines of code. The complexity shows up when it runs in production.

You need Chrome installed (300+ MB), fonts that look the same on Linux as they do on your Mac, enough memory for each browser tab (a couple hundred megabytes each), and logic for when something hangs or crashes. If it runs in Docker or Lambda, add cold start delays and Chromium version issues on top. We've seen teams spend a week on this for what amounts to "render some markdown and take a picture."

One API call takes about 2 seconds and costs one credit. If you're already running headless Chrome for other screenshot tasks, adding markdown rendering costs nothing extra. But if markdown-to-image is the only reason you'd spin up a browser, the math favors the API.

What it doesn't do

A few things worth knowing upfront. The default styling looks good for most markdown, but you can't control every detail. If you need exact control over layout and fonts, send ready-made HTML via the html parameter instead. The markdown path is built for speed — you trade some design control for simplicity.

Code blocks show up in a monospace font, but without syntax colors. Code is readable, just not color-highlighted like on GitHub. If you need that, convert your markdown to HTML with a highlighter like Prism or Shiki and send it via html.

Fonts can look slightly different depending on the OS. Our servers run Linux, so Helvetica shows up as Liberation Sans and Arial looks a tiny bit different than on a Mac. If that matters, add a Google Fonts import through the css parameter — that way the font is the same everywhere.

Parameter reference

ParameterTypeRequiredDescription
markdownstringYes (if no url or html)Raw markdown text to render as an image. Max 500,000 characters.
cssstringNoCustom CSS to override default markdown styling.
dark_modebooleanNoSwitch to dark color scheme. Default: false.
widthintegerNoViewport width in pixels. Default: 1280.
heightintegerNoViewport height in pixels. Default: 800.
formatstringNopng, jpeg, webp, avif, pdf. Default: png.
qualityintegerNoCompression quality (1-100) for JPEG/WebP. Default: 80.
full_pagebooleanNoCapture the full document height. Default: false.
retinabooleanNoCapture at 2x pixel density. Default: false.
omit_backgroundbooleanNoTransparent background (PNG only). Default: false.

The markdown parameter is available on all plans, including the free tier. Start with a simple request — your README or a short changelog — and see how the output looks. If the default styling works, you're done. If you want something custom, add css and adjust from there. Most users start with the defaults and never change them.

Markdown to image in one API call

Get your API key — 200 free screenshots/month

Frequently asked questions

The default rendering uses system fonts (sans-serif for text, monospace for code blocks). To use a specific font, add a Google Fonts import through the css parameter — for example, @import url(fonts.googleapis.com/css2?family=Inter); body { font-family: Inter, sans-serif; }. This way the font loads on our server and renders consistently regardless of the operating system.

Code blocks render in a monospace font with a distinct background, but without color syntax highlighting. The text is readable, just not color-coded like on GitHub. If you need syntax colors, convert your markdown to HTML using a highlighter like Prism or Shiki and send it via the html parameter instead.

The markdown parameter accepts up to 500,000 characters per request. That covers even very long documents — a typical README is 2,000–5,000 characters. For reference, 500,000 characters is roughly 80,000–100,000 words.

Yes. All parameters work together. You can send markdown with dark_mode: true and a css override in the same request. The dark mode sets the base color scheme, and your custom CSS overrides anything you want to change on top of that — fonts, spacing, background color, whatever you need.

With markdown, you send plain text and the API handles conversion to HTML, applies default styling, and renders it. With html, you send ready-made HTML and CSS — you control every detail of the layout. Use markdown when you want speed and simplicity. Use html when you need pixel-perfect control over the design.