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.

S3
universal protocol
5+
compatible providers
< 30 s
from request to your bucket
Create a free account or sign in to see your API key pre-filled in the examples.

How it works

  1. Submit a capture via POST /capture with a callback_url pointing to your server.
  2. Shotbot renders the page and posts the image file as multipart to your endpoint.
  3. 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
<?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
<?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
<?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.

Python
# 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

Python
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.

Python
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.cloud
  • region: fr-par
  • use_path_style_endpoint: false

OVH Object Storage

  • endpoint: https://s3.gra.io.cloud.ovh.net
  • region: gra
  • use_path_style_endpoint: true

Cloudflare R2

  • endpoint: https://<account_id>.r2.cloudflarestorage.com
  • region: auto
  • use_path_style_endpoint: false

No egress fees.

Backblaze B2

  • endpoint: https://s3.us-west-004.backblazeb2.com
  • region: us-west-004
  • use_path_style_endpoint: false

Get started

Free account with 200 captures per month. No credit card required.

Related use cases