Cloudinary Screenshot Alternative: Add-on vs a Dedicated API

Cloudinary's URL2PNG add-on next to a dedicated screenshot API: option mapping, signing, CDN delivery and expiry, plus a tested switch in Node, Python and Ruby.

By Simon Chiu · Updated

Disclosure: RenderScreenshot is our product. Cloudinary facts on this page come from its URL2PNG add-on docs and its pricing page; URL2PNG option units come from the URL2PNG docs. All were read on 2026-10-18. We have no Cloudinary account with the add-on, so this page has no side-by-side screenshots and no timings. The migration code builds both providers' URLs offline; no URL was fetched from either service.

People search for a Cloudinary alternative for two different jobs. One is website screenshots, which Cloudinary offers through the URL2PNG add-on. The other is social cards, which Cloudinary makes with text overlays on a base image; that job is compared in the best OG image generators. This page is about the first job: an add-on inside a media platform against an API that only renders pages.

TL;DR

Stay with Cloudinary if Move to RenderScreenshot if
Your images already live in Cloudinary and you transform them with its URL syntax Screenshots are the main job, not one image source among many
You want permanent derived images and format negotiation (f_auto) on one CDN You want capture options: cookie-banner blocking, dark mode, waiting, clicks, PDF
A few screenshot options (viewport, full page, delay, user agent) are enough You want official SDKs in six languages and typed error codes
One vendor and one bill matter more than capture control You can regenerate image URLs within 30 days, or store files yourself

How each one works

Cloudinary. You enable the URL2PNG add-on, then request an image whose delivery type is url2png and whose public ID is the website URL. Options go after the URL as /url2png/viewport=1280x800|fullpage=true. Cloudinary transformations can follow, for example a resize or an overlay. By default the URL must be signed (the s--...-- part) or generated ahead of time with the explicit API. Once a version exists, its derived transformations can use unsigned URLs. You can also turn off the signing requirement for the add-on in the console's security settings.

RenderScreenshot. The capture is the product. A GET /v1/screenshot URL with flat parameters is the image, signed with an HMAC so it can sit in public HTML. Or your server sends a POST with the full option set and gets bytes or JSON back. Every response carries a public CDN link, X-Cache-URL, that lives for the cache TTL.

Option mapping

Cloudinary URL2PNG add-on RenderScreenshot Notes
type: "url2png", public ID = website URL url parameter GET reference
viewport=1280x800 width=1280, height=800 Width 1 to 7680, height 1 to 4320
fullpage=true full_page=true POST adds scroll options for lazy images
delay=2 (seconds, per URL2PNG docs) delay=2000 (milliseconds) Capped at 10000
user_agent=... user_agent=...
Signed delivery URL (sign_url: true) Signed GET URL: key_id, expires, signature Ours always expires, at most 30 days. See signed URLs in og:image tags
explicit API to generate ahead of time POST /v1/screenshot with your rs_live_ key Store the returned link or file
Transformation w_600,c_scale output.resize: { "width": 600 } POST only, so it cannot go in a signed URL. A GET can only set the viewport
f_auto, q_auto format (png, jpeg, webp) and quality One fixed format per render, no negotiation per browser
Overlays, borders, rounded corners Not available Do these in CSS on a page you capture, or keep Cloudinary for this step
Cached derived image on Cloudinary's CDN CDN link for the cache TTL (1 hour to 30 days) Or your own bucket with the storage option
Other URL2PNG options See the URL2PNG alternative mapping Same option names as URL2PNG itself

Delivery compared

This is where the two products differ most, and it decides the migration more than any option does.

Cloudinary add-on RenderScreenshot
Who serves the image Cloudinary's CDN, as a derived asset Our CDN via X-Cache-URL, or the API itself for a signed URL
How long the URL works Cloudinary's docs say the screenshots are cached and delivered through its CDN; they do not state a cache duration for the add-on Signed URL: until expires (at most 30 days). CDN link: until the TTL ends (at most 30 days)
Refresh Not described in the add-on docs cache_refresh=true, or a purge
Further processing Any Cloudinary transformation in the same URL Resize in the same POST; anything else happens before capture, in the page
Repeat fetches Delivery bandwidth counts toward Cloudinary credits A cache hit with cache_available costs 0 credits

If you need permanent URLs and Cloudinary's transformations, the honest answer may be both: capture with RenderScreenshot, then upload the result into Cloudinary from its CDN link with Cloudinary's upload API, and keep delivering from there.

Migration: one function, two providers

The listings take the URL2PNG-style options you already pass to Cloudinary and return an image URL. RS_PERCENT moves a share of target URLs to RenderScreenshot; the share is picked by a hash of the target, so one page always uses one provider. Both sides use the official SDKs. The RenderScreenshot side signs cache_available=true, so repeat views of an unchanged screenshot cost 0 credits, and uses an expiry that stays the same for a 7-day window.

Node.js

// screenshot-url.mjs: build a website screenshot URL from URL2PNG-style options,
// with Cloudinary and RenderScreenshot behind one function.
import { createHash } from 'node:crypto';
import { v2 as cloudinary } from 'cloudinary';
import { Client, TakeOptions } from 'renderscreenshot';

const rs = new Client(process.env.RS_API_KEY, {
  signingKey: process.env.RS_SIGNING_KEY,    // rs_secret_...
  publicKeyId: process.env.RS_PUBLIC_KEY_ID, // rs_pub_...
});
const DAY = 86400 * 1000;
const RS_PERCENT = Number(process.env.RS_PERCENT ?? 0);

// Before: the Cloudinary URL2PNG add-on (cloudinary.config() reads CLOUDINARY_URL).
function cloudinaryUrl(target, { viewport = '1280x1024', fullpage = false, delay, userAgent } = {}) {
  const opts = [`viewport=${viewport}`, `fullpage=${fullpage}`];
  if (delay) opts.push(`delay=${delay}`);
  if (userAgent) opts.push(`user_agent=${userAgent}`);
  return cloudinary.url(`${target}/url2png/${opts.join('|')}`, { type: 'url2png', sign_url: true, secure: true });
}

// After: a signed RenderScreenshot GET URL with the same options.
function renderScreenshotUrl(target, { viewport = '1280x1024', fullpage = false, delay, userAgent } = {}) {
  const [width, height] = viewport.split('x').map(Number);
  let options = TakeOptions.url(target).width(width).height(height).cacheAvailable().cacheTtl(2592000);
  if (fullpage) options = options.fullPage();
  if (delay) options = options.delay(Math.min(delay * 1000, 10000)); // URL2PNG: seconds; ours: ms, max 10000
  if (userAgent) options = options.userAgent(userAgent);
  // A 7-day window keeps the URL stable for a week; it always has 21 to 28 days left.
  const start = Math.floor(Date.now() / (7 * DAY)) * 7 * DAY;
  return rs.generateUrl(options, new Date(start + 28 * DAY));
}

export function screenshotUrl(target, opts) {
  const bucket = createHash('sha256').update(target).digest()[0] % 100;
  return bucket < RS_PERCENT ? renderScreenshotUrl(target, opts) : cloudinaryUrl(target, opts);
}

Python

# screenshot_url.py: URL2PNG-style options -> Cloudinary or RenderScreenshot image URL.
import hashlib
import os
import time

import cloudinary  # reads CLOUDINARY_URL
from cloudinary.utils import cloudinary_url
from renderscreenshot import Client, TakeOptions

rs = Client(
    os.environ['RS_API_KEY'],
    signing_key=os.environ['RS_SIGNING_KEY'],      # rs_secret_...
    public_key_id=os.environ['RS_PUBLIC_KEY_ID'],  # rs_pub_...
)
DAY = 86400


def cloudinary_screenshot_url(target, viewport='1280x1024', fullpage=False, delay=None, user_agent=None):
    opts = [f'viewport={viewport}', f"fullpage={'true' if fullpage else 'false'}"]
    if delay:
        opts.append(f'delay={delay}')
    if user_agent:
        opts.append(f'user_agent={user_agent}')
    url, _ = cloudinary_url(f"{target}/url2png/{'|'.join(opts)}", type='url2png', sign_url=True, secure=True)
    return url


def renderscreenshot_url(target, viewport='1280x1024', fullpage=False, delay=None, user_agent=None):
    width, height = (int(n) for n in viewport.split('x'))
    options = TakeOptions.url(target).width(width).height(height).cache_available().cache_ttl(2592000)
    if fullpage:
        options = options.full_page()
    if delay:
        options = options.delay(min(delay * 1000, 10000))  # URL2PNG: seconds; ours: ms, max 10000
    if user_agent:
        options = options.user_agent(user_agent)
    start = int(time.time() // (7 * DAY)) * 7 * DAY  # stable for a week, 21 to 28 days left
    return rs.generate_url(options, start + 28 * DAY)


def screenshot_url(target, **opts):
    bucket = hashlib.sha256(target.encode()).digest()[0] % 100
    if bucket < int(os.environ.get('RS_PERCENT', '0')):
        return renderscreenshot_url(target, **opts)
    return cloudinary_screenshot_url(target, **opts)

Ruby

# screenshot_url.rb: URL2PNG-style options -> Cloudinary or RenderScreenshot image URL.
require 'digest'
require 'base64'     # cloudinary 2.4.3 calls Base64 without requiring it (Ruby 3.4+)
require 'cloudinary' # reads CLOUDINARY_URL
require 'renderscreenshot'

module ScreenshotUrl
  DAY = 86_400
  RS = RenderScreenshot::Client.new(
    ENV.fetch('RS_API_KEY'),
    signing_key: ENV.fetch('RS_SIGNING_KEY'),     # rs_secret_...
    public_key_id: ENV.fetch('RS_PUBLIC_KEY_ID')  # rs_pub_...
  )

  module_function

  def cloudinary(target, viewport: '1280x1024', fullpage: false, delay: nil, user_agent: nil)
    opts = ["viewport=#{viewport}", "fullpage=#{fullpage}"]
    opts << "delay=#{delay}" if delay
    opts << "user_agent=#{user_agent}" if user_agent
    Cloudinary::Utils.cloudinary_url("#{target}/url2png/#{opts.join('|')}", type: 'url2png', sign_url: true, secure: true)
  end

  def render_screenshot(target, viewport: '1280x1024', fullpage: false, delay: nil, user_agent: nil)
    width, height = viewport.split('x').map(&:to_i)
    options = RenderScreenshot::TakeOptions.url(target).width(width).height(height).cache_available.cache_ttl(2_592_000)
    options = options.full_page if fullpage
    options = options.delay([delay * 1000, 10_000].min) if delay # URL2PNG: seconds; ours: ms, max 10000
    options = options.user_agent(user_agent) if user_agent
    start = (Time.now.to_i / (7 * DAY)) * 7 * DAY # stable for a week, 21 to 28 days left
    RS.generate_url(options, expires_at: Time.at(start + (28 * DAY)))
  end

  def for(target, **opts)
    bucket = Digest::SHA256.digest(target).getbyte(0) % 100
    bucket < ENV.fetch('RS_PERCENT', '0').to_i ? render_screenshot(target, **opts) : cloudinary(target, **opts)
  end
end

Tested on 2026-10-18 with Node.js 22.18 (cloudinary 2.7.0), Python 3.15 (cloudinary 1.44.1) and Ruby 4.0 (cloudinary 2.4.3), with the RenderScreenshot SDKs built from source. All three printed the same signed RenderScreenshot URL for the same input, and a copy of our API's signature check accepted it. The Cloudinary URLs were built with fake credentials and not fetched, so we did not confirm that Cloudinary accepts them.

Roll out in steps

  1. Find every place that builds a url2png URL, and route them through the function above with RS_PERCENT=0. Nothing changes yet.
  2. Check the options your code uses against the mapping table. Transformations after the capture (resize, overlays) need a POST and a stored file, or need to stay on Cloudinary.
  3. Set RS_PERCENT=10. Open a sample of pages and compare the two images.
  4. Make sure every page that shows a screenshot builds its URL at render time. Our URLs expire, so a URL stored in a database stops working within 30 days.
  5. Raise to 100, then remove the Cloudinary branch.

Response and auth differences

  • Credentials. Cloudinary signs delivery URLs with your API secret through its SDK. We sign with a separate rs_secret_ key, and the URL carries only the rs_pub_ ID, so the key that signs URLs cannot call the rest of the API.
  • What a signed URL locks. Every parameter, including the target and the expiry. A changed parameter fails with invalid_signature.
  • Server-side calls. POST /v1/screenshot with a Bearer key returns the image bytes, or JSON with image.url when you send Accept: application/json.
  • Errors. In an <img> tag a failed screenshot shows as a broken image on both services. Our error bodies carry a stable code and a link to a page about that error.

Price at three volumes

Cloudinary's pricing page (checked 2026-10-18, USD) lists Free with 25 credits a month, Plus at $99 a month ($89 billed yearly) with 225 credits, Advanced at $249 a month ($224 billed yearly) with 600 credits, and Enterprise on request. One credit is 1,000 transformations, 1 GB of storage or 1 GB of image bandwidth. The page says paid plans can use paid add-ons; we could not find the URL2PNG add-on's own quotas and prices on a public page, because the add-on marketplace is inside the console.

Screenshots per month Cloudinary RenderScreenshot
1,000 A Cloudinary plan plus the add-on's quota price (not public); bandwidth for delivery uses plan credits Starter, $10/month (2,500 credits)
10,000 Same: plan plus add-on quota Growth, $40/month (12,000 credits)
100,000 Same: plan plus add-on quota; Enterprise on request More than Scale's 60,000 credits ($100/month); contact us

On our side, a render is one credit whatever the size or format, a cache hit is 0 credits, and loading the CDN link uses no credits. Our free plan has 150 credits every month. If you already pay for Cloudinary for other images, the add-on may cost you less than a second vendor; check its price in your console before you decide.

What we did not measure

We did not benchmark the URL2PNG add-on against our API, so this page reports no latency, success rate or file size. To compare them yourself, take 10 to 20 of your own target URLs, fetch each one at least three times from both services with the same viewport, and record the median time for the first (uncached) and later (cached) fetches and the bytes of each image.

When to stay with Cloudinary

  • Screenshots are a small part of a large media workflow. Your images, transformations and delivery are already in Cloudinary, and the add-on is one more source.
  • You need permanent URLs without running storage. Matching that with us needs a paid plan and your own bucket.
  • You chain transformations in the URL, such as resizing for several breakpoints with f_auto. Our GET URL cannot resize after capture.
  • The add-on's options cover your pages. If viewport, full page and a delay give good screenshots of your targets, there is little to gain.

Try the switch with a free key

Create a free account and a signing key pair, run the listing with RS_PERCENT=100 for one target URL, and open both images side by side.

Frequently asked questions

Does Cloudinary take website screenshots?
Yes, through the URL2PNG Website Screenshots add-on. You register for the add-on in the Cloudinary console, then request a URL with the url2png delivery type and the target website as the public ID. By default those URLs must be signed or generated eagerly.
Can I keep Cloudinary for delivery and use another API for the capture?
Yes. Render the screenshot with a server-side request, then upload the result to Cloudinary from its public CDN link with Cloudinary's upload API. Cloudinary then handles resizing, formats and delivery as it does for any other image.
How do Cloudinary's URL2PNG options map to RenderScreenshot?
viewport=1280x800 becomes width=1280 and height=800, fullpage=true becomes full_page=true, delay in seconds becomes delay in milliseconds (at most 10000), and user_agent stays user_agent.
Do RenderScreenshot screenshot URLs expire?
Yes. A signed URL must expire within 30 days, and an X-Cache-URL link lasts for the cache TTL, at most 30 days. For a permanent file, copy each render into your own S3-compatible bucket on a paid plan.

Related in this topic

Try RenderScreenshot free

150 free credits every month, no credit card required.