A screenshot API credit is one successful render. RenderScreenshot charges 1 credit per image or PDF that comes back, whatever its size, format or page length, and nothing for anything that fails or is served from an earlier render with cache.available. Credits reset every billing period, never roll over, and when they run out the API stops with a clear error instead of billing overage. This guide lists exactly what is charged, how plan changes affect your allowance, and the patterns that keep a bill predictable. It is part of the guides; current prices are always on the pricing page.

Checked on October 15, 2026, against the API source and the billing settings in the dashboard. The credit guard script was tested with Node.js 22.18.0 against a local mock of the API, including the out-of-credits path. Prices below are as listed on the pricing page on that date.

Plans

Plan Price per month Credits per month Cost per 1,000 renders
Free $0, no credit card 150 $0
Starter $10 2,500 $4.00
Growth $40 12,000 $3.33
Scale $100 60,000 $1.67

The cost per 1,000 assumes you use the whole allowance. Plans also differ in rate limits and batch size; see bulk screenshots for the batch limits.

What uses a credit

Event Credits
Successful render: PNG, JPEG, WebP, AVIF, PDF or HTML, any viewport, full page or not 1
cache.available: true and an identical earlier render exists 0
cache.available: true and no earlier render exists 1
cache.refresh: true 1 (it always renders)
cache.enabled: false 1
Failed render (target_error: timeout, navigation failure, selector not found) 0
Validation, authentication or authorization error 0
Rate-limited request (429) 0
Batch: each completed item 1
Batch: each failed item 0
Webhook deliveries, cache management, presets and devices endpoints 0
Loading the X-Cache-URL CDN link 0
Loading a signed URL Same as any request: 1 per render, 0 with cache_available=true and an earlier render

Two consequences worth planning for:

  • Guards are free. A wait.for_selector that fails because a page did not load properly costs nothing, so strict guards save money as well as bad images.
  • Batches check first. POST /v1/batch needs at least as many credits left as it has URLs before it starts, even though failed items are not charged. A 50-URL batch with 40 credits left is refused with insufficient_credits.

Requests that repeat

An og:image or embedded screenshot URL is fetched by every crawler and every visitor whose cache is cold. If each fetch renders, each fetch costs a credit. Two ways to pay once:

  1. Render once, embed the CDN link. Store the X-Cache-URL from the first render and put that in your HTML. Loading it costs nothing until the cache TTL ends (24 hours by default, up to 30 days with cache.ttl).
  2. Let repeat requests reuse the render. Add cache.available: true (cache_available=true on GET and in signed URLs). Requests identical to an earlier render within the TTL return it for 0 credits.
curl "https://api.renderscreenshot.com/v1/screenshot?url=https%3A%2F%2Fexample.com%2Fblog%2Flaunch&preset=og_card&cache_available=true&cache_ttl=604800" \
  -H "Authorization: Bearer $RS_API_KEY" -o card.png -D - | grep -i '^x-credits'

The first call prints X-Credits-Used: 1; the next identical call within the TTL prints X-Credits-Used: 0. Lookups are scoped to your account, and you still need at least 1 credit left to make the request. To pay for a fresh image deliberately, send cache.refresh: true; cache invalidation covers when that is the right call.

Billing periods and plan changes

  • Paid plans reset to the full allowance when each subscription invoice is paid, so your period follows your billing date.
  • Free plan credits reset every month.
  • No rollover. Unused credits do not carry into the next period.
  • Upgrade at any time from Dashboard, Billing. The upgrade is charged pro rata, and your allowance rises to the new plan's total straight away; credits you already used this period still count.
  • Downgrade takes effect at your next billing period.
  • Cancel and the account returns to the Free plan.

Invoices and payment

Billing runs through Stripe. In Dashboard, Billing, Manage Subscription opens Stripe's customer portal, where you change plan, update the card and see past invoices. If a payment fails, the subscription shows as past due in the dashboard until the card is fixed.

Running out

When the balance reaches zero, every render request returns:

{
  "error": {
    "type": "authorization_error",
    "code": "insufficient_credits",
    "message": "Account has no remaining credits",
    "retryable": false
  }
}

with status 403. Nothing is charged and nothing renders. It is not retryable: wait for the reset, or upgrade and retry. There is no overage billing, so a runaway loop cannot produce a surprise bill; it stops.

Watching your balance

Every successful response tells you where you stand:

  • X-Credits-Used: what this request cost (0 or 1).
  • X-Credits-Remaining: what is left this period.
  • With Accept: application/json, the same numbers are in usage.credits and usage.remaining. Batch results carry usage.remaining too.
  • Dashboard, Usage shows the period's totals.

This wrapper reads the header on every call, warns once below a threshold, and turns the out-of-credits error into a clear stop:

// credit-guard.mjs: wrap screenshot calls, track X-Credits-Remaining, warn early and stop cleanly at zero.
// Node 22+, no dependencies.
const API = process.env.RS_API_BASE ?? 'https://api.renderscreenshot.com';
const WARN_AT = Number(process.env.RS_WARN_AT ?? 200); // warn when this many credits are left

export class OutOfCredits extends Error {}
export const state = { remaining: null, warned: false };

export async function screenshot(body) {
  const res = await fetch(`${API}/v1/screenshot`, {
    method: 'POST',
    headers: { Authorization: `Bearer ${process.env.RS_API_KEY}`, 'Content-Type': 'application/json' },
    body: JSON.stringify(body),
  });
  if (res.status === 403) {
    const err = await res.json();
    if (err.error?.code === 'insufficient_credits') throw new OutOfCredits('no credits left until the next reset or an upgrade');
    throw new Error(`403 ${err.error?.code}`);
  }
  if (!res.ok) throw new Error(`${res.status} ${await res.text()}`); // failed renders cost nothing
  state.remaining = Number(res.headers.get('x-credits-remaining'));
  if (!state.warned && state.remaining <= WARN_AT) {
    state.warned = true;
    console.warn(`only ${state.remaining} credits left this period`); // send this to Slack or email instead
  }
  return Buffer.from(await res.arrayBuffer());
}

if (import.meta.url === `file://${process.argv[1]}`) {
  try {
    for (let i = 0; i < Number(process.argv[2] ?? 3); i++) await screenshot({ url: `https://example.com/?n=${i}`, preset: 'og_card' });
    console.log(`done; ${state.remaining} credits left`);
  } catch (err) {
    if (err instanceof OutOfCredits) { console.error(err.message); process.exitCode = 3; } else throw err;
  }
}

Against a mock account with 3 credits and RS_WARN_AT=1, it warned at 1 credit left, then stopped with no credits left until the next reset or an upgrade and exit status 3 instead of retrying.

SDK examples: stop cleanly when credits run out

A 403 on a render request means the account cannot render right now; insufficient_credits is the usual reason. Stop the job instead of retrying, and log the error code.

Node.js

import { Client, TakeOptions, RenderScreenshotError } from 'renderscreenshot';

try {
  await client.take(TakeOptions.url(url).preset('og_card'));
} catch (err) {
  if (err instanceof RenderScreenshotError && err.httpStatus === 403) stopJob(err.code);
  else throw err;
}

Python

try:
    client.take(TakeOptions.url(url).preset('og_card'))
except RenderScreenshotError as e:
    if e.http_status == 403:
        stop_job(e.code)
    else:
        raise

Ruby

begin
  client.take(RenderScreenshot::TakeOptions.url(url).preset('og_card'))
rescue RenderScreenshot::Error => e
  raise unless e.http_status == 403

  stop_job(e.code)
end

PHP

try {
    $client->take(TakeOptions::url($url)->preset('og_card'));
} catch (RenderScreenshotException $e) {
    if ($e->httpStatus !== 403) throw $e;
    stopJob($e->errorCode);
}

Java

try {
    client.take(TakeOptions.url(url).preset("og_card"));
} catch (RenderScreenshotException e) {
    if (e.getHttpStatus() != 403) throw e;
    stopJob(e.getCode());
}

Go

_, err := client.Take(ctx, rs.URL(url).Preset("og_card"))
var apiErr *rs.Error
if errors.As(err, &apiErr) && apiErr.HTTPStatus == 403 {
    stopJob(apiErr.Code)
}

stopJob stands for your own code: pause the queue, alert someone, or schedule the job after the reset. The Node.js, Python, Ruby and Go versions ran against a mock account with no credits and stopped on the 403. The PHP and Java versions follow the SDK references and were not verified.

Troubleshooting

Symptom Likely cause Fix
Credits drop faster than your traffic suggests Embedded URLs render on every fetch Embed X-Cache-URL, or add cache_available=true
X-Credits-Used: 1 on a repeated request Options differ slightly (a timestamp in the URL, another viewport) Make repeated requests byte-identical; drop cache-busting params
insufficient_credits on a batch with credits left Fewer credits than URLs in the batch Smaller batches
403 mid-job every month Allowance too small for the job Upgrade, or spread the job across periods
Upgraded but still blocked Upgrade not finished in Stripe Check Dashboard, Billing; retry after the plan shows
Unused credits disappeared No rollover Size the plan for a typical month

Related in this topic

Was this page helpful?