How to Save Screenshot API Images Directly to Amazon S3
Send screenshot API output to Amazon S3 with a presigned PUT URL, or use a provider’s direct-delivery option. Includes runnable Node.js, Python, and cURL examples.
To save a screenshot API image in Amazon S3, have a trusted backend create a short-lived presigned PUT URL for a specific bucket and object key. Get the screenshot as binary bytes, then send those bytes as the body of an HTTP PUT to that URL. This keeps AWS credentials on your backend and lets the image travel from the screenshot service or application directly to S3 after the URL is issued.
Some screenshot APIs also support provider-managed delivery to S3. That can remove the upload step from your application, but it depends on the provider and its credential-handling model. The general presigned URL pattern works with screenshot APIs that return image bytes.
1. Choose a delivery method
| Method | How it works | Good fit when |
|---|---|---|
| Backend-issued presigned PUT | Your backend signs an upload URL for one S3 key. The screenshot bytes are PUT to S3 using that URL. | You want control over key names, expiry, permissions, and screenshot API choice. |
| Provider-managed S3 delivery | The screenshot provider accepts destination parameters and writes the result to your bucket. | Your chosen provider supports it and its credential model fits your security requirements. |
| Application-server relay | Your application downloads the screenshot bytes, then uploads them to S3 using server-side AWS credentials. | You need to inspect, transform, or validate the bytes before storage, and the extra network hop is acceptable. |
AWS presigned URLs allow another party to upload a specific object without receiving AWS credentials. The URL grants the permissions of the IAM principal that created it, and an upload to an existing key replaces that object. See the AWS guide to uploading objects with presigned URLs and its overview of presigned URL behavior.
2. Recommended flow: backend signs, client uploads
- The caller requests an upload authorization from your authenticated backend.
- The backend validates the request, chooses the object key, and signs a short-lived
PUTfor that key. - The caller obtains the screenshot image bytes from the screenshot API.
- The caller PUTs the raw bytes to the presigned URL, using the same content type if it was included in the signature.
- The application records the bucket and key, plus the upload result. Do not save the presigned URL as if it were a permanent image address.
The signing endpoint should derive the key from trusted application data or validate it against an allowed prefix. Do not let an unauthenticated caller choose arbitrary bucket names or keys.
Node.js backend: issue a presigned URL
This example uses Express and AWS SDK for JavaScript v3. The AWS SDK obtains credentials through its default provider chain, so configure an IAM role or another supported credential source for the server. The signing identity needs permission to put objects only in the required bucket and key prefix.
npm install express @aws-sdk/client-s3 @aws-sdk/s3-request-presigner
import express from 'express';
import { randomUUID } from 'node:crypto';
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
const app = express();
const bucket = process.env.S3_BUCKET;
const region = process.env.AWS_REGION;
if (!bucket || !region) throw new Error('Set S3_BUCKET and AWS_REGION');
const s3 = new S3Client({ region });
app.post('/api/screenshot-upload-url', async (req, res) => {
try {
// Authenticate and authorize this request in production before signing.
const contentType = 'image/png';
const key = `screenshots/${randomUUID()}.png`;
const command = new PutObjectCommand({
Bucket: bucket,
Key: key,
ContentType: contentType,
});
const uploadUrl = await getSignedUrl(s3, command, { expiresIn: 300 });
res.json({ uploadUrl, key, contentType, expiresIn: 300 });
} catch (error) {
console.error('Could not create screenshot upload URL', error);
res.status(500).json({ error: 'Could not create upload URL' });
}
});
app.listen(3000, () => console.log('Listening on port 3000'));
Set S3_BUCKET and AWS_REGION in the server environment. In a deployed environment, prefer a role attached to the workload over long-lived static keys. The example fixes the content type and generates a unique key. Adapt authentication, authorization, logging, and error handling to your application.
Python backend: issue a presigned URL
With Boto3 installed and AWS credentials configured for the backend identity, a small Flask endpoint can sign the same kind of upload:
pip install Flask boto3
import os
import uuid
from flask import Flask, jsonify
import boto3
app = Flask(__name__)
bucket = os.environ['S3_BUCKET']
region = os.environ['AWS_REGION']
s3 = boto3.client('s3', region_name=region)
@app.post('/api/screenshot-upload-url')
def create_upload_url():
# Authenticate and authorize this request in production before signing.
content_type = 'image/png'
key = f"screenshots/{uuid.uuid4()}.png"
url = s3.generate_presigned_url(
'put_object',
Params={'Bucket': bucket, 'Key': key, 'ContentType': content_type},
ExpiresIn=300,
HttpMethod='PUT',
)
return jsonify(uploadUrl=url, key=key, contentType=content_type, expiresIn=300)
if __name__ == '__main__':
app.run(port=3000)
Presigned URLs are bearer credentials: anyone who obtains one can use its allowed operation until it expires or the signing credentials stop being valid. Keep expiry brief, avoid logging the full URL, and return it only to the authorized caller.
3. Get the screenshot bytes and upload them
The upload body must be the image bytes, not JSON, a base64 string, or a multipart form unless your particular upload endpoint explicitly expects one. The example below uses ScreenshotNeo as the screenshot API. Its API returns the captured image from a GET request; use the returned bytes as the body of the S3 PUT. See the ScreenshotNeo API documentation for request options and response details.
Node.js: screenshot, then PUT to S3
const authorization = await fetch('https://your-app.example/api/screenshot-upload-url', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
});
if (!authorization.ok) throw new Error(`Signing failed: ${authorization.status}`);
const { uploadUrl, contentType } = await authorization.json();
const screenshotParams = new URLSearchParams({
access_key: process.env.SCREENSHOTNEO_API_KEY,
url: 'https://stripe.com',
format: 'png',
});
const screenshot = await fetch(`https://api.screenshotneo.com/v1/shot?${screenshotParams}`);
if (!screenshot.ok) throw new Error(`Screenshot request failed: ${screenshot.status}`);
const imageBytes = await screenshot.arrayBuffer();
const uploaded = await fetch(uploadUrl, {
method: 'PUT',
headers: { 'Content-Type': contentType },
body: imageBytes,
});
if (!uploaded.ok) throw new Error(`S3 upload failed: ${uploaded.status} ${await uploaded.text()}`);
console.log('Screenshot uploaded');
Run this in a server-side Node.js process with SCREENSHOTNEO_API_KEY set. If running in a browser, the signing endpoint still needs to be your own backend, and the S3 bucket needs a matching CORS rule. A browser cannot safely hold the AWS signing credentials.
Python: screenshot, then PUT to S3
This version uses requests for HTTP. It expects your application backend to provide the presigned URL.
pip install requests
import os
import requests
api_key = os.environ['SCREENSHOTNEO_API_KEY']
authorization = requests.post(
'https://your-app.example/api/screenshot-upload-url', timeout=15
)
authorization.raise_for_status()
upload = authorization.json()
screenshot = requests.get(
'https://api.screenshotneo.com/v1/shot',
params={'access_key': api_key, 'url': 'https://stripe.com', 'format': 'png'},
timeout=90,
)
screenshot.raise_for_status()
result = requests.put(
upload['uploadUrl'],
data=screenshot.content,
headers={'Content-Type': upload['contentType']},
timeout=90,
)
result.raise_for_status()
print('Screenshot uploaded')
cURL: upload an existing screenshot file
If the image has already been saved locally, request a presigned URL from your backend and upload the file. Do not add an AWS authorization header; the URL itself carries the signature.
curl -sS -X PUT \
-H 'Content-Type: image/png' \
--data-binary @shot.png \
'PRESIGNED_PUT_URL'
To capture with ScreenshotNeo first, save its binary response as a file, then run the upload command. Keep the API key out of browser-side code and command history where possible.
curl -G 'https://api.screenshotneo.com/v1/shot' \
-d 'access_key=YOUR_API_KEY' \
--data-urlencode 'url=https://stripe.com' \
-d 'format=png' -o shot.png
For a direct shell pipeline, use temporary files or a script that checks both HTTP responses. A naïve pipe can send an API error body to S3 as if it were a valid image.
4. Direct delivery from a screenshot provider
Provider-managed delivery is an alternative when the screenshot API explicitly supports an S3 destination. For example, ScreenshotsCloud documents parameters named s3_id, s3_secret, s3_bucket, and optional s3_path for direct upload. Review its AWS S3 Uploading documentation for the current request format before adapting an integration.
This feature can simplify application code because the provider performs the destination upload. Its credential model differs from a backend-signed URL: the documented interface passes an S3 ID and secret as request parameters. Before using it, check how credentials are transmitted, stored, and logged, and whether the provider supports the narrow permissions and key constraints your security model requires. Do not assume this has the same exposure profile as a URL signed by your own backend.
5. Browser uploads and S3 CORS
Server-to-server PUT requests do not use browser CORS. A browser uploading directly to S3 does. Configure the bucket’s CORS policy for the exact web origin, PUT method, and headers the browser sends. Keep the allowed origin and headers as narrow as your application permits; CORS controls which browser origins may make cross-origin requests, but it does not authorize the upload.
AWS’s S3 CORS documentation describes bucket CORS rules. Its example architecture for direct browser uploads also explains the signed-URL pattern; use a restrictive production policy rather than copying a broad wildcard example.
6. Object keys, content types, and other options
| Choice | Recommendation | Reason |
|---|---|---|
| Bucket and region | Choose the bucket on the backend and sign for its actual region. | A region mismatch can cause request or signature failures. |
| Object key | Use a unique, application-controlled key such as screenshots/{id}.png. |
PUT to an occupied key replaces the prior object. |
| Format and extension | Choose PNG, JPEG, or WebP deliberately and make the key extension agree. | Consumers often infer image type from metadata or filename. |
| Content-Type | Sign a content type if useful, and send exactly that value on upload. | Signed headers must match the actual request. |
| Expiry | Use the shortest lifetime that fits screenshot capture and upload time. | The URL can be reused until expiry, subject to credential validity. |
| Overwrite policy | Use unique keys for immutable captures, or explicitly manage replacements. | A repeat upload to the same signed key overwrites the object. |
| Visibility | Keep objects private unless public access is an intentional product decision. | Upload permission and read permission are separate concerns. |
Presigned URL expiry can be set as high as seven days with AWS CLI or SDK tools, but a screenshot upload generally benefits from a much shorter window. The effective lifetime can be shorter if the credentials used to sign the URL expire first. Do not treat expiry as protection against a URL leaked while it remains valid.
7. Reliability, performance, and cost
- Network path: Direct-to-S3 upload avoids routing the image bytes through your application server after signing. With an application-server relay, the server handles both the screenshot download and S3 upload, which adds a transfer hop and uses its network bandwidth.
- Retries: Retry transient network failures with bounded backoff. If a URL expires before a retry, request a new one. Reusing the same URL and key can replace the object, so use a stable operation identifier when retries must be idempotent.
- Partial success: The screenshot may succeed while the S3 upload fails. Track these as separate stages, and do not report a completed capture until the upload has succeeded.
- Validation: Check the screenshot response status before uploading. Optionally validate expected content type or image signatures in a trusted backend before recording success.
- Payload size: For ordinary screenshot images, a single PUT is straightforward. If your generated objects become large enough that interrupted uploads are costly, evaluate S3 multipart upload; it requires a different signing and completion flow.
- Costs: Account for screenshot API usage, S3 storage, S3 requests, and data transfer according to the services and regions you use. The research sources provide no topic-specific benchmark or universal cost estimate; compare your own image sizes, retention period, request volume, and transfer paths.
8. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
SignatureDoesNotMatch |
The URL was altered, the method or signed header differs, content type does not match, the region is wrong, or system time is skewed. | Use the exact URL unchanged; use PUT; match signed headers; verify bucket region and clock synchronization. AWS lists these checks in its presigned upload guide. |
AccessDenied |
The signing identity lacks permission for the target key, or a bucket policy blocks the operation. | Check the signer’s object-write permissions and applicable bucket policy. Limit permissions to the needed bucket and prefix. |
ExpiredToken or expired URL response |
The URL expired, or the temporary credentials used to sign it expired first. | Request a fresh URL immediately before upload and avoid long delays between signing and transfer. |
| Browser reports a CORS error | The bucket CORS policy does not allow the page origin, PUT method, or request headers. | Add a narrowly scoped rule for the exact origin and request shape. Confirm the OPTIONS preflight response in browser developer tools. |
| Object exists but is not a valid image | An API error response, JSON body, or base64 text was uploaded instead of image bytes. | Check the screenshot HTTP status before reading bytes; send raw binary as the PUT body. |
| Unexpected image type or metadata | File extension, screenshot format, and Content-Type disagree. | Set the screenshot format explicitly and align the S3 key suffix and signed Content-Type. |
| Existing screenshot disappeared | The upload reused an object key and replaced the previous object. | Generate unique keys or implement intentional versioning/overwrite behavior. |
| Upload succeeds locally but fails in deployment | The deployed backend has no AWS credentials, uses a different region, or its role lacks the necessary permission. | Configure the deployment’s workload identity and check its effective IAM permissions and region. |
9. Or skip the browser setup
ScreenshotNeo is a website screenshot API and MCP server for developers. Its one-call API returns an image; the code below requests a PNG that you can then upload to S3 using the presigned PUT flow above. See the ScreenshotNeo documentation for the API options.
curl -G 'https://api.screenshotneo.com/v1/shot' \
-d access_key=YOUR_API_KEY \
--data-urlencode url=https://stripe.com \
-d format=png -o shot.png
curl -sS -X PUT \
-H 'Content-Type: image/png' \
--data-binary @shot.png \
'PRESIGNED_PUT_URL'
ScreenshotNeo removes cookie banners, newsletter popups, and chat widgets before the screenshot. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers identify the page verdict and billing status. Its MCP server gives AI agents tools to take screenshots, get page information, and capture PDFs. The Free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 screenshots.
Sign up for ScreenshotNeo and get 1,000 screenshots a month free, with no card required.
10. Frequently asked questions
Can I upload the screenshot without saving it to disk?
Yes. Keep the response body in memory and use it as the body of the S3 PUT. The Node.js and Python examples do this.
Does a presigned URL make the uploaded image public?
No. It authorizes the signed operation. Whether the object can be read publicly depends on your bucket and object access configuration.
Can the same presigned PUT URL be reused?
It can be used until it expires, but repeated uploads to its key replace the object. Use a unique key if each capture must be retained.
Do I need CORS for a server-side upload?
No. CORS is enforced by browsers. It matters when JavaScript in a web page sends the PUT directly to S3.
Can every screenshot API deliver straight to S3?
No. Direct provider delivery is provider-specific. If the API returns image bytes, your application can generally upload them using a presigned URL or a server-side S3 client.


