ScreenshotNeo

BlogHow-to

How to Download and Store Screenshot API Captures in an Indian AWS Region

Capture a website, store its image privately in Amazon S3 Mumbai (ap-south-1), and share it safely with authenticated access or a presigned URL.

By the ScreenshotNeo team4 October 202612 min read

Direct answer: request the screenshot from a trusted server or worker, confirm the response format, and upload the resulting image bytes as a private object in an S3 bucket configured for Mumbai (ap-south-1). Retrieve it with your application’s AWS permissions or a short-lived presigned URL. The S3 region tells you where the stored object resides; it does not establish where a screenshot provider rendered or temporarily handled the page.

“Screenshot API” can mean a website-rendering service, or AWS EC2’s GetConsoleScreenshot action for troubleshooting an instance console. They have different response formats and purposes. This guide covers both response handling and the S3 storage workflow, with website captures as the primary case.

1. Identify what the screenshot API returns

Check the provider’s current API reference before writing the download step. A successful request might return raw PNG, JPEG, or WebP bytes; JSON containing a Base64-encoded image; or JSON containing a URL that you fetch separately. Check the HTTP status and response content type before saving anything as an image.

Response type What to do Typical handling
Binary image body Upload the response bytes directly. Stream large bodies where practical. Check status and an expected image content type.
Base64 in JSON Parse the documented field and Base64-decode it to bytes. Reject malformed or unexpectedly large payloads.
Image URL in JSON Fetch the URL, then validate that second response before upload. Apply timeouts and restrict which destinations your worker can fetch.

For example, AWS EC2’s GetConsoleScreenshot retrieves a JPG screenshot of a running instance for troubleshooting, and its response content is Base64-encoded. It is not a general website screenshot API. See the AWS EC2 GetConsoleScreenshot reference. A website screenshot provider may use a different response shape.

2. Create or choose an S3 bucket in India

AWS lists Asia Pacific (Mumbai) as ap-south-1. Create the destination bucket in that region, then configure your SDK or CLI to use the bucket’s actual region. AWS publishes regional S3 endpoint information; do not assume a bucket is in the region selected in an unrelated application setting.

Keep the bucket private. S3 Block Public Access is enabled by default for new buckets, and effective public-access controls can also be influenced at account and organization levels. Use IAM-authorized reads or presigned URLs rather than making the bucket public just to simplify downloads. See S3 Block Public Access.

The bucket region describes the S3 destination. It does not prove that the screenshot service’s renderer, temporary storage, logs, or delivery network are in India. If your requirement covers the full data lifecycle, verify those locations and retention terms with the screenshot provider.

3. Download a binary capture and upload it with Python

The following runnable pattern assumes the screenshot API returns raw image bytes. Install the libraries with python -m pip install requests boto3. Set the API key, bucket name, and region in the environment. The AWS SDK obtains credentials through its normal credential chain; for production, prefer an attached role or another managed identity over hard-coded keys.

import os
from datetime import datetime, timezone
from urllib.parse import urlsplit

import boto3
import requests

SCREENSHOT_API_URL = os.environ["SCREENSHOT_API_URL"]
SCREENSHOT_API_KEY = os.environ["SCREENSHOT_API_KEY"]
S3_BUCKET = os.environ["S3_BUCKET"]
AWS_REGION = os.getenv("AWS_REGION", "ap-south-1")
PAGE_URL = "https://example.com"

# Use a unique key so a retry does not silently overwrite a previous capture.
now = datetime.now(timezone.utc)
key = f"captures/{now:%Y/%m/%d}/{now:%H%M%S}-{os.urandom(6).hex()}.png"

with requests.get(
    SCREENSHOT_API_URL,
    params={"url": PAGE_URL, "access_key": SCREENSHOT_API_KEY},
    stream=True,
    timeout=(10, 90),
) as response:
    response.raise_for_status()
    content_type = response.headers.get("Content-Type", "").split(";", 1)[0].lower()
    if content_type not in {"image/png", "image/jpeg", "image/webp"}:
        raise ValueError(f"Expected an image response, got {content_type or 'no content type'}")

    # Streaming avoids keeping the full response in an extra Python bytes object.
    response.raw.decode_content = True
    s3 = boto3.client("s3", region_name=AWS_REGION)
    s3.upload_fileobj(
        response.raw,
        S3_BUCKET,
        key,
        ExtraArgs={
            "ContentType": content_type,
            "Metadata": {
                "source-host": (urlsplit(PAGE_URL).hostname or "unknown")[:255],
                "captured-at": now.isoformat(),
            },
        },
    )

print(f"Stored s3://{S3_BUCKET}/{key} in {AWS_REGION}")

Replace SCREENSHOT_API_URL and the request parameters with the provider’s documented endpoint and authentication scheme. Do not store an API secret in browser JavaScript. The example records only the source host rather than the full URL, which may contain private query parameters. S3 objects consist of data and metadata; keep any richer capture record, such as viewport and job ID, in a database or in carefully selected object metadata. See Amazon S3 object and bucket concepts.

4. Upload the capture with cURL and the AWS CLI

For a simple binary response, save the image to a temporary file, verify the HTTP result, then upload it using an AWS identity configured on the machine. This flow is useful for a shell-based worker; for large captures, a streaming SDK flow can avoid the temporary local file.

set -eu

SCREENSHOT_API_URL="${SCREENSHOT_API_URL:?Set SCREENSHOT_API_URL}"
SCREENSHOT_API_KEY="${SCREENSHOT_API_KEY:?Set SCREENSHOT_API_KEY}"
S3_BUCKET="${S3_BUCKET:?Set S3_BUCKET}"
AWS_REGION="${AWS_REGION:-ap-south-1}"
PAGE_URL="https://example.com"
KEY="captures/$(date -u +%Y/%m/%d)/$(date -u +%H%M%S)-$$.png"
TMP_FILE="$(mktemp)"
trap 'rm -f "$TMP_FILE"' EXIT

curl --fail --silent --show-error --location \
  --connect-timeout 10 --max-time 90 \
  -G "$SCREENSHOT_API_URL" \
  --data-urlencode "url=$PAGE_URL" \
  --data-urlencode "access_key=$SCREENSHOT_API_KEY" \
  -o "$TMP_FILE"

aws s3 cp "$TMP_FILE" "s3://$S3_BUCKET/$KEY" \
  --region "$AWS_REGION" --content-type "image/png"
printf 'Stored s3://%s/%s in %s\n' "$S3_BUCKET" "$KEY" "$AWS_REGION"

The shell example assumes PNG output and a provider that accepts those illustrative query parameters; adapt both to the provider’s documentation. If you need to verify the response content type in shell, capture headers with cURL and reject a response that is not an expected image before uploading.

5. Handle Base64 JSON responses

If the documented API returns Base64 inside JSON, decode the named field before calling S3. This Python example shows the essential conversion; replace image_base64 and the request details with the actual response schema.

import base64
import boto3
import requests
from io import BytesIO

response = requests.get(
    "https://provider.example/api/capture",
    headers={"Authorization": "Bearer " + API_TOKEN},
    params={"url": "https://example.com"},
    timeout=(10, 90),
)
response.raise_for_status()
payload = response.json()
image_bytes = base64.b64decode(payload["image_base64"], validate=True)

boto3.client("s3", region_name="ap-south-1").upload_fileobj(
    BytesIO(image_bytes),
    S3_BUCKET,
    OBJECT_KEY,
    ExtraArgs={"ContentType": "image/jpeg"},
)

Use the content type and format documented by the provider; do not label a decoded JPG as PNG. Bound response sizes where possible, since Base64 expands the data representation and JSON parsing can hold additional copies in memory.

6. Retrieve a private object

For application-to-application access, let the application read the object with IAM permissions scoped to the necessary bucket prefix. For temporary user downloads, create a presigned GET URL for one object and use a short expiration suitable for the task:

import boto3

s3 = boto3.client("s3", region_name="ap-south-1")
url = s3.generate_presigned_url(
    "get_object",
    Params={"Bucket": S3_BUCKET, "Key": OBJECT_KEY},
    ExpiresIn=300,
)
print(url)

A presigned URL is a bearer link: anyone who obtains it can use the signed operation until it expires, within the permissions of the identity that created it. Avoid writing it to application logs, analytics, tickets, or public pages. Generate a new link after expiry. If using a presigned PUT instead of uploading through an SDK, scope it to one object, use the correct region, and send the same signed headers, including content type when required. Uploading again to the same key replaces the existing object. See AWS presigned URL guidance.

This example uses Node.js’s built-in fetch and the AWS SDK v3. Install the SDK with npm install @aws-sdk/client-s3 @aws-sdk/s3-request-presigner. It assumes the API returns binary image bytes.

import { randomUUID } from "node:crypto";
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";

const apiUrl = process.env.SCREENSHOT_API_URL;
const apiKey = process.env.SCREENSHOT_API_KEY;
const bucket = process.env.S3_BUCKET;
const region = process.env.AWS_REGION || "ap-south-1";
if (!apiUrl || !apiKey || !bucket) throw new Error("Missing screenshot or S3 configuration");

const pageUrl = "https://example.com";
const endpoint = new URL(apiUrl);
endpoint.searchParams.set("url", pageUrl);
endpoint.searchParams.set("access_key", apiKey); // Use the provider's documented auth method.

const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 90_000);
let response;
try {
  response = await fetch(endpoint, { signal: controller.signal });
} finally {
  clearTimeout(timer);
}
if (!response.ok) throw new Error(`Screenshot API returned HTTP ${response.status}`);
const contentType = (response.headers.get("content-type") || "").split(";", 1)[0].toLowerCase();
if (!["image/png", "image/jpeg", "image/webp"].includes(contentType)) {
  throw new Error(`Expected image response, got ${contentType || "no content type"}`);
}
const image = Buffer.from(await response.arrayBuffer());
const extension = { "image/png": "png", "image/jpeg": "jpg", "image/webp": "webp" }[contentType];
const key = `captures/${new Date().toISOString().slice(0, 10)}/${randomUUID()}.${extension}`;

const s3 = new S3Client({ region });
await s3.send(new PutObjectCommand({
  Bucket: bucket,
  Key: key,
  Body: image,
  ContentType: contentType,
  Metadata: { "source-host": new URL(pageUrl).hostname },
}));

const downloadUrl = await getSignedUrl(
  s3,
  new (await import("@aws-sdk/client-s3")).GetObjectCommand({ Bucket: bucket, Key: key }),
  { expiresIn: 300 },
);
console.log(JSON.stringify({ bucket, key, region, downloadUrl }));

For very large images, use a streaming or multipart upload design rather than buffering the full body in a Buffer. Ensure retries do not accidentally overwrite an object unless replacement is intended.

8. AWS EC2 console screenshot variant

When the source is EC2’s console screenshot action, let the AWS SDK make the signed AWS request and decode the Base64 content field it returns. With Python and boto3, the core operation is:

import base64
from io import BytesIO
import boto3

region = "ap-south-1"
ec2 = boto3.client("ec2", region_name=region)
result = ec2.get_console_screenshot(InstanceId="i-0123456789abcdef0")
image_bytes = base64.b64decode(result["ImageData"], validate=True)

s3 = boto3.client("s3", region_name=region)
s3.upload_fileobj(
    BytesIO(image_bytes),
    S3_BUCKET,
    OBJECT_KEY,
    ExtraArgs={"ContentType": "image/jpeg"},
)

The caller needs permission for the EC2 action and the S3 upload. Use the AWS SDK rather than manually constructing a signed EC2 request. The result is an EC2 instance console image, not a screenshot of a website.

9. Key design, metadata, and overwrite behavior

  • Use unique keys for captures. A date prefix plus a capture or job identifier makes listing and lifecycle rules easier and reduces accidental replacement.
  • Choose a deliberate overwrite policy. Reusing a key replaces the current object unless versioning or another retention design changes that behavior. Make retries idempotent by associating each job with a stable key when appropriate.
  • Set content type accurately. Use image/png, image/jpeg, or image/webp to match the actual bytes.
  • Store useful, limited metadata. Capture time, output format, viewport, source host, and internal job ID can help. Avoid secrets, authorization values, sensitive full URLs, or personal data in keys and metadata.
  • Plan retention. Apply a lifecycle policy or application cleanup process if captures should expire. S3 storage and requests can incur charges under AWS pricing; estimate using your object size, retention period, request volume, and region rather than assuming this pipeline has a fixed cost.

10. Reliability, performance, and cost

  • Use bounded timeouts. Set separate connection and overall request timeouts. A full-page render can take longer than a basic page load, but an unbounded worker can tie up capacity indefinitely.
  • Retry selectively. Retry transient network failures, throttling, and selected server errors with exponential backoff and jitter. Do not endlessly retry invalid URLs, authorization failures, unsupported output settings, or deterministic page failures.
  • Make retries safe. Use a stable job ID or unique object key, and record whether capture and upload completed. If a worker crashes between upload and marking the job complete, a retry should not create uncontrolled duplicates.
  • Control memory and transfer costs. Stream binary responses into an upload path where possible. Full-page screenshots and Base64 JSON can consume significant memory. If captures are large, use multipart-capable SDK upload paths and enforce sensible input and output size limits.
  • Keep credentials out of logs and clients. Use workload identities or managed credentials, narrow S3 permissions to a prefix, and keep screenshot API credentials server-side.
  • Account for both services. The screenshot API may have its own billing and limits; S3 storage, requests, and data transfer may also cost money. The research available here does not support a fixed end-to-end price or latency estimate.

11. Troubleshooting

Symptom Likely cause Fix
Saved file is JSON or HTML instead of an image The API returned an error or a JSON wrapper, not raw bytes. Check HTTP status, content type, and provider response schema. Parse and decode the documented field or fetch the returned image URL.
Image decoder reports invalid data Base64 was not decoded, a truncated response was saved, or the extension/content type is wrong. Decode only the documented Base64 field; verify transfer completed and set the true format and content type.
Access denied on S3 upload The AWS identity lacks write permission, bucket policy denies the request, or encryption policy requires additional headers. Check the active identity and bucket policy; grant only the required object-write permission and satisfy bucket encryption requirements.
Authorization or signature mismatch Wrong credentials, region, signed headers, clock skew, or a changed request than the one that was signed. Use the bucket’s actual region, synchronize the host clock, and send the exact method, path, and headers used to create the signature.
Presigned link returns AccessDenied or expired The signer lacks read permission, the URL expired, or the request no longer matches the signed operation. Generate a fresh GET URL from an identity with read access and preserve its method and signed query parameters.
Capture key unexpectedly changes contents A retry or concurrent task uploaded to the same key. Use unique keys or a deliberate stable idempotency key; decide whether versioning is appropriate.
Download works for the app but not a recipient The recipient is using an expired URL or the bucket is private and no signed access was provided. Generate a new short-lived presigned GET link or proxy the download through an authenticated application endpoint.
Capture is stored in Mumbai but residency is still unclear The storage region was mistaken for the provider’s processing location. Verify render, temporary storage, logs, and delivery locations with the capture provider. S3 region alone cannot establish end-to-end residency.

12. Or skip the browser setup

ScreenshotNeo is a website screenshot API and MCP server. Call its endpoint from your trusted worker, then upload the returned image bytes to your Mumbai S3 bucket using the pattern above. Check the ScreenshotNeo API documentation for request options and response handling.

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,
)
r.raise_for_status()
with open("shot.webp", "wb") as f:
    f.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}`);
if (!res.ok) throw new Error(`ScreenshotNeo returned HTTP ${res.status}`);
const bytes = Buffer.from(await res.arrayBuffer());

With ScreenshotNeo, cookie banners, newsletter popups, and chat widgets are removed before the shot. Bot checks, blank pages, and failed loads are never billed. An MCP server lets AI agents take screenshots. The Free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000. Use the returned image bytes in your S3 upload step, and check the response headers for page verdict and billing information. Sign up for 1,000 free screenshots a month with no card.

Frequently asked questions

Is an S3 bucket in Mumbai enough to claim the entire screenshot workflow stays in India?

No. It identifies the S3 storage destination. The screenshot provider’s rendering and handling locations need separate confirmation.

Can I put the presigned URL in a public webpage?

You can, but anyone who obtains it can use it until it expires. Treat it as a bearer credential and share it only with intended recipients.

Should every capture use the same S3 object key?

Only if replacing the previous capture is intentional. Otherwise use a unique key or a stable per-job key with an explicit retry policy.

Does EC2 GetConsoleScreenshot capture a website?

No. It retrieves a running EC2 instance’s console image for troubleshooting. Use a website-rendering screenshot service for website captures.