Screenshots
straight to S3.
Submit a capture via the Shotbot API with a callback URL. When the render is ready, Shotbot posts the file to your endpoint — you upload it to your bucket from there.
How it works
- Submit a capture via
POST /capturewith acallback_urlpointing to your server. - Shotbot renders the page and posts the image file as multipart to your endpoint.
- Your handler verifies the
callback_secret, then uploads the file to your S3 bucket.
No dependency on Shotbot for storage — your bucket stays fully under your control.
Step 1 — The callback handler
Create a callback-s3.php file on your server, publicly reachable.
Install the AWS SDK first: composer require aws/aws-sdk-php.
<?php
// callback-s3.php — receives the Shotbot callback and uploads to S3
require 'vendor/autoload.php';
use Aws\S3\S3Client;
define('CALLBACK_SECRET', 'your_secret_here');
define('S3_REGION', 'us-east-1');
define('S3_ENDPOINT', null); // null = native AWS S3
define('S3_ACCESS_KEY', getenv('S3_ACCESS_KEY'));
define('S3_SECRET_KEY', getenv('S3_SECRET_KEY'));
define('S3_BUCKET', 'my-bucket');
// Verify the secret
if (!hash_equals(CALLBACK_SECRET, $_POST['callback_secret'] ?? '')) {
http_response_code(403); exit;
}
// Ignore failures (status=ERR)
if (($_POST['status'] ?? '') !== 'OK') {
http_response_code(200); exit;
}
$token = preg_replace('/[^A-Za-z0-9]/', '', $_POST['token'] ?? '');
$format = in_array($_POST['format'] ?? '', ['jpg','png','webp','avif','pdf'])
? $_POST['format'] : 'jpg';
$file = $_FILES['file'] ?? null;
if (!$token || !$file || $file['error'] !== UPLOAD_ERR_OK) {
http_response_code(400); exit;
}
$s3 = new S3Client([
'version' => 'latest',
'region' => S3_REGION,
'endpoint' => S3_ENDPOINT,
'credentials' => ['key' => S3_ACCESS_KEY, 'secret' => S3_SECRET_KEY],
'use_path_style_endpoint' => false,
]);
$key = 'screenshots/' . date('Y/m/d/') . $token . '.' . $format;
$s3->putObject([
'Bucket' => S3_BUCKET,
'Key' => $key,
'Body' => fopen($file['tmp_name'], 'rb'),
'ContentType' => ($format === 'pdf') ? 'application/pdf' : 'image/' . $format,
'ACL' => 'public-read',
]);
http_response_code(200);
Step 2 — Trigger the capture
<?php
$ch = curl_init('https://api.shotbot.net/capture');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_POSTFIELDS => json_encode([
'key' => 'YOUR_API_KEY',
'url' => 'https://example.com',
'format' => 'webp',
'callback_url' => 'https://yourapp.com/callback-s3.php',
'callback_secret' => 'your_secret_here',
]),
]);
$res = json_decode(curl_exec($ch), true);
echo $res['token']; // identifies this capture in the callback
Batch: 500 URLs to your bucket
With the batch API, submit up to 500 URLs (5,000 with Shotbot Pro) in a single request. Each completed capture posts individually to your endpoint.
<?php
$ch = curl_init('https://api.shotbot.net/capture/batch');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_POSTFIELDS => json_encode([
'key' => 'YOUR_API_KEY',
'callback_url' => 'https://yourapp.com/callback-s3.php',
'callback_secret' => 'your_secret_here',
'format' => 'webp',
'jobs' => [
['url' => 'https://example.com'],
['url' => 'https://example.org'],
],
]),
]);
$res = json_decode(curl_exec($ch), true);
echo $res['queued'] . ' captures queued';
Step 1 — The callback handler
Create a Flask endpoint publicly reachable on your server.
Install dependencies: pip install flask boto3.
# callback_s3.py — receives the Shotbot callback and uploads to S3
# pip install flask boto3
import hmac, os, re
from datetime import date
import boto3
from flask import Flask, request, abort
app = Flask(__name__)
CALLBACK_SECRET = 'your_secret_here'
S3_BUCKET = 'my-bucket'
s3 = boto3.client(
's3',
region_name = 'us-east-1',
endpoint_url = None, # swap for Scaleway/OVH/R2/B2
aws_access_key_id = os.environ['S3_ACCESS_KEY'],
aws_secret_access_key = os.environ['S3_SECRET_KEY'],
)
@app.post('/callback-s3')
def callback():
received = request.form.get('callback_secret', '')
if not hmac.compare_digest(CALLBACK_SECRET, received):
abort(403)
if request.form.get('status') != 'OK':
return '', 200
token = re.sub(r'[^A-Za-z0-9]', '', request.form.get('token', ''))
fmt = request.form.get('format', 'jpg')
if fmt not in ('jpg', 'png', 'webp', 'avif', 'pdf'):
fmt = 'jpg'
file = request.files.get('file')
if not token or not file:
abort(400)
key = f"screenshots/{date.today().strftime('%Y/%m/%d/')}{token}.{fmt}"
content_type = 'application/pdf' if fmt == 'pdf' else f'image/{fmt}'
s3.put_object(
Bucket = S3_BUCKET,
Key = key,
Body = file.read(),
ContentType = content_type,
ACL = 'public-read',
)
return '', 200
Step 2 — Trigger the capture
import requests
res = requests.post('https://api.shotbot.net/capture', json={
'key': 'YOUR_API_KEY',
'url': 'https://example.com',
'format': 'webp',
'callback_url': 'https://yourapp.com/callback-s3',
'callback_secret': 'your_secret_here',
})
data = res.json()
print(data['token']) # identifies this capture in the callback
Batch: 500 URLs to your bucket
With the batch API, submit up to 500 URLs (5,000 with Shotbot Pro) in a single request. Each completed capture posts individually to your endpoint.
import requests
res = requests.post('https://api.shotbot.net/capture/batch', json={
'key': 'YOUR_API_KEY',
'callback_url': 'https://yourapp.com/callback-s3',
'callback_secret': 'your_secret_here',
'format': 'webp',
'jobs': [
{'url': 'https://example.com'},
{'url': 'https://example.org'},
],
})
data = res.json()
print(f"{data['queued']} captures queued")
S3-compatible providers
All providers work with the same SDK — just swap endpoint and region:
AWS S3
endpoint: null (AWS default)region:us-east-1,eu-west-3…use_path_style_endpoint:false
Scaleway Object Storage
endpoint:https://s3.fr-par.scw.cloudregion:fr-paruse_path_style_endpoint:false
OVH Object Storage
endpoint:https://s3.gra.io.cloud.ovh.netregion:grause_path_style_endpoint:true
Cloudflare R2
endpoint:https://<account_id>.r2.cloudflarestorage.comregion:autouse_path_style_endpoint:false
No egress fees.
Backblaze B2
endpoint:https://s3.us-west-004.backblazeb2.comregion:us-west-004use_path_style_endpoint:false
Get started
Free account with 200 captures per month. No credit card required.