Abstract Screenshot API Alternative: Map and Migrate

An Abstract Screenshot API alternative with timing control, blocking and signed URLs: parameter map, pricing at three volumes and tested migration code.

By Simon Chiu · Updated

Disclosure: I build RenderScreenshot. Abstract is a broad API suite, and if you use several of its APIs under one account, that convenience is a real reason to stay. This page tells you when.

Teams look for an Abstract Screenshot API alternative for one of three reasons: the screenshots are taken before the page has finished rendering, a cookie banner covers half the image, or the price of the Standard plan does not fit a volume of a few thousand shots. Abstract's screenshot endpoint is deliberately small: a URL, a viewport, a delay, some CSS and a user agent. RenderScreenshot adds the timing and blocking controls that fix the first two problems and smaller plans for the third. This page maps every Abstract parameter, compares prices, and gives you tested migration code in Node, Python and Ruby. It is one of the spokes of our best screenshot API comparison.

Last verified: October 15, 2026, against the Abstract Website Screenshot API documentation and the product and pricing page.

Should you switch?

Stay with Abstract if Move to RenderScreenshot if
You already pay for several Abstract APIs and value one vendor, one dashboard Screenshots catch spinners or empty sections because the page renders after load
Your targets are your own simple pages, captured full page as JPEG Cookie banners, ads or chat widgets cover your captures
You need 3 requests a second at most and 60,000 requests a month suits you You need fewer than 60,000 screenshots a month and want a smaller plan
You only call the API server-side You want to embed screenshot URLs in public HTML with signed, expiring links
You need element capture, retina output, WebP or AVIF, PDFs or batch jobs

The default that changes every image

Abstract's capture_full_page defaults to true, and export_format defaults to jpeg. Most integrations never set either, so they receive full-page JPEGs without knowing it. RenderScreenshot defaults to the 1280x800 viewport and PNG. If you port a request parameter by parameter and leave defaults alone, your thumbnails turn into short PNGs and every layout that expected a tall image breaks.

Port the behaviour, not the parameter list: set capture.mode to full_page and output.format to jpeg unless you send the opposite to Abstract today. The migration code below does that automatically.

Parameter map

Abstract RenderScreenshot (POST body) Notes
api_key (query) Authorization: Bearer rs_live_... Query-string auth with api_key also works on our GET endpoint, for testing
url url Also html or markdown input on our side
capture_full_page (default true) capture.mode: full_page or viewport (default) Use the full_page preset to add scroll simulation for lazy images
width viewport.width (default 1280, 1 to 7680)
height viewport.height (default 800, 1 to 4320)
delay (seconds) wait.delay (milliseconds) Multiply by 1000
css_injection page.styles (array of CSS strings or {url} objects)
user_agent browser.user_agent
export_format (jpeg default, png) output.format (png default, jpeg, webp, avif, pdf, html)
no equivalent wait.until (load, domcontentloaded, networkidle), wait.for_selector The fix for half-rendered captures
no equivalent block.cookie_banners, block.ads, block.trackers, block.chat_widgets
no equivalent capture.selector with capture.mode: element One element instead of the page
no equivalent viewport.scale (1 to 3) Retina output

Full parameter references: wait, capture, blocking, output options.

Abstract's product page FAQ mentions raw HTML input and GIF output, but the API reference documents neither, so the table only maps what the reference lists.

Pricing at three volumes

List prices, monthly billing, checked on October 15, 2026. One credit is one screenshot on RenderScreenshot.

Monthly screenshots Abstract RenderScreenshot
1,000 $99 (Standard, 60,000 requests) $10 (Starter, 2,500 credits)
10,000 $99 (Standard, 60,000 requests) $40 (Growth, 12,000 credits)
100,000 Enterprise, price on request above our largest listed plan (Scale, $100 for 60,000 credits); ask us
Free 100 requests, 1 request/second 150 credits every month, no credit card

Sources: Abstract screenshot pricing and our pricing. Abstract's Standard plan has a rate of 3 requests per second. Our rate limits are set per minute and per concurrent request, and rise with the plan (rate limits).

Between 1,000 and about 50,000 screenshots a month we cost less. Near 60,000 a month the two list prices are close ($99 against $100), and Abstract's single plan includes 3 requests a second.

Test before you move

We have not run Abstract through our benchmark yet, so this page has no latency or success-rate numbers for it, and no side-by-side images. Instead, run the shadow script below on your own URLs. It sends the same request to both APIs at the same time and saves both images, so you can compare them on the pages that matter to you.

What to look at, in this order:

  1. Completeness. Open the two images side by side. Look for missing images below the fold, placeholder skeletons and cookie banners.
  2. Shape. Both images should have the same width and roughly the same height. If ours is 800 px tall, the request did not ask for a full page.
  3. Latency. The script prints milliseconds per provider. Run it at least three times per URL; the first request after a deploy is often slower on any service.
  4. Bytes. JPEG at quality 80 against JPEG at our default quality of 80 should be close. Large differences mean different content, not a better encoder.

Migration code

Node: shadow both APIs

// shadow.mjs: send each request to Abstract and RenderScreenshot, keep both images.
// Node 22+, npm i renderscreenshot
// Usage: ABSTRACT_KEY=... RS_API_KEY=rs_live_... node shadow.mjs https://example.com
import { Client, TakeOptions, RenderScreenshotError } from 'renderscreenshot';
import { writeFile } from 'node:fs/promises';

const ABSTRACT = process.env.ABSTRACT_BASE ?? 'https://screenshot.abstractapi.com';
const rs = new Client(process.env.RS_API_KEY, process.env.RS_BASE_URL ? { baseUrl: process.env.RS_BASE_URL } : {});

// Translate the query parameters you send to Abstract today into TakeOptions.
// Abstract defaults: capture_full_page=true, export_format=jpeg, delay in seconds.
export function fromAbstract(params) {
  let opts = TakeOptions.url(params.url);
  if (params.width) opts = opts.width(Number(params.width));
  if (params.height) opts = opts.height(Number(params.height));
  if ((params.capture_full_page ?? 'true') !== 'false') opts = opts.fullPage();
  opts = opts.format(params.export_format === 'png' ? 'png' : 'jpeg');
  if (params.delay) opts = opts.delay(Number(params.delay) * 1000); // seconds -> ms
  if (params.user_agent) opts = opts.userAgent(params.user_agent);
  // css_injection has no TakeOptions method in the SDK docs; send it as
  // page.styles in a raw POST body instead (see the Python version below).
  return opts;
}

async function viaAbstract(params) {
  const qs = new URLSearchParams({ api_key: process.env.ABSTRACT_KEY, ...params });
  const res = await fetch(`${ABSTRACT}/v1/?${qs}`, { signal: AbortSignal.timeout(60_000) });
  if (!res.ok) throw new Error(`Abstract ${res.status}: ${await res.text()}`);
  return Buffer.from(await res.arrayBuffer());
}

const params = { url: process.argv[2] ?? 'https://example.com', width: '1280', height: '800', export_format: 'png' };
const timed = async (fn) => { const t0 = performance.now(); const buf = await fn(); return { buf, ms: Math.round(performance.now() - t0) }; };

try {
  const [a, b] = await Promise.all([timed(() => viaAbstract(params)), timed(() => rs.take(fromAbstract(params)))]);
  await writeFile('abstract.png', a.buf);
  await writeFile('renderscreenshot.png', b.buf);
  console.log(`abstract: ${a.ms} ms, ${a.buf.length} bytes`);
  console.log(`renderscreenshot: ${b.ms} ms, ${b.buf.length} bytes`);
} catch (err) {
  if (err instanceof RenderScreenshotError) console.error(`RenderScreenshot ${err.code}: ${err.message}`);
  else console.error(err.message);
  process.exitCode = 1;
}

Python: re-render a URL list (no SDK)

This version calls the REST API directly with the standard library, so it also shows the nested POST body, including page.styles for Abstract's css_injection.

"""rerender.py: re-capture a list of URLs with RenderScreenshot (raw HTTP, no SDK).

Python 3.10+, standard library only.
Usage: RS_API_KEY=rs_live_... python3 rerender.py urls.txt
"""
import json
import os
import sys
import urllib.error
import urllib.parse
import urllib.request

API = os.environ.get("RS_BASE_URL", "https://api.renderscreenshot.com")


def body_from_abstract(params: dict) -> dict:
    """Map Abstract query parameters to the nested POST body."""
    body = {
        "url": params["url"],
        "viewport": {"width": int(params.get("width", 1280)), "height": int(params.get("height", 800))},
        # Abstract captures the full page unless capture_full_page=false.
        "capture": {"mode": "viewport" if params.get("capture_full_page") == "false" else "full_page"},
        # Abstract returns JPEG unless export_format=png.
        "output": {"format": params.get("export_format", "jpeg")},
    }
    if "delay" in params:
        body["wait"] = {"delay": int(params["delay"]) * 1000}  # seconds -> ms
    if "css_injection" in params:
        body["page"] = {"styles": [params["css_injection"]]}
    if "user_agent" in params:
        body["browser"] = {"user_agent": params["user_agent"]}
    return body


def capture(body: dict) -> bytes:
    req = urllib.request.Request(
        f"{API}/v1/screenshot",
        data=json.dumps(body).encode(),
        headers={"Authorization": f"Bearer {os.environ['RS_API_KEY']}", "Content-Type": "application/json"},
        method="POST",
    )
    with urllib.request.urlopen(req, timeout=60) as res:
        return res.read()


def main(path: str) -> int:
    failed = 0
    for line in open(path):
        url = line.strip()
        if not url:
            continue
        body = body_from_abstract({"url": url, "css_injection": ".cookie-banner{display:none}"})
        try:
            data = capture(body)
        except urllib.error.HTTPError as err:
            failed += 1
            print(f"{url}: HTTP {err.code} {err.read()[:200]!r}", file=sys.stderr)
            continue
        name = urllib.parse.quote(url, safe="") + ".jpg"
        with open(name, "wb") as fh:
            fh.write(data)
        print(f"{url}: {len(data)} bytes -> {name}")
    return 1 if failed else 0


if __name__ == "__main__":
    sys.exit(main(sys.argv[1] if len(sys.argv) > 1 else "urls.txt"))

Ruby: a drop-in service object

# screenshot_service.rb: drop-in replacement for an Abstract-backed service object.
# gem install renderscreenshot
require 'renderscreenshot'

class ScreenshotService
  def initialize(client: RenderScreenshot.client(ENV.fetch('RS_API_KEY'), base_url: ENV['RS_BASE_URL']))
    @client = client
  end

  # Same arguments the old Abstract wrapper took; same defaults as Abstract
  # (full page, JPEG), so existing callers see the same kind of image.
  def capture(url, width: 1280, height: 800, full_page: true, format: 'jpeg', delay_seconds: nil)
    options = RenderScreenshot::TakeOptions.url(url).width(width).height(height).format(format)
    options = options.full_page if full_page
    options = options.delay(delay_seconds * 1000) if delay_seconds
    attempts = 0
    begin
      @client.take(options)
    rescue RenderScreenshot::RateLimitError => e
      attempts += 1
      raise if attempts > 3

      sleep(e.retry_after || 1)
      retry
    end
  end
end

if $PROGRAM_NAME == __FILE__
  image = ScreenshotService.new.capture(ARGV.fetch(0, 'https://example.com'), delay_seconds: 1)
  File.binwrite('screenshot.jpg', image)
  puts "saved #{image.bytesize} bytes"
end

All three tested on October 15, 2026 against a local mock of both APIs that checks auth and records each request body: Node 22.18.0 with [email protected], Python 3.14.8, Ruby 4.0.7 with the renderscreenshot gem 1.0.0. Not run against production; use the shadow script for that with your own keys.

Rollout plan

  1. Shadow for a week. Run the Node script, or its fromAbstract function inside your app, on a sample of real traffic. Keep serving Abstract's image.
  2. Fix the differences you see. Most will be timing: add wait.until: 'networkidle' or a wait.for_selector for pages that render late, and block.cookie_banners for EU sites.
  3. Switch reads, keep the fallback. Serve our image; on any error, call Abstract. Remove the fallback after a clean week.
  4. Use the cache. Every response carries an X-Cache-URL header with a public CDN link that needs no key. Store it instead of the bytes if you only display the image (caching docs).

When to stay with Abstract

Stay if Abstract's screenshot API already gives you complete images for your targets, and you buy other Abstract APIs under the same account. One vendor review, one invoice and one dashboard are worth something, and moving one endpoint out of a suite adds an integration to maintain. Stay too if your volume is close to 60,000 a month and 3 requests a second is enough; at that point the list prices are almost the same.

Move if you see half-rendered pages, banners over the content, or you pay $99 a month for a few thousand screenshots. Create a free key and run the shadow script on the ten URLs that look worst today.

Frequently asked questions

Why do my images look different after switching from Abstract?
Abstract captures the full page and returns JPEG by default. RenderScreenshot captures the 1280x800 viewport and returns PNG by default. Set full page and the format explicitly in the new request and the output matches in shape.
Does Abstract's Screenshot API wait for JavaScript or block cookie banners?
Its parameter reference lists a delay in seconds, CSS injection and a custom user agent, but no wait-for-selector, network-idle wait or blocking options. RenderScreenshot has wait.until, wait.for_selector, block.cookie_banners and block.ads.
How much does Abstract's Screenshot API cost?
On October 15, 2026, Abstract listed a free plan with 100 requests and a Standard plan at $99 a month for 60,000 requests, with Enterprise on request. Each Abstract API has its own key and plan.
Can I keep Abstract for its other APIs?
Yes. Abstract issues a separate key per API, so moving screenshots does not touch your email validation, IP geolocation or other Abstract integrations.

Related in this topic

Try RenderScreenshot free

150 free credits every month, no credit card required.