Upload API

Upload images in batch

Stores up to 10 images (max 50 MB combined) from repeated multipart "file" parts. Files are processed in order and results are per-file: the response is 201 when every file succeeds and 207 when results are mixed, with each item carrying either the stored upload or its own error code. When every file fails, the status reflects the per-file failures (402, 413, 415, 422, or 500) but the body is still the per-item result object, not the error envelope — request-level failures (auth, rate limit, malformed multipart, batch size or count) use the plain error envelope. Each stored file consumes one upload unit of the monthly quota.

POST/api/v1/upload/batch

Authentication

Send your API key as a bearer token on every request: Authorization: Bearer sw_.... Create keys in the dashboard; see the authentication docs for details.

Parameters

Request content type: multipart/form-data

ParameterTypeDefaultDescription
filerequiredfile (binary, repeated)One part per image, all named "file". Up to 10 files and 50 MB of combined file data per request; each file also has the individual 10 MB limit and byte-signature validation of the single-upload endpoint.
expires_inintegerLifetime of the link in seconds, from 60 (one minute) to 31,536,000 (one year). Defaults when omitted: no scheduled expiry on an active paid plan; the 30-day plan maximum on Free. Free-plan links are capped at 30 days regardless of the value sent. Applies to every file in the batch. Range: 60–31,536,000.

Examples

cURL
curl -X POST https://shotwisp.com/api/v1/upload/batch \
  -H "Authorization: Bearer $SHOTWISP_API_KEY" \
  -F "file=@./first.png" \
  -F "file=@./second.jpg" \
  -F "expires_in=86400"
Node.js
const form = new FormData();
for (const file of files) form.append("file", file); // repeat the "file" part
form.append("expires_in", "86400"); // optional

const res = await fetch("https://shotwisp.com/api/v1/upload/batch", {
  method: "POST",
  headers: { Authorization: `Bearer ${process.env.SHOTWISP_API_KEY}` },
  body: form,
});
const batch = await res.json(); // res.status: 201 all stored, 207 mixed
for (const item of batch.items) {
  console.log(item.ok ? item.upload.url : `${item.source_filename}: ${item.error.code}`);
}
Python
import os, requests

res = requests.post(
    "https://shotwisp.com/api/v1/upload/batch",
    headers={"Authorization": f"Bearer {os.environ['SHOTWISP_API_KEY']}"},
    files=[
        ("file", open("first.png", "rb")),
        ("file", open("second.jpg", "rb")),
    ],
    data={"expires_in": "86400"},  # optional
)
for item in res.json()["items"]:  # res.status_code: 201 all stored, 207 mixed
    print(item["upload"]["url"] if item["ok"] else item["error"]["code"])

Response

Success status: 201 or 207. Per-file results in request order. 201 when all files stored; 207 when some failed. Content types: application/json.

207 Multi-Status
{
  "items": [
    {
      "ok": true,
      "index": 0,
      "source_filename": "first.png",
      "upload": {
        "id": "u7Kd2mXq9RfW4bZn",
        "slug": "k7mwq2ax",
        "url": "https://shotwisp.com/i/k7mwq2ax",
        "file_url": "https://shotwisp.com/f/k7mwq2ax",
        "delete_url": "https://shotwisp.com/api/uploads/u7Kd2mXq9RfW4bZn/delete?token=Fj3kW9sLq2Xv7Rp4Tz8mNc5d",
        "expires_at": "2026-08-13T14:05:00.000Z",
        "size_bytes": 481290,
        "content_type": "image/png",
        "filename": "first.png"
      }
    },
    {
      "ok": false,
      "index": 1,
      "source_filename": "second.jpg",
      "error": {
        "code": "unsupported_type",
        "message": "Unsupported image type. The file's contents do not match a supported format."
      }
    }
  ],
  "summary": { "total": 2, "succeeded": 1, "failed": 1 }
}
Every response carries rate-limit headers; metered successes add quota headers. Byte responses (images, PDFs) carry the capture id in x-shotwisp-id.

Errors

Every error uses the envelope {"error": {"code", "message"}}. This endpoint can return:

CodeHTTP statusRetryableMeaning
unauthorized401noThe API key is missing, malformed, revoked, or the account is suspended or unverified. Do not retry with the same key. Fix the Authorization header or create a new API key.
quota_exceeded402noThe monthly upload or render quota is used up and overages are off or unavailable on the plan. Do not retry until the monthly quota resets, the plan is upgraded, or overages are enabled.
batch_too_large413noCombined file data in a batch exceeds 50 MB. Do not retry unchanged. Split the batch or reduce combined size.
file_too_large413noAn uploaded file exceeds the 10 MB per-file limit. Do not retry unchanged. Reduce the file below the limit.
unsupported_type415noThe file's bytes do not match a supported image format (PNG, JPEG, GIF, WebP, SVG, AVIF). Do not retry unchanged. Convert the file to a supported image format.
too_many_files422noA batch contains more than 10 file parts. Do not retry unchanged. Send fewer files per batch.
validation_error422noA request field failed validation. The message states which field and why. Do not retry unchanged. The message names the failing field; fix it first.
rate_limited429yesThe plan's per-minute request budget is spent. Rejected requests are not metered. Retry after waiting the number of seconds in the Retry-After header, then back off exponentially with jitter.
internal500yesUnexpected error on Shotwisp's side. The request was not metered. Safe to retry with exponential backoff.

Limits and machine-readable spec

Plan quotas, rate limits, and every fixed limit are consolidated in Limits. This endpoint is also fully described in the OpenAPI 3.1 specification (operationId createUploadBatch). Building with an AI agent? Read the AI agent guide.