Uploads
Hand a screenshot to StoreShot once (multipart, from a URL or with a presigned upload) and reference its id in generations and sets.
Generations and screenshot sets take their screenshots as uploads: you hand an image over once and
pass its id. There are three ways to upload, and each ends in the same ready upload with the
same metadata (width, height, orientation, sha256). Uploads need the uploads:write scope.
The Node.js and Python samples on this page continue from the first one (api, headers).
Multipart
Send the file as the file part of a multipart/form-data body. Simple, and right for most
integrations.
curl
UPLOAD=$(curl -sS https://api.storeshot.art/v1/uploads \
-H "Authorization: Bearer $STORESHOT_API_KEY" -F file=@home.png)
UPLOAD_ID=$(echo "$UPLOAD" | jq -r .id)
echo "$UPLOAD" | jq '{id, status, width, height, orientation}'Node.js
import { readFile } from 'node:fs/promises';
const api = 'https://api.storeshot.art/v1';
const headers = { Authorization: `Bearer ${process.env.STORESHOT_API_KEY}` };
const form = new FormData();
form.append('file', new Blob([await readFile('home.png')]), 'home.png');
const upload = await (await fetch(`${api}/uploads`, { method: 'POST', headers, body: form })).json();
console.log(upload.id, upload.status, upload.width, upload.height);Python
import os, requests
api = "https://api.storeshot.art/v1"
headers = {"Authorization": f"Bearer {os.environ['STORESHOT_API_KEY']}"}
with open("home.png", "rb") as f:
upload = requests.post(f"{api}/uploads", headers=headers, files={"file": f}).json()
print(upload["id"], upload["status"], upload["width"], upload["height"])From a URL
Send { "url": "https://…" } and the server fetches the image once. Only https:// URLs on port
443 that resolve to public addresses are fetched, with at most 3 redirects and 15 seconds in total;
a failure is 422 URL_FETCH_FAILED with details.reason. Only the origin and path of the URL are
stored (sourceUrl).
curl
curl -sS https://api.storeshot.art/v1/uploads \
-H "Authorization: Bearer $STORESHOT_API_KEY" \
-H 'Content-Type: application/json' \
-d '{"url":"https://example.com/screenshots/home.png"}' \
| jq '{id, status, source, sourceUrl}'Node.js
const imported = await (
await fetch(`${api}/uploads`, {
method: 'POST',
headers: { ...headers, 'Content-Type': 'application/json' },
body: JSON.stringify({ url: 'https://example.com/screenshots/home.png' }),
})
).json();
console.log(imported.id, imported.status, imported.sourceUrl);Python
imported = requests.post(
f"{api}/uploads", headers=headers, json={"url": "https://example.com/screenshots/home.png"}
).json()
print(imported["id"], imported["status"], imported["sourceUrl"])Presigned upload
For large files, or to keep image bytes off your API traffic: ask for an upload URL with the exact
type and size, PUT the file there from your server with exactly the returned headers (they are
signed), then complete the upload. The URL is valid for 15 minutes; a pending upload that is never
completed expires after 24 hours. Completing before the file arrived is
409 UPLOAD_NOT_READY (details.reason: object_missing): upload, then retry.
curl
PRESIGNED=$(curl -sS https://api.storeshot.art/v1/uploads/presign \
-H "Authorization: Bearer $STORESHOT_API_KEY" \
-H 'Content-Type: application/json' \
-d '{"contentType":"image/png","bytes":'"$(wc -c < home.png)"'}')
curl -sS -X PUT --data-binary @home.png -H 'Content-Type: image/png' \
"$(echo "$PRESIGNED" | jq -r .uploadUrl)"
curl -sS -X POST "https://api.storeshot.art/v1/uploads/$(echo "$PRESIGNED" | jq -r .upload.id)/complete" \
-H "Authorization: Bearer $STORESHOT_API_KEY" | jq '{id, status, source, bytes}'Node.js
const bytes = await readFile('home.png');
const presigned = await (
await fetch(`${api}/uploads/presign`, {
method: 'POST',
headers: { ...headers, 'Content-Type': 'application/json' },
body: JSON.stringify({ contentType: 'image/png', bytes: bytes.length }),
})
).json();
const put = await fetch(presigned.uploadUrl, {
method: presigned.method,
headers: { 'Content-Type': presigned.headers['Content-Type'] },
body: bytes,
});
if (!put.ok) throw new Error(`upload failed: ${put.status}`);
const completed = await (
await fetch(`${api}/uploads/${presigned.upload.id}/complete`, { method: 'POST', headers })
).json();
console.log(completed.id, completed.status, completed.source);Python
with open("home.png", "rb") as f:
data = f.read()
presigned = requests.post(
f"{api}/uploads/presign",
headers=headers,
json={"contentType": "image/png", "bytes": len(data)},
).json()
requests.put(presigned["uploadUrl"], data=data, headers=presigned["headers"]).raise_for_status()
completed = requests.post(
f"{api}/uploads/{presigned['upload']['id']}/complete", headers=headers
).json()
print(completed["id"], completed["status"], completed["source"])Read, list and delete
# One upload, by id
curl -sS "https://api.storeshot.art/v1/uploads/$UPLOAD_ID" \
-H "Authorization: Bearer $STORESHOT_API_KEY" | jq '{id, status, expiresAt}'
# Newest first; pass nextCursor back as ?cursor= for the next page
curl -sS 'https://api.storeshot.art/v1/uploads?limit=20' \
-H "Authorization: Bearer $STORESHOT_API_KEY" | jq '{count: (.data | length), nextCursor}'
# Delete: the file is removed now (204)
curl -sS -X DELETE "https://api.storeshot.art/v1/uploads/$UPLOAD_ID" \
-H "Authorization: Bearer $STORESHOT_API_KEY" -o /dev/null -w '%{http_code}\n'
Limits
- Formats: PNG, JPEG or WebP, detected from the file’s bytes (HEIC is refused with
415 UNSUPPORTED_FORMAT). - Size: up to 20 MiB, 50 megapixels and 16384 px on the longest edge.
- Stored uploads per account (pending and ready together):
| Plan | Uploads | Total size |
|---|---|---|
| No plan | 500 | 2048 MB |
| Pro | 1000 | 5120 MB |
| Agency | 2000 | 10240 MB |
| Scale | 5000 | 20480 MB |
Over the quota a new upload is 403 FORBIDDEN with details.reason: upload_quota_exceeded
(details.kind count or bytes): delete uploads you no longer need. A file over the size limits
is 413 IMAGE_TOO_LARGE; anything that is not a PNG, JPEG or WebP image (by its bytes, never its
name) is 415 UNSUPPORTED_FORMAT. Export HEIC photos as PNG or JPEG first.
Retention
Uploads are deleted 30 days after they were created (their file is removed; generations made from
them keep their results). Pending or failed uploads expire after 24 hours. DELETE removes the
file at once. A generation queued from an upload that expired before it started fails with
UPLOAD_NOT_READY and, with a live key, its credit is refunded. Every retention period of the API is listed in
the data retention policy.