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:
- Completeness. Open the two images side by side. Look for missing images below the fold, placeholder skeletons and cookie banners.
- 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.
- 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.
- 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
- Shadow for a week. Run the Node script, or its
fromAbstractfunction inside your app, on a sample of real traffic. Keep serving Abstract's image. - Fix the differences you see. Most will be timing: add
wait.until: 'networkidle'or await.for_selectorfor pages that render late, andblock.cookie_bannersfor EU sites. - Switch reads, keep the fallback. Serve our image; on any error, call Abstract. Remove the fallback after a clean week.
- Use the cache. Every response carries an
X-Cache-URLheader 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.