# Uploads

> HTML version: https://storeshot.art/docs/uploads/

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.

<!-- tabs -->
```sh
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}'
```

```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`).

<!-- tabs -->
```sh
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}'
```

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

<!-- tabs -->
```sh
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}'
```

```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

```sh
# 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](/data-retention/).
