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.