Skip to content

Core concepts

Error handling

Parse errors, decide what to retry, and build a robust client.

Response format

Every error has a JSON body with a single error string and a meaningful status code:

{ "error": "Insufficient credits: 1 required, 0 available" }

Successful capture responses are binary, so always check the status (or Content-Type) before treating a response as an image.

Retry strategy

Status Retry? Strategy
400 No Fix the request.
401 Invalid/Inactive/Expired API Key No Fix the key. Repeated failures lead to a temporary IP block.
401 Rate limit exceeded: … (plan) Yes Exponential backoff starting at ~1 s. Reduce concurrency.
401 Rate limit exceeded for the API key: quota … No Wait for the rolling window, or raise the key's quota.
402 No Billing action required (payment, invoices, credits).
403 No Upgrade the plan or wait for the IP block to lift.
404 No The target page doesn't exist.
408 / network timeout Yes Retry with backoff; for slow pages use is_async=true.
413 No Reduce output size (lower scale_factor, use selector, or format=pdf).
500, 502, 503, 504 Yes Up to 3 retries with exponential backoff (1 s, 1.5 s, 2.25 s…).

Failed requests are never billed, so retries never double-charge you.

A complete TypeScript handler

export class SnaplensError extends Error {
  constructor(public status: number, message: string, public retryable: boolean) {
    super(message);
  }
}

const RETRYABLE = new Set([408, 500, 502, 503, 504]);

function isRetryable(status: number, message: string) {
  if (RETRYABLE.has(status)) return true;
  // 401 is shared by auth errors and rate limits: only retry plan rate limits.
  return status === 401 && message.startsWith("Rate limit exceeded:");
}

const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));

export async function snaplens(path: string, params: Record<string, string>, maxRetries = 3): Promise<Response> {
  const url = `http://localhost:4000${path}?${new URLSearchParams(params)}`;
  for (let attempt = 0; ; attempt++) {
    let res: Response;
    try {
      res = await fetch(url, {
        headers: { "x-api-key": process.env.SNAPLENS_API_KEY! },
        signal: AbortSignal.timeout(65_000),
      });
    } catch (err) {
      if (attempt < maxRetries) {
        await sleep(1000 * 1.5 ** attempt);
        continue;
      }
      throw new SnaplensError(0, `Network error: ${(err as Error).message}`, true);
    }

    if (res.ok) return res;

    const body = await res.json().catch(() => ({}) as { error?: string });
    const message = body.error ?? res.statusText;
    const retryable = isRetryable(res.status, message);

    if (retryable && attempt < maxRetries) {
      await sleep(1000 * 1.5 ** attempt);
      continue;
    }
    throw new SnaplensError(res.status, message, retryable);
  }
}

// usage
try {
  const res = await snaplens("/v1/capture", { url: "https://example.com" });
  const image = Buffer.from(await res.arrayBuffer());
} catch (err) {
  if (err instanceof SnaplensError && err.status === 402) {
    // notify billing owner
  }
  throw err;
}

The official SDKs implement exactly this logic, raising SnaplensApiError with status and message.

soft_fail for no-code tools

Some automation tools stop on any non-2xx response. Add soft_fail=true to always receive 200 and read the error field instead. See Global parameters.

Logging

Every request — including failures — appears in API Logs in the dashboard with its status, duration and error message, so you can debug without reproducing the call.