ScreenshotNeo

BlogHow-to

How to Capture a Webpage Screenshot Through an API in a React App in India

Capture webpages from React through a server-side API route that protects your key. Get runnable code, India-specific checks, and capture troubleshooting.

By the ScreenshotNeo team4 October 202612 min read

Direct answer: Have your React app send a capture request to your own backend. The backend validates the requested URL, calls a screenshot API using a server-side credential, and returns the resulting image or an image URL to React. Never put a production API key in browser code: users can inspect bundled JavaScript and browser requests.

The steps are the same in India as elsewhere. The available research does not establish which providers render from an Indian IP, process data in India, bill in INR, or offer India-specific availability. If those matter, confirm them directly with the provider before choosing one.

1. How the request flow works

  1. The user enters or selects a webpage in your React interface.
  2. React sends the URL and permitted capture settings to your backend.
  3. The backend checks authorization, validates the URL and options, and calls the screenshot provider.
  4. The backend returns image bytes or a provider URL, matching that provider’s documented response.
  5. React displays or downloads the result.

Providers differ in endpoint, authentication, request schema, response shape, available formats, and capture controls. Check the selected provider’s current documentation before adapting the example. The reviewed documentation describes both binary-image responses and URL or JSON response patterns. [Screenshot API documentation] [ScreenshotAPI.to documentation] [screenshot-api.net documentation]

2. Build a small React client and Node.js backend

This runnable example uses a backend route that calls a provider returning image bytes. The upstream endpoint and request fields shown are placeholders: replace them with the exact endpoint, authentication scheme, and capture parameters documented by your chosen provider. Do not deploy placeholder values.

Backend: Express proxy

Install the dependencies and set the provider URL and secret in the server environment:

npm install express
npm install --save-dev nodemon
SCREENSHOT_API_URL=https://provider.example/v1/screenshot
SCREENSHOT_API_KEY=replace-with-server-side-secret
PORT=3001

Save as server.mjs. Node.js 18 or newer provides the global fetch used here.

import express from 'express';

const app = express();
app.use(express.json({ limit: '16kb' }));

const allowedHosts = new Set(['example.com', 'www.example.com']);

function validateTarget(value) {
  let target;
  try {
    target = new URL(value);
  } catch {
    throw new Error('Enter a valid absolute URL.');
  }
  if (target.protocol !== 'https:' && target.protocol !== 'http:') {
    throw new Error('Only HTTP and HTTPS URLs are allowed.');
  }
  if (!allowedHosts.has(target.hostname)) {
    throw new Error('This host is not allowed.');
  }
  return target.href;
}

app.post('/api/screenshot', async (req, res) => {
  let targetUrl;
  try {
    targetUrl = validateTarget(req.body?.url);
  } catch (error) {
    return res.status(400).json({ error: error.message });
  }

  if (!process.env.SCREENSHOT_API_KEY || !process.env.SCREENSHOT_API_URL) {
    return res.status(500).json({ error: 'Screenshot service is not configured.' });
  }

  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), 60000);

  try {
    const upstream = await fetch(process.env.SCREENSHOT_API_URL, {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${process.env.SCREENSHOT_API_KEY}`,
        'Content-Type': 'application/json',
        'Accept': 'image/png'
      },
      body: JSON.stringify({ url: targetUrl, format: 'png', full_page: true }),
      signal: controller.signal
    });

    if (!upstream.ok) {
      const detail = await upstream.text();
      console.error('Screenshot provider error:', upstream.status, detail);
      return res.status(502).json({ error: 'The screenshot provider could not complete the capture.' });
    }

    const contentType = upstream.headers.get('content-type') || '';
    if (!contentType.startsWith('image/')) {
      console.error('Unexpected provider content type:', contentType);
      return res.status(502).json({ error: 'The provider returned an unexpected response.' });
    }

    const image = Buffer.from(await upstream.arrayBuffer());
    res.set('Content-Type', contentType);
    res.set('Cache-Control', 'private, no-store');
    return res.status(200).send(image);
  } catch (error) {
    const timeout = error.name === 'AbortError';
    console.error('Screenshot request failed:', error);
    return res.status(timeout ? 504 : 502).json({
      error: timeout ? 'The screenshot request timed out.' : 'Could not reach the screenshot provider.'
    });
  } finally {
    clearTimeout(timer);
  }
});

app.listen(Number(process.env.PORT || 3001), () => {
  console.log(`Screenshot backend listening on port ${process.env.PORT || 3001}`);
});

The allowlist is an important security boundary. A public endpoint that accepts arbitrary URLs can be abused to request screenshots of internal services or private network addresses. For a product that must capture arbitrary sites, use a deliberate SSRF defense: block loopback, private, link-local and reserved IP ranges; resolve and validate DNS results; account for redirects and DNS rebinding; and enforce outbound network controls. Do not rely on a hostname string check alone.

In production, also authenticate the person calling your backend, apply per-user rate limits, cap request size and capture dimensions, and avoid logging credentials or sensitive target URLs. The example returns provider errors as a generic message to the browser while logging details on the server.

React client

Call your own backend, receive the binary response as a Blob, and create a temporary object URL for display and download:

import { useEffect, useState } from 'react';

export default function ScreenshotForm() {
  const [url, setUrl] = useState('https://example.com');
  const [imageUrl, setImageUrl] = useState('');
  const [error, setError] = useState('');
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    return () => {
      if (imageUrl) URL.revokeObjectURL(imageUrl);
    };
  }, [imageUrl]);

  async function capture(event) {
    event.preventDefault();
    setLoading(true);
    setError('');
    setImageUrl('');

    try {
      const response = await fetch('/api/screenshot', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ url })
      });

      if (!response.ok) {
        const data = await response.json().catch(() => ({}));
        throw new Error(data.error || `Capture failed (${response.status})`);
      }

      const blob = await response.blob();
      if (!blob.type.startsWith('image/')) {
        throw new Error('The server did not return an image.');
      }
      setImageUrl(URL.createObjectURL(blob));
    } catch (err) {
      setError(err.message || 'Could not capture the webpage.');
    } finally {
      setLoading(false);
    }
  }

  return (
    <main>
      <form onSubmit={capture}>
        <label htmlFor="target-url">Webpage URL</label>
        <input
          id="target-url"
          type="url"
          value={url}
          onChange={(event) => setUrl(event.target.value)}
          required
        />
        <button type="submit" disabled={loading}>
          {loading ? 'Capturing…' : 'Capture screenshot'}
        </button>
      </form>
      {error && <p role="alert">{error}</p>}
      {imageUrl && (
        <section>
          <img src={imageUrl} alt="Captured webpage" />
          <a href={imageUrl} download="webpage.png">Download PNG</a>
        </section>
      )}
    </main>
  );
}

Configure your development server to proxy /api to the backend, or use a full backend URL and configure CORS on your server. A same-origin production deployment is often simpler: it avoids browser CORS configuration and keeps the provider call server-side.

3. cURL, Python, and Node.js request patterns

These examples demonstrate the common authenticated HTTP shape for a provider that accepts JSON and returns image bytes. Replace the illustrative endpoint, credential header, and request fields with the provider’s documented contract. Never put a secret in React environment variables that are compiled into browser assets.

cURL

curl --fail-with-body \
  -X POST "$SCREENSHOT_API_URL" \
  -H "Authorization: Bearer $SCREENSHOT_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Accept: image/png" \
  --data '{"url":"https://example.com","format":"png","full_page":true}' \
  --output screenshot.png

Python

import os
import requests

endpoint = os.environ['SCREENSHOT_API_URL']
key = os.environ['SCREENSHOT_API_KEY']

response = requests.post(
    endpoint,
    headers={
        'Authorization': f'Bearer {key}',
        'Accept': 'image/png',
    },
    json={
        'url': 'https://example.com',
        'format': 'png',
        'full_page': True,
    },
    timeout=(10, 60),
)
response.raise_for_status()
if not response.headers.get('Content-Type', '').startswith('image/'):
    raise RuntimeError(f"Expected image response, got {response.headers.get('Content-Type')}")
with open('screenshot.png', 'wb') as image:
    image.write(response.content)

Node.js

const endpoint = process.env.SCREENSHOT_API_URL;
const key = process.env.SCREENSHOT_API_KEY;

const response = await fetch(endpoint, {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${key}`,
    'Content-Type': 'application/json',
    Accept: 'image/png'
  },
  body: JSON.stringify({
    url: 'https://example.com',
    format: 'png',
    full_page: true
  }),
  signal: AbortSignal.timeout(60000)
});

if (!response.ok) {
  throw new Error(`Capture failed with HTTP ${response.status}: ${await response.text()}`);
}
const contentType = response.headers.get('content-type') || '';
if (!contentType.startsWith('image/')) {
  throw new Error(`Expected image bytes, got ${contentType}`);
}
await import('node:fs/promises').then(({ writeFile }) =>
  writeFile('screenshot.png', Buffer.from(await response.arrayBuffer()))
);

If the provider returns JSON containing a URL instead of bytes, parse JSON on the backend and either return the URL or fetch the image server-side. Check URL expiration, access controls, and retention before storing or exposing that URL.

4. Choose capture settings that match the job

There is no universal parameter set. Use only controls documented by the selected API. Common questions to resolve before shipping:

Need What to check
Viewport screenshot Viewport width and height, device presets, and whether device scale factor affects output pixels.
Entire document Whether full-page capture is supported and how lazy-loaded content is triggered.
Image format PNG for lossless detail, JPEG for smaller photographic images, or WebP if the provider and consumers support it.
Wait behavior Whether the service supports a delay, selector wait, or network-idle condition, and the maximum wait.
Dynamic or authenticated pages Whether custom headers, cookies, or an authorization mechanism are supported, and how secrets are protected.
Large capture Maximum dimensions, full-page limits, output size, and whether the provider switches to asynchronous jobs.

For India-facing sites, test representative pages that depend on consent dialogs, regional redirects, localized content, or access restrictions. A caller’s location does not prove the remote browser’s location. If geolocation, timezone, regional pricing, or an Indian egress IP is required, verify that the provider supports the exact control you need and confirm it with a real account before making a commitment.

5. India-specific checks before production

  • Rendering region: Ask where the browser runs and whether you can select an India region or Indian IP. The reviewed sources do not verify this for the listed providers.
  • Data location and retention: Confirm where screenshots and submitted URLs are processed and stored, retention duration, deletion behavior, and applicable terms.
  • Billing: Confirm currency, taxes, payment methods, invoices, and whether the provider supports the billing arrangement your organization needs. Do not assume INR pricing.
  • Availability and quotas: Confirm account eligibility, rate limits, plan quotas, concurrency, and support arrangements directly with the vendor.
  • Network behavior: Check whether target websites restrict datacenter traffic, require regional access, or return bot checks to remote browsers.

These are procurement and deployment checks, not properties you can infer from a successful HTTP request. The documentation reviewed for this guide does not establish India-based rendering, local processing, INR billing, or India-specific reliability.

6. Security and reliability checklist

  • Keep the provider key in a server-side secret store or environment configuration.
  • Authenticate users and authorize which targets they may capture.
  • Defend against server-side request forgery, including private IPs, redirects, and DNS resolution changes.
  • Set a timeout at both the backend and provider request layers; return a clear timeout response.
  • Rate-limit by user and cap concurrent work so a burst cannot exhaust your quota.
  • Validate the upstream content type and response size before forwarding it.
  • Use bounded retries only for transient failures. Do not blindly retry a request that may incur a charge or create duplicate jobs.
  • For slow captures, use a job ID and polling or a signed webhook if the provider supports asynchronous jobs.
  • Decide whether results may be cached. Cache only when the target and data sensitivity permit it, and respect the provider’s cache and retention behavior.

7. Performance, reliability, and cost

Capture time depends on the target page, its assets and scripts, the requested viewport or full-page length, provider queueing, and network conditions. The research contains no independent latency, reliability, or India-region benchmark, so test your own representative URLs rather than planning around an unsupported timing promise.

Keep the UI responsive while work runs. Disable duplicate submissions, show a pending state, and provide a way to retry after a clear failure. For long-running work, use an asynchronous job flow instead of holding an HTTP request open. Set request deadlines with enough room for expected page loading, but keep them bounded.

Estimate cost from the provider’s current plan, billable-event definition, quotas, and retry policy. Verify whether failed captures, cache hits, or asynchronous jobs count as usage; providers define these differently. Monitor request counts and failures on your backend, and avoid logging keys or full sensitive URLs. Recheck plan terms before launch because vendor pricing and quotas can change.

8. Troubleshooting

Symptom Likely cause Fix
401 or 403 from provider Missing, invalid, expired, or incorrectly formatted credential; account lacks access. Check the provider’s required auth scheme and server secret configuration. Never solve this by exposing the key in React.
400 or 422 Wrong endpoint schema, unsupported option, malformed URL, or invalid dimensions. Compare the request body and parameter names with current provider docs. Start with the smallest documented request and add options one at a time.
Browser reports CORS error React is calling the third-party provider directly, or your backend CORS policy omits the app origin. Call your own backend from React. If cross-origin access is necessary, allow only your app origins on your backend.
JSON appears where an image was expected The provider returns an error or a JSON object containing a screenshot URL. Inspect status and content type server-side; parse the documented response shape rather than treating every successful response as image bytes.
Blank or incomplete screenshot The page needs more time, lazy content was not loaded, a selector was not ready, or access was blocked. Use supported wait controls, check target behavior from the provider’s browser environment, and inspect the provider’s status or verdict fields if available.
504 or timeout Slow target, long scripts, queue delay, or deadline too short. Set a bounded but suitable timeout, reduce unnecessary full-page work, and consider an asynchronous job workflow.
Works locally but fails after deployment Missing production secret, outbound network restrictions, timeout differences, or incorrect proxy routing. Check server configuration and outbound access; verify the deployed app routes to the backend and does not bundle credentials.
Unexpected targets can be captured The route accepts arbitrary URLs without sufficient network validation. Require authorization and enforce SSRF protections on resolved addresses, redirects, and outbound network access.
Usage rises unexpectedly Repeated clicks, automatic retries, polling, or unbounded user access. Debounce submissions, deduplicate jobs where supported, rate-limit users, and track provider usage.

9. Or skip the browser setup

ScreenshotNeo is a website screenshot API and MCP server. One GET request can return a PNG, JPEG, WebP, or PDF; the API key belongs on your backend. See the ScreenshotNeo API documentation for request options and current details.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

Use this request from your backend and keep YOUR_API_KEY out of browser code. ScreenshotNeo removes cookie banners, newsletter popups, and chat widgets before the shot; bot checks, blank pages, and failed loads are never billed. Its MCP server lets AI agents take screenshots. The free plan includes 1,000 screenshots a month with no card, and paid plans start at $5 for 3,000.

Sign up free for 1,000 screenshots a month, with no card required.

10. FAQ

Can React take a screenshot without a backend?

A browser can capture its own rendered page with browser-side tools, but capturing arbitrary remote webpages through a provider requires a provider request. Keep a production provider credential on the server and call it through your backend.

Does using an API guarantee the screenshot is rendered in India?

No. The fact that your app is used in India does not establish where the provider’s browser runs. Confirm rendering region and egress location with the provider.

Can I return the screenshot as a URL instead of image bytes?

Yes, if the chosen provider returns a URL and its access and expiration behavior suit your app. Match your backend to the documented response, and avoid exposing private or long-lived links unintentionally.

Which image format should I choose?

Choose a format supported by both the capture API and your consumers. PNG is a straightforward default for crisp UI captures; compare output size and visual needs for other formats.

How do I capture a page that requires login?

Use only provider-documented cookie or header support, protect those credentials, and confirm the provider’s processing and retention terms are acceptable for the page’s data.