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_selectorthat 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/batchneeds 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 withinsufficient_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:
- Render once, embed the CDN link. Store the
X-Cache-URLfrom 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 withcache.ttl). - Let repeat requests reuse the render. Add
cache.available: true(cache_available=trueonGETand 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 inusage.creditsandusage.remaining. Batch results carryusage.remainingtoo. - 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
- Pricing: plans and prices.
- Caching:
cache.available, TTL andX-Cache-URL. - Bulk screenshots: batch limits and the credit check.
- Error codes: every
authorization_errorcode. - Start with 150 free credits a month: create a free account.