Docs menu

Uploads

Hand a screenshot to StoreShot once (multipart, from a URL or with a presigned upload) and reference its id in generations and sets.

View as Markdown

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):
PlanUploadsTotal size
No plan5002048 MB
Pro10005120 MB
Agency200010240 MB
Scale500020480 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.