Blog
Tutorial

How to Take Full-Page Screenshots via API: Node.js, Python & cURL

Learn how to capture full-page screenshots via API using cURL, Node.js, and Python. Covers async polling, lazy-load delays, and viewport options with real code examples.

Full-page screenshots — images that capture the entire scrollable document rather than just the visible viewport — are one of the most common requests developers bring to a screenshot API. They are essential for archiving web pages, generating PDF reports, auditing layouts, and producing complete visual records for legal or compliance purposes. This tutorial walks you through taking full-page screenshots with ScreenshotFreeAPI using cURL, Node.js, and Python, including the async polling pattern that makes production use reliable.

How Full-Page Screenshots Work

A regular viewport screenshot captures only what is visible in the browser window — typically 1280×800 pixels. A full-page screenshot instructs the browser engine to measure the full scrollable height of the document, then systematically capture and stitch every section into a single continuous image. On pages with lazy-loaded images (loaded as the user scrolls), the engine must also scroll the page to trigger image loading before stitching begins.

ScreenshotFreeAPI handles all of this automatically when you pass "fullPage": true. You can also pass a delay parameter (in milliseconds) to give JavaScript-heavy pages additional time to settle before the capture fires.

Step 1: Create the Screenshot Job

All screenshot requests to ScreenshotFreeAPI are asynchronous. You POST a job and receive an HTTP 202 Accepted response immediately, along with a jobId. This means your application never blocks waiting for a 15-second Playwright capture.

cURL

bash
# Step 1: Create the job
curl -X POST https://api.screenshotfreeapi.com/screenshots/web \
  -H "Authorization: Bearer sfa_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com",
    "fullPage": true,
    "format": "png",
    "viewport": { "width": 1440, "height": 900 },
    "delay": 1500
  }'

# Response:
# { "jobId": "job_01hwxyz", "status": "queued" }

Step 2: Poll for Job Status

Once you have the jobId, poll the status endpoint until the status transitions to completed or failed. For production use, prefer webhooks (covered below) — polling is fine for scripts and CLIs.

bash
# Step 2: Poll status
curl https://api.screenshotfreeapi.com/jobs/job_01hwxyz/status \
  -H "Authorization: Bearer sfa_YOUR_KEY"

# Response while processing:
# { "jobId": "job_01hwxyz", "status": "processing", "progress": 45 }

# Response when done:
# { "jobId": "job_01hwxyz", "status": "completed" }

Step 3: Fetch the Result

bash
# Step 3: Fetch the result
curl https://api.screenshotfreeapi.com/jobs/job_01hwxyz/result \
  -H "Authorization: Bearer sfa_YOUR_KEY"

# Response:
# {
#   "jobId": "job_01hwxyz",
#   "status": "completed",
#   "screenshots": [
#     {
#       "url": "https://sfa-results.s3.amazonaws.com/job_01hwxyz/full.png?X-Amz-Expires=900...",
#       "format": "png",
#       "width": 1440,
#       "height": 8320
#     }
#   ]
# }

Warning

Presigned S3 URLs in the result are valid for 15 minutes. Download and store the image in your own storage as soon as you retrieve the result — do not link users directly to the presigned URL.

Complete Node.js Example (Async/Await, Node 18+)

The following example uses the native fetch API available in Node 18+ with no additional dependencies. It implements the full create → poll → fetch pattern with exponential backoff polling.

typescript
const API_KEY = process.env.SFA_API_KEY!; // sfa_YOUR_KEY
const BASE_URL = 'https://api.screenshotfreeapi.com';

async function sleep(ms: number): Promise<void> {
  return new Promise(resolve => setTimeout(resolve, ms));
}

async function pollUntilComplete(jobId: string, maxAttempts = 30): Promise<string> {
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
    const res = await fetch(`${BASE_URL}/jobs/${jobId}/status`, {
      headers: { Authorization: `Bearer ${API_KEY}` },
    });

    if (!res.ok) throw new Error(`Status check failed: ${res.status}`);

    const data = await res.json() as { status: string; progress?: number };

    if (data.status === 'completed') return jobId;
    if (data.status === 'failed') throw new Error(`Job ${jobId} failed`);

    const backoff = Math.min(1000 * 2 ** attempt, 8000);
    console.log(`Job ${jobId}: ${data.status} (${data.progress ?? 0}%) — retrying in ${backoff}ms`);
    await sleep(backoff);
  }
  throw new Error(`Job ${jobId} did not complete within the timeout`);
}

async function takeFullPageScreenshot(url: string): Promise<string> {
  // Step 1: Create the job
  const createRes = await fetch(`${BASE_URL}/screenshots/web`, {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${API_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      url,
      fullPage: true,
      format: 'png',
      viewport: { width: 1440, height: 900 },
      delay: 1500,
    }),
  });

  if (!createRes.ok) throw new Error(`Create failed: ${createRes.status}`);
  const { jobId } = await createRes.json() as { jobId: string };
  console.log(`Created job: ${jobId}`);

  // Step 2: Poll until complete
  await pollUntilComplete(jobId);

  // Step 3: Fetch result
  const resultRes = await fetch(`${BASE_URL}/jobs/${jobId}/result`, {
    headers: { Authorization: `Bearer ${API_KEY}` },
  });

  if (!resultRes.ok) throw new Error(`Result fetch failed: ${resultRes.status}`);

  const result = await resultRes.json() as {
    screenshots: Array<{ url: string; width: number; height: number }>;
  };

  const screenshotUrl = result.screenshots[0].url;
  console.log(`Screenshot ready: ${screenshotUrl}`);
  return screenshotUrl;
}

// Usage
takeFullPageScreenshot('https://example.com')
  .then(url => console.log('Download from:', url))
  .catch(console.error);

Complete Python Example

The Python example uses httpx for async HTTP — install it with pip install httpx. The pattern is identical: create, poll, fetch.

python
import asyncio
import os
import httpx

API_KEY = os.environ["SFA_API_KEY"]  # sfa_YOUR_KEY
BASE_URL = "https://api.screenshotfreeapi.com"
HEADERS = {"Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json"}


async def poll_until_complete(client: httpx.AsyncClient, job_id: str, max_attempts: int = 30) -> None:
    for attempt in range(max_attempts):
        r = client.get(f"{BASE_URL}/jobs/{job_id}/status", headers=HEADERS)
        r.raise_for_status()
        data = r.json()

        if data["status"] == "completed":
            return
        if data["status"] == "failed":
            raise RuntimeError(f"Job {job_id} failed")

        backoff = min(1.0 * 2**attempt, 8.0)
        print(f"Job {job_id}: {data['status']} ({data.get('progress', 0)}%) — retrying in {backoff:.1f}s")
        await asyncio.sleep(backoff)

    raise TimeoutError(f"Job {job_id} did not complete within allowed attempts")


async def take_full_page_screenshot(url: str) -> str:
    async with httpx.AsyncClient() as client:
        # Step 1: Create the job
        r = await client.post(
            f"{BASE_URL}/screenshots/web",
            headers=HEADERS,
            json={
                "url": url,
                "fullPage": True,
                "format": "png",
                "viewport": {"width": 1440, "height": 900},
                "delay": 1500,
            },
        )
        r.raise_for_status()
        job_id = r.json()["jobId"]
        print(f"Created job: {job_id}")

        # Step 2: Poll until complete
        await poll_until_complete(client, job_id)

        # Step 3: Fetch the result
        r = await client.get(f"{BASE_URL}/jobs/{job_id}/result", headers=HEADERS)
        r.raise_for_status()
        screenshot_url = r.json()["screenshots"][0]["url"]
        print(f"Screenshot ready: {screenshot_url}")
        return screenshot_url


if __name__ == "__main__":
    url = asyncio.run(take_full_page_screenshot("https://example.com"))
    print("Download from:", url)

Tips for Better Full-Page Screenshots

  • Use `delay` for lazy-loaded content — set "delay": 2000 on pages that defer images below the fold. This triggers a full scroll pass before capture.
  • Set a wide viewport for desktop captures"width": 1440 is a safe default for modern widescreen layouts. Use "width": 375 for mobile-first layouts.
  • Choose WebP for small file sizes — full-page captures of long pages can produce 10MB+ PNG files. "format": "webp" with "quality": 85 cuts file size by 60–80% with no perceptible quality loss.
  • Use webhooks in production — instead of polling, pass "webhookUrl": "https://yourapp.com/hooks/screenshot" and receive a POST when the job completes. Eliminates polling overhead entirely.
  • Cache aggressively — pass "cacheKey": "homepage-v2" and repeated requests for the same URL return the cached result instantly without a new Playwright session.

Start capturing full-page screenshots in minutes. Free tier includes 100 screenshots/month — no credit card required.

Start free — no credit card

Priya Nair

Senior Engineer at ScreenshotFreeAPI