insufficient_credits

Your account has used all of its credits for the current period. The API stops rendering and returns this error until the allowance resets or you upgrade. Nothing is billed beyond your plan, so this error is the hard stop that protects you from a surprise invoice. This page is part of the error code reference.

HTTP status 403 Forbidden
error.type authorization_error
error.retryable false
Credits used 0

Example responses

A single screenshot when the balance is zero:

{
  "error": {
    "type": "authorization_error",
    "code": "insufficient_credits",
    "message": "Account has no remaining credits",
    "retryable": false,
    "docs": "https://docs.renderscreenshot.com/errors/insufficient_credits"
  },
  "request_id": "req_abc123"
}

A batch needs enough credits for every URL before it starts, so it can fail while you still have some credits:

{
  "error": {
    "type": "authorization_error",
    "code": "insufficient_credits",
    "message": "Insufficient credits. This batch requires 50 credits but you have 12 remaining.",
    "retryable": false,
    "docs": "https://docs.renderscreenshot.com/errors/insufficient_credits"
  },
  "request_id": "req_def456"
}

Why retryable is false

The error clears only when the period resets or the plan changes, which is hours or days away. A retry loop with backoff keeps failing, fills your logs and hides the real problem. Treat it as "pause", not "retry".

Common causes

  • Normal growth. Traffic grew and the plan is now too small.
  • A loop or a crawler. A bug re-renders the same page, or a bot hits a route that creates a screenshot per request.
  • No caching. Identical requests render again every time. With cache.available set, a repeat inside the cache TTL is served from cache at zero credits. See caching.
  • A batch larger than the balance. The batch is refused as a whole.

How to fix it

  1. Check the balance with GET /v1/usage (credits.remaining) or the dashboard.
  2. If the usage is expected, upgrade on the pricing page. The new allowance applies at once.
  3. If the usage is not expected, find the source in the request logs before you upgrade.
  4. Pause screenshot jobs until the reset. Do not drop them: keep them in the queue.
  5. Watch the X-Credits-Remaining response header so you know before you reach zero.

Pause the queue, do not retry

This script checks the balance before it starts, warns when the X-Credits-Remaining header gets low, and stops at the first insufficient_credits. It exits with code 75 (EX_TEMPFAIL) so a scheduler can run it again later.

// credit-guard.mjs (Node 22+): stop a queue cleanly when the credits run out.
const API = process.env.RS_BASE_URL ?? 'https://api.renderscreenshot.com';
const headers = { Authorization: `Bearer ${process.env.RS_API_KEY}` };

export class OutOfCredits extends Error {}

export async function creditsRemaining() {
  const res = await fetch(`${API}/v1/usage`, { headers });
  if (!res.ok) throw new Error(`GET /v1/usage failed with HTTP ${res.status}`);
  return (await res.json()).credits.remaining;
}

export async function capture(url) {
  const res = await fetch(`${API}/v1/screenshot`, {
    method: 'POST',
    headers: { ...headers, 'Content-Type': 'application/json' },
    body: JSON.stringify({ url, preset: 'og_card' }),
  });
  if (res.ok) {
    const left = Number(res.headers.get('x-credits-remaining'));
    if (left < 100) console.warn(`Only ${left} credits left in this period`);
    return Buffer.from(await res.arrayBuffer());
  }
  const { error, request_id } = await res.json();
  if (error.code === 'insufficient_credits') throw new OutOfCredits(`${error.message} (${request_id})`);
  throw new Error(`${res.status} ${error.code}: ${error.message}`);
}

// Process a list, but check the balance first and stop at the first 403.
const urls = process.argv.slice(2);
const before = await creditsRemaining();
if (before < urls.length) console.warn(`${urls.length} URLs, ${before} credits: some will not run`);

for (const [i, url] of urls.entries()) {
  try {
    await capture(url);
    console.log(`ok ${url}`);
  } catch (e) {
    if (!(e instanceof OutOfCredits)) throw e;
    console.error(`Out of credits. ${urls.length - i} URLs left for the next period: ${e.message}`);
    process.exitCode = 75; // EX_TEMPFAIL: tell the scheduler to try again later
    break;
  }
}

With the Ruby SDK, the error is a RenderScreenshot::AuthorizationError with e.code == 'insufficient_credits':

require 'renderscreenshot'

client = RenderScreenshot.client(ENV.fetch('RS_API_KEY'))

begin
  image = client.take(RenderScreenshot::TakeOptions.url('https://example.com').preset('og_card'))
rescue RenderScreenshot::AuthorizationError => e
  raise unless e.code == 'insufficient_credits'

  # Not retryable until the allowance resets or the plan changes.
  warn "Paused screenshot jobs: #{e.message} (#{e.request_id})"
  exit 75
end

Tested on 2026-10-15 with Node 22.18, and Ruby 4.0.7 plus the renderscreenshot gem 1.0.0, against a local mock that started with 3 credits. Five URLs gave three captures, low-balance warnings, a clean stop on the fourth URL and exit code 75.

Related in this topic

Was this page helpful?