APITemplate.io Alternative: HTML Cards Instead of Editor

Moving image templates off APITemplate.io? Map overrides, Direct URLs and download_url to HTML templates, with a tested adapter in three languages.

By Simon Chiu · Updated

Disclosure: RenderScreenshot is our product. APITemplate.io facts on this page come from its getting-started guide, its Direct URL docs, its v1 API reference and its pricing page, all read on 2026-10-18. We have no APITemplate.io account. We did not render with it, so this page has no side-by-side images and no timings. The adapter code ran against local stubs of both APIs, not the live services.

APITemplate.io and RenderScreenshot both turn data into images and PDFs over an API. The difference is where the template lives. In APITemplate.io, an image template is a design in its web editor: you name elements such as headline or photo, then send an overrides array that changes their text or image source. In RenderScreenshot, the template is an HTML page in your own repository, rendered by Chromium. Everything else on this page follows from that choice. For the wider market of card generators, see the best OG image generators compared.

TL;DR

Stay with APITemplate.io if Move to RenderScreenshot if
Non-developers design and edit templates in a visual editor Developers own the templates and review them in pull requests
You need generated files hosted for as long as your subscription runs 30-day CDN links are fine, or you copy files to your own bucket
You use its no-code integrations or its regional API hosts You want CSS layout: flexbox, grid, web fonts, long text that wraps
Your volume fits its plan quotas and the editor saves design time You also need screenshots of live pages, not only templates

Two template models

An APITemplate.io image template is a canvas of positioned elements: text, images, shapes, QR codes and barcodes. The API changes properties of those elements by name. That works well for fixed layouts. It gets harder when content varies, for example a title that is sometimes four words and sometimes twenty, because the layout logic sits in the editor and not in code.

An HTML template is a web page. Text wraps, boxes grow, and you can test it in a browser at 1200×630 before any API call. The price is that someone has to write HTML and CSS, and every design change is a code change.

Mapping table

APITemplate.io RenderScreenshot Notes
POST https://rest.apitemplate.io/v2/create-image?template_id=... POST /v1/screenshot with url (your template page) or html See content input for html and html_base_url
X-API-KEY header Authorization: Bearer rs_live_... Bearer token
overrides: [{ "name": "headline", "text": "..." }] Query parameters on your template URL, or values you put into html The adapter below turns each override into headline.text=...
download_url in the response image.url with Accept: application/json; also the X-Cache-URL header Expires with the cache TTL, at most 30 days. See X-Cache-URL
download_url_png next to the JPEG One format per render: output.format is png, jpeg, webp or pdf Each render is one credit
Direct URL: /v2/create-image-url/{template_id}?auth=...&title.text=... Signed GET URL: key_id, expires, signature Signed URLs in og:image tags
Direct URL auth code with a quota and an expiration HMAC signature over every parameter, expiry required, at most 30 days Nobody can change a signed parameter
Direct URL caching: new image only when parameters or template change cache_available=true returns an identical earlier render for 0 credits Without it, each fetch renders
create-pdf-from-html html plus output.format: "pdf" PDF options
create-pdf-from-url url plus output.format: "pdf"
webhook_url (async) webhook: { "url": ... } in the POST body Webhooks
Bring your own S3 bucket (Enterprise plans) storage option, S3-compatible, on paid plans Path template must contain {hash} and {ext}
Regional hosts (EU, Australia, Singapore) One endpoint, api.renderscreenshot.com

Rebuild one template as HTML

Each named element in the editor becomes something on an HTML page. This template reads its values from the query string and writes them with textContent, so a title such as <script> is shown as text and never runs. Host it as a static file next to your site, for example https://www.example.com/cards/promo.html.

<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <style>
    body { margin: 0; width: 1200px; height: 630px; display: flex; font-family: system-ui, sans-serif; }
    .text { flex: 1; padding: 72px; display: flex; flex-direction: column; justify-content: center; }
    h1 { font-size: 64px; line-height: 1.1; margin: 0; overflow-wrap: anywhere; }
    img { width: 480px; height: 630px; object-fit: cover; }
  </style>
</head>
<body>
  <div class="text"><h1 id="headline"></h1></div>
  <img id="photo" alt="">
  <script>
    const q = new URLSearchParams(location.search);
    document.getElementById('headline').textContent = q.get('headline.text') ?? '';
    const src = q.get('photo.src');
    if (src && src.startsWith('https://')) document.getElementById('photo').src = src;
  </script>
</body>
</html>

The script runs before the page's load event, so the photo is part of the load that the renderer waits for by default. Open the file in a browser with a long headline to check wrapping before you switch any traffic. We did not render this template with the API for this page.

Migration: one function, two providers

The adapter keeps the shape your code already uses: it takes an APITemplate.io template ID and an overrides array, and returns an object with download_url. A map from template ID to HTML template name decides which templates are ready to move. RS_PERCENT sends a share of requests to RenderScreenshot. The choice is a hash of the template and data, so the same card always goes to the same provider.

Node.js

// card-image.mjs: one function, two providers. Callers keep receiving { download_url }.
import { createHash } from 'node:crypto';

const RS_PERCENT = Number(process.env.RS_PERCENT ?? 0); // 0..100, raise it step by step
const TEMPLATE_ORIGIN = process.env.CARD_TEMPLATE_ORIGIN;  // where your HTML card templates live

// Overrides [{ name: 'headline', text: 'Hi' }] become ?headline.text=Hi on your HTML template.
function templateUrl(templateName, overrides) {
  const url = new URL(`/cards/${templateName}.html`, TEMPLATE_ORIGIN);
  for (const { name, ...props } of overrides) {
    for (const [prop, value] of Object.entries(props)) url.searchParams.set(`${name}.${prop}`, String(value));
  }
  return url.toString();
}

async function viaApiTemplate(templateId, overrides) {
  const res = await fetch(`https://rest.apitemplate.io/v2/create-image?template_id=${encodeURIComponent(templateId)}`, {
    method: 'POST',
    headers: { 'X-API-KEY': process.env.APITEMPLATE_API_KEY, 'Content-Type': 'application/json' },
    body: JSON.stringify({ overrides }),
  });
  if (!res.ok) throw new Error(`APITemplate.io HTTP ${res.status}`);
  const body = await res.json();
  return { download_url: body.download_url, provider: 'apitemplate' };
}

async function viaRenderScreenshot(templateName, overrides) {
  const res = await fetch(process.env.RS_ENDPOINT ?? 'https://api.renderscreenshot.com/v1/screenshot', {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${process.env.RS_API_KEY}`,
      'Content-Type': 'application/json',
      Accept: 'application/json',
    },
    body: JSON.stringify({
      url: templateUrl(templateName, overrides),
      preset: 'og_card',
      cache: { available: true, ttl: 2592000 },
    }),
  });
  const body = await res.json();
  if (!res.ok) throw new Error(`RenderScreenshot ${res.status} ${body.error?.code}: ${body.error?.message}`);
  return { download_url: body.image.url, expires_at: body.cache?.expires_at, provider: 'renderscreenshot' };
}

// Same template + data always goes to the same provider, so a card never flips between them.
function useRenderScreenshot(key) {
  const bucket = createHash('sha256').update(key).digest()[0] % 100;
  return bucket < RS_PERCENT;
}

// map: APITemplate.io template ID -> name of your HTML template
export async function createCardImage(templateId, overrides, map) {
  const key = templateId + JSON.stringify(overrides);
  if (map[templateId] && useRenderScreenshot(key)) return viaRenderScreenshot(map[templateId], overrides);
  return viaApiTemplate(templateId, overrides);
}

Python

# card_image.py: one function, two providers. Callers keep receiving {'download_url': ...}.
import hashlib
import json
import os
import urllib.request
from urllib.parse import urlencode

RS_ENDPOINT = os.environ.get('RS_ENDPOINT', 'https://api.renderscreenshot.com/v1/screenshot')


def template_url(template_name, overrides):
    query = {f"{o['name']}.{prop}": str(value)
             for o in overrides for prop, value in o.items() if prop != 'name'}
    return f"{os.environ['CARD_TEMPLATE_ORIGIN']}/cards/{template_name}.html?{urlencode(query)}"


def _post(url, headers, payload):
    req = urllib.request.Request(url, data=json.dumps(payload).encode(), headers=headers, method='POST')
    with urllib.request.urlopen(req, timeout=60) as res:  # raises HTTPError on 4xx/5xx
        return json.load(res)


def via_apitemplate(template_id, overrides):
    body = _post(
        f'https://rest.apitemplate.io/v2/create-image?template_id={template_id}',
        {'X-API-KEY': os.environ['APITEMPLATE_API_KEY'], 'Content-Type': 'application/json'},
        {'overrides': overrides},
    )
    return {'download_url': body['download_url'], 'provider': 'apitemplate'}


def via_renderscreenshot(template_name, overrides):
    body = _post(
        RS_ENDPOINT,
        {'Authorization': f"Bearer {os.environ['RS_API_KEY']}",
         'Content-Type': 'application/json', 'Accept': 'application/json'},
        {'url': template_url(template_name, overrides), 'preset': 'og_card',
         'cache': {'available': True, 'ttl': 2592000}},
    )
    return {'download_url': body['image']['url'],
            'expires_at': body.get('cache', {}).get('expires_at'), 'provider': 'renderscreenshot'}


def create_card_image(template_id, overrides, template_map):
    percent = int(os.environ.get('RS_PERCENT', '0'))
    key = (template_id + json.dumps(overrides, sort_keys=True)).encode()
    bucket = hashlib.sha256(key).digest()[0] % 100
    if template_id in template_map and bucket < percent:
        return via_renderscreenshot(template_map[template_id], overrides)
    return via_apitemplate(template_id, overrides)

Ruby

# card_image.rb: one method, two providers. Callers keep receiving { 'download_url' => ... }.
require 'digest'
require 'json'
require 'net/http'
require 'uri'

module CardImage
  RS_ENDPOINT = URI(ENV.fetch('RS_ENDPOINT', 'https://api.renderscreenshot.com/v1/screenshot'))

  module_function

  def template_url(template_name, overrides)
    query = overrides.flat_map do |o|
      o.reject { |k, _| k == 'name' }.map { |prop, value| ["#{o['name']}.#{prop}", value.to_s] }
    end
    "#{ENV.fetch('CARD_TEMPLATE_ORIGIN')}/cards/#{template_name}.html?#{URI.encode_www_form(query)}"
  end

  def post_json(uri, headers, payload)
    res = Net::HTTP.post(uri, JSON.generate(payload), headers.merge('Content-Type' => 'application/json'))
    raise "HTTP #{res.code}: #{res.body}" unless res.is_a?(Net::HTTPSuccess)

    JSON.parse(res.body)
  end

  def via_apitemplate(template_id, overrides)
    uri = URI("https://rest.apitemplate.io/v2/create-image?template_id=#{URI.encode_www_form_component(template_id)}")
    body = post_json(uri, { 'X-API-KEY' => ENV.fetch('APITEMPLATE_API_KEY') }, { overrides: overrides })
    { 'download_url' => body['download_url'], 'provider' => 'apitemplate' }
  end

  def via_renderscreenshot(template_name, overrides)
    body = post_json(RS_ENDPOINT,
                     { 'Authorization' => "Bearer #{ENV.fetch('RS_API_KEY')}", 'Accept' => 'application/json' },
                     { url: template_url(template_name, overrides), preset: 'og_card',
                       cache: { available: true, ttl: 2_592_000 } })
    { 'download_url' => body.dig('image', 'url'), 'expires_at' => body.dig('cache', 'expires_at'),
      'provider' => 'renderscreenshot' }
  end

  def create(template_id, overrides, template_map)
    bucket = Digest::SHA256.digest(template_id + JSON.generate(overrides)).getbyte(0) % 100
    if template_map.key?(template_id) && bucket < ENV.fetch('RS_PERCENT', '0').to_i
      via_renderscreenshot(template_map[template_id], overrides)
    else
      via_apitemplate(template_id, overrides)
    end
  end
end

Tested on 2026-10-18 with Node.js 22.18, Python 3.15 and Ruby 4.0 against local stubs. The tests checked the headers and JSON bodies sent to each provider, the template URL built from the overrides (including & in a headline), and that unmapped templates stay on APITemplate.io. RS_ENDPOINT exists only so a test can point the adapter at a stub. The bucket hash is computed differently in each language, so keep one language per service.

Roll out in steps

  1. Rebuild one template as HTML. Compare it with the APITemplate.io output at 1200×630 for short, long and empty values.
  2. Add it to the map and set RS_PERCENT=10. Check the stored download_url values and your error logs.
  3. Raise the percentage to 50, then 100. Then move the next template.
  4. If you store download_url in a database, also store expires_at. Our links end with the cache TTL; APITemplate.io says paid-plan files stay while the subscription is active. Re-render before expiry (renewal pattern) or copy each file to your own bucket.

Response and auth differences

  • Sync by default on both. APITemplate.io returns JSON with a link. RenderScreenshot returns the image bytes unless you send Accept: application/json, which returns JSON with the link.
  • One secret key vs a key family. APITemplate.io uses one API key in X-API-KEY and per-template auth codes for Direct URLs. RenderScreenshot uses an rs_live_ key for server calls and an rs_pub_/rs_secret_ pair for signed URLs, so the URL in public HTML never contains a credential.
  • Errors. Ours return JSON with a stable code and a docs link for each error, listed in error codes.
  • Rate limit. Ours is per account, shared by all keys. APITemplate.io's pricing page states no number.

Price at three volumes

APITemplate.io lists these plans on its pricing page (checked 2026-10-18, USD). The page shows two prices for each plan, for example $29 and $35 for Starter; we list both because the page did not make clear which one is the annual rate. Image quotas are per month and cover images and PDFs together.

Images per month APITemplate.io RenderScreenshot
1,000 Starter, $29 or $35/month (1,500 images and PDFs) Starter, $10/month (2,500 credits)
10,000 Enterprise, $139 or $179/month (20,000); Standard's 9,000 is just short Growth, $40/month (12,000 credits)
100,000 Above Enterprise 40K ($224 or $280/month for 40,000); pay-as-you-go must be enabled on request More than Scale's 60,000 credits ($100/month); contact us

Two things move these numbers. APITemplate.io's free plan is 50 images or PDFs a month with 3 templates; ours is 150 credits every month. And on our side, a render served from cache with cache_available costs 0 credits, while a failed render costs nothing.

What we did not measure

We did not run a benchmark against APITemplate.io, so this page reports no latency, success rate or file size for either service. If speed matters to you, render the same card on both with a free account each, at least three times per template, and compare the median time and the output bytes. The two outputs will not be identical: one is an editor layout, the other is Chromium's rendering of your HTML.

When to stay with APITemplate.io

  • Your designers own the templates. A visual editor with named elements is the product. Moving to HTML makes developers the bottleneck for every design change.
  • You rely on hosted files that do not expire. Matching that with us needs a paid plan and your own bucket.
  • You use its no-code integrations, such as Zapier, Make, n8n or Airtable, and you do not want to maintain code.
  • You need its regional hosts for data residency.
  • Your templates are mostly PDFs built in its editor. The move only pays off if you also want HTML control.

Try the adapter with a free key

Create a free account, rebuild one template as HTML, and run the adapter with RS_PERCENT=100 against a test template ID. Compare the two images side by side before you move real traffic.

Frequently asked questions

Can I import APITemplate.io image templates into RenderScreenshot?
No. Image templates live in APITemplate.io's visual editor. You rebuild each one once as an HTML page, and each named element becomes a query parameter or a field in the HTML you send.
What replaces download_url?
image.url in the JSON response, which is also sent as the X-Cache-URL header. It is a public CDN link that lasts for the cache TTL, at most 30 days. For files that must last longer, copy each render into your own S3-compatible bucket with the storage option on a paid plan.
Is there an equivalent of APITemplate.io's Direct URL?
Yes. A signed GET URL renders the image when it is first fetched, so it can go straight into an og:image or img tag. It is signed with HMAC-SHA256 and must expire within 30 days.
Can RenderScreenshot make PDFs from HTML like create-pdf-from-html?
Yes. Send html (or url) with output.format set to pdf. Paper size, margins, headers and footers are PDF options of the same endpoint.

Related in this topic

Try RenderScreenshot free

150 free credits every month, no credit card required.